// SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.24; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /// @title SurplusBuffer — waterfall steps 3–4 (White Paper v3.2 §7, §8) /// @notice Vig revenue (USDT) fills the buffer to its target FIRST; only excess is /// released for RAIN buyback-and-burn ("burning while the buffer is empty /// would waste capital that may be needed for solvency"). The buffer absorbs /// residual bad debt. The RAIN backstop (step 4) is timelock-gated and /// per-epoch capped — measured, never a reflexive mint. contract SurplusBuffer is ReentrancyGuard { using SafeERC20 for IERC20; error NotTimelock(); error NotLiquidationEngine(); error NotDeployer(); error AlreadyFinalized(); error BufferBelowTarget(); error BackstopCapExceeded(); address public immutable deployer; IERC20 public immutable usdt; IERC20 public immutable rainToken; address public timelock; address public liquidationEngine; address public burnSink; // buyback executor (timelock-approved market buyer+burner) bool public finalized; uint256 public bufferTarget; // 6-dec USDT uint256 public badDebtOutstanding; // WAD accounting of unabsorbed bad debt // capped RAIN backstop (waterfall step 4) uint256 public backstopEpochCap; // RAIN per epoch uint256 public backstopEpochLength = 7 days; uint256 public epochStart; uint256 public soldThisEpoch; event RevenueReceived(uint256 amount6); event BadDebtAbsorbed(uint256 requestedWad, uint256 absorbedWad); event ExcessReleased(uint256 amount6, address to); event BackstopSold(uint256 rainAmount, address to); event TargetSet(uint256 target); uint256 private constant SCALE = 1e12; constructor(address _usdt, address _rain) { deployer = msg.sender; usdt = IERC20(_usdt); rainToken = IERC20(_rain); } function setup(address _timelock, address _liquidationEngine, address _burnSink) external { if (msg.sender != deployer) revert NotDeployer(); if (finalized) revert AlreadyFinalized(); timelock = _timelock; liquidationEngine = _liquidationEngine; burnSink = _burnSink; finalized = true; } function setTarget(uint256 target6) external { if (msg.sender != timelock) revert NotTimelock(); bufferTarget = target6; emit TargetSet(target6); } function setBackstopCap(uint256 rainPerEpoch, uint256 epochLength) external { if (msg.sender != timelock) revert NotTimelock(); backstopEpochCap = rainPerEpoch; backstopEpochLength = epochLength; } /// @notice Anyone can push vig revenue in (market adapters forward the 5% vig). function receiveRevenue(uint256 amount6) external nonReentrant { usdt.safeTransferFrom(msg.sender, address(this), amount6); emit RevenueReceived(amount6); } /// @notice Absorb bad debt (LiquidationEngine only). Returns how much was covered. function absorbBadDebt(uint256 amountWad) external returns (uint256 absorbed) { if (msg.sender != liquidationEngine) revert NotLiquidationEngine(); uint256 have6 = usdt.balanceOf(address(this)); uint256 need6 = (amountWad + SCALE - 1) / SCALE; uint256 cover6 = need6 > have6 ? have6 : need6; absorbed = cover6 * SCALE; if (absorbed > amountWad) absorbed = amountWad; badDebtOutstanding += amountWad - absorbed; // covered portion stays in the buffer's accounting; uncovered rolls to backstop emit BadDebtAbsorbed(amountWad, absorbed); } /// @notice Release ONLY the excess above target for buyback-and-burn (WP §7 step 3, /// §8: "the vig first routes to a Surplus Buffer until it reaches its target; /// only the excess is used for RAIN buyback-and-burn"). function releaseExcessForBurn(uint256 amount6) external nonReentrant { if (msg.sender != timelock) revert NotTimelock(); uint256 bal = usdt.balanceOf(address(this)); if (bal < bufferTarget + amount6) revert BufferBelowTarget(); usdt.safeTransfer(burnSink, amount6); emit ExcessReleased(amount6, burnSink); } /// @notice Waterfall step 4: capped, measured treasury-RAIN sale. Timelock-gated, /// per-epoch cap enforced in code. Never an unlimited reflexive mint. function backstopSellRain(uint256 rainAmount, address to) external nonReentrant { if (msg.sender != timelock) revert NotTimelock(); if (block.timestamp >= epochStart + backstopEpochLength) { epochStart = block.timestamp; soldThisEpoch = 0; } if (soldThisEpoch + rainAmount > backstopEpochCap) revert BackstopCapExceeded(); soldThisEpoch += rainAmount; rainToken.safeTransfer(to, rainAmount); emit BackstopSold(rainAmount, to); } }