SKIP TO CONTENT

Demo build — mock protocol data. No chain, no ABIs yet.

  • SYNCING: ……

K401Buyback.sol

Inverse bond. A standing, permissionless bid at rfv() - 1.5%, so the protocol quotes both sides of backing.

109 lines4.4 KBSolidity
Source of src/K401Buyback.sol, 109 lines of Solidity
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.24;
3 
4import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
6import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
7import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8import {IERC20Like, IK401, IK401Oracle, IK401Treasury} from "./interfaces/IK401Interfaces.sol";
9 
10/**
11 * @title K401Buyback — the inverse bond
12 * @notice A standing, permissionless bid at `rfv() - 1.5%`. The protocol quotes BOTH sides
13 * around backing: bonds sell above NAV, the buyback buys just under RFV.
14 *
15 * @dev The 401K bought is burned immediately, and the USDG paid comes from this contract's
16 * own balance (the 5% FeeSplitter stream), never from the treasury's RFV bucket.
17 * Supply falls while Bucket A is untouched, so `rfvPerToken()` strictly increases.
18 */
19contract K401Buyback is Ownable, ReentrancyGuard {
20 using SafeERC20 for IERC20;
21 
22 uint256 public constant WAD = 1e18;
23 uint256 public constant BPS = 10_000;
24 /// @notice Spread below RFV. Immutable.
25 uint256 public constant DISCOUNT_BPS = 150; // 1.50%
26 
27 IK401 public immutable k401;
28 IERC20 public immutable usdg;
29 uint8 public immutable usdgDecimals;
30 IK401Treasury public immutable treasury;
31 IK401Oracle public immutable oracle;
32 
33 uint256 public totalBoughtBack;
34 uint256 public totalUsdgSpent;
35 
36 event BoughtBack(address indexed seller, uint256 amount401k, uint256 usdgPaid, uint256 priceWad);
37 
38 error ZeroAddress();
39 error ZeroAmount();
40 error StaleOracle();
41 error SlippageExceeded();
42 error InsufficientBidLiquidity();
43 
44 constructor(address k401_, address usdg_, address treasury_, address oracle_, address owner_) Ownable(owner_) {
45 if (k401_ == address(0) || usdg_ == address(0) || treasury_ == address(0) || oracle_ == address(0)) {
46 revert ZeroAddress();
47 }
48 k401 = IK401(k401_);
49 usdg = IERC20(usdg_);
50 usdgDecimals = IERC20Like(usdg_).decimals();
51 treasury = IK401Treasury(treasury_);
52 oracle = IK401Oracle(oracle_);
53 }
54 
55 /// @notice Standing bid: RFV per token minus 150 bps, 18-decimal USD.
56 function bidPrice() public view returns (uint256) {
57 return (treasury.rfv() * (BPS - DISCOUNT_BPS)) / BPS;
58 }
59 
60 /// @notice USDG this contract can still pay out, in 18-decimal USD.
61 function bidCapacityWad() public view returns (uint256) {
62 return _toWad(usdg.balanceOf(address(this)), usdgDecimals);
63 }
64 
65 function quote(uint256 amount401k) public view returns (uint256 usdgOut) {
66 return _fromWad((amount401k * bidPrice()) / WAD, usdgDecimals);
67 }
68 
69 /**
70 * @notice Sell 401K into the standing bid. Permissionless settlement.
71 * @dev Fail-closed on a stale oracle even though the price itself comes from RFV:
72 * a dead oracle means the rest of the system is halted and the buyback must
73 * not be the one live venue an attacker can arbitrage against.
74 */
75 function sell(uint256 amount401k, uint256 minUsdgOut) external nonReentrant returns (uint256 usdgOut) {
76 if (amount401k == 0) revert ZeroAmount();
77 if (!oracle.isValid()) revert StaleOracle();
78 
79 uint256 price = bidPrice();
80 usdgOut = _fromWad((amount401k * price) / WAD, usdgDecimals);
81 if (usdgOut == 0) revert ZeroAmount();
82 if (usdgOut < minUsdgOut) revert SlippageExceeded();
83 if (usdgOut > usdg.balanceOf(address(this))) revert InsufficientBidLiquidity();
84 
85 // Effects
86 totalBoughtBack += amount401k;
87 totalUsdgSpent += usdgOut;
88 
89 // Interactions: take the 401K, burn it, then pay.
90 IERC20(address(k401)).safeTransferFrom(msg.sender, address(this), amount401k);
91 k401.burnFrom(address(this), amount401k);
92 usdg.safeTransfer(msg.sender, usdgOut);
93 
94 emit BoughtBack(msg.sender, amount401k, usdgOut, price);
95 }
96 
97 function _toWad(uint256 amount, uint8 dec) internal pure returns (uint256) {
98 if (dec == 18) return amount;
99 if (dec < 18) return amount * (10 ** (18 - dec));
100 return amount / (10 ** (dec - 18));
101 }
102 
103 function _fromWad(uint256 amountWad, uint8 dec) internal pure returns (uint256) {
104 if (dec == 18) return amountWad;
105 if (dec < 18) return amountWad / (10 ** (18 - dec));
106 return amountWad * (10 ** (dec - 18));
107 }
108}
109 

Click any line number to deep-link to it — the target line highlights on load.