SKIP TO CONTENT

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

  • SYNCING: ……

K401Distributor.sol

Epoch emission. rate = R_MAX * clamp((P - 1) / (K - 1), 0, 1) — exactly zero at or below NAV.

186 lines7.0 KBSolidity
Source of src/K401Distributor.sol, 186 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 {IK401, IK401Oracle, IK401Treasury, IK401Staking} from "./interfaces/IK401Interfaces.sol";
7 
8/**
9 * @title K401Distributor — premium-clamped emission
10 *
11 * EPOCH_LENGTH = 8 hours (3 rebases/day)
12 * R_MAX = 45 bps per epoch (0.45%)
13 * K_PREMIUM = 1.75e18
14 * P = twap / nav
15 * rate = R_MAX * clamp((P - 1e18) / (K_PREMIUM - 1e18), 0, 1e18)
16 *
17 * `P <= 1.0` => `rate == 0`, exactly. No emission at or below backing; this is the
18 * single line that kills the OHM death spiral. The mint is additionally clamped by
19 * the treasury's RFV headroom so `rfvPerToken() >= 1e18` can never be breached.
20 *
21 * Emission is minted to `K401Staking`, never to holders. Liquid balances do not move.
22 */
23contract K401Distributor is Ownable, ReentrancyGuard {
24 /*//////////////////////////////////////////////////////////////
25 IMMUTABLE PARAMETERS
26 //////////////////////////////////////////////////////////////*/
27 
28 uint256 public constant EPOCH_LENGTH = 8 hours;
29 uint256 public constant R_MAX_BPS = 45;
30 uint256 public constant K_PREMIUM = 1.75e18;
31 uint256 public constant WAD = 1e18;
32 uint256 public constant BPS = 10_000;
33 /// @notice Share of the epoch mint paid to whoever pokes `rebase()`.
34 uint256 public constant CALLER_REBATE_BPS = 50; // 0.50%
35 /// @notice Rebases per year, used only for the APY view.
36 uint256 public constant EPOCHS_PER_YEAR = 1095;
37 
38 IK401 public immutable k401;
39 IK401Oracle public immutable oracle;
40 IK401Treasury public immutable treasury;
41 IK401Staking public immutable staking;
42 
43 /*//////////////////////////////////////////////////////////////
44 STORAGE
45 //////////////////////////////////////////////////////////////*/
46 
47 uint256 public epoch;
48 uint256 public nextEpochTs;
49 uint256 public lastMinted;
50 uint256 public lastRateWad;
51 uint256 public totalEmitted;
52 
53 event Rebase(
54 uint256 indexed epoch,
55 uint256 premiumWad,
56 uint256 rateWad,
57 uint256 minted,
58 uint256 toStaking,
59 uint256 callerRebate,
60 address indexed caller
61 );
62 
63 error EpochNotOver();
64 error ZeroAddress();
65 
66 constructor(address k401_, address oracle_, address treasury_, address staking_, address owner_) Ownable(owner_) {
67 if (k401_ == address(0) || oracle_ == address(0) || treasury_ == address(0) || staking_ == address(0)) {
68 revert ZeroAddress();
69 }
70 k401 = IK401(k401_);
71 oracle = IK401Oracle(oracle_);
72 treasury = IK401Treasury(treasury_);
73 staking = IK401Staking(staking_);
74 nextEpochTs = block.timestamp + EPOCH_LENGTH;
75 }
76 
77 /*//////////////////////////////////////////////////////////////
78 PURE FORMULA
79 //////////////////////////////////////////////////////////////*/
80 
81 /// @notice `rate` as a fraction of total supply, 1e18 fixed point.
82 /// @dev Returns exactly 0 for premiumWad <= 1e18, and the R_MAX cap at or above K_PREMIUM.
83 function rateFor(uint256 premiumWad) public pure returns (uint256) {
84 if (premiumWad <= WAD) return 0;
85 uint256 numerator = premiumWad - WAD;
86 uint256 denominator = K_PREMIUM - WAD;
87 uint256 clamped = (numerator * WAD) / denominator;
88 if (clamped > WAD) clamped = WAD;
89 // R_MAX_BPS / BPS of supply, scaled by `clamped`.
90 return (R_MAX_BPS * clamped) / BPS;
91 }
92 
93 /// @notice Premium P = twap / nav, 1e18 fixed point.
94 function premium(uint256 twapWad, uint256 navWad) public pure returns (uint256) {
95 if (navWad == 0) return 0;
96 return (twapWad * WAD) / navWad;
97 }
98 
99 /*//////////////////////////////////////////////////////////////
100 VIEWS
101 //////////////////////////////////////////////////////////////*/
102 
103 /// @notice Non-reverting preview of the next epoch.
104 function preview()
105 public
106 view
107 returns (uint256 premiumWad, uint256 rateWad, uint256 mintAmount, bool oracleOk)
108 {
109 (uint256 twap, bool ok) = oracle.peek();
110 oracleOk = ok;
111 if (!ok) return (0, 0, 0, false);
112 uint256 navWad = treasury.nav();
113 premiumWad = premium(twap, navWad);
114 rateWad = rateFor(premiumWad);
115 mintAmount = (k401.totalSupply() * rateWad) / WAD;
116 uint256 headroom = treasury.maxMintable();
117 if (mintAmount > headroom) mintAmount = headroom;
118 if (staking.totalStaked() == 0) mintAmount = 0;
119 }
120 
121 /// @notice Compounded APY implied by the current epoch rate, 1e18 fixed point.
122 function apy() external view returns (uint256) {
123 (, uint256 rateWad,,) = preview();
124 if (rateWad == 0) return 0;
125 uint256 base = WAD + rateWad;
126 uint256 result = WAD;
127 uint256 n = EPOCHS_PER_YEAR;
128 while (n != 0) {
129 if (n & 1 == 1) result = (result * base) / WAD;
130 base = (base * base) / WAD;
131 n >>= 1;
132 }
133 return result - WAD;
134 }
135 
136 /*//////////////////////////////////////////////////////////////
137 REBASE
138 //////////////////////////////////////////////////////////////*/
139 
140 /**
141 * @notice Permissionless epoch roll. Fail-closed on a stale oracle.
142 * @dev Liquid ERC20 balances are untouched: the mint goes to `K401Staking`
143 * (plus a 0.5% rebate to the caller) and is distributed through the index.
144 */
145 function rebase() external nonReentrant returns (uint256 minted, uint256 rebateToCaller) {
146 if (block.timestamp < nextEpochTs) revert EpochNotOver();
147 
148 uint256 twap = oracle.consult(); // reverts StaleOracle if outside the valid window
149 uint256 navWad = treasury.nav();
150 uint256 premiumWad = premium(twap, navWad);
151 uint256 rateWad = rateFor(premiumWad);
152 
153 uint256 amount = (k401.totalSupply() * rateWad) / WAD;
154 
155 // Never mint past RFV coverage.
156 uint256 headroom = treasury.maxMintable();
157 if (amount > headroom) amount = headroom;
158 
159 // With nothing staked there is no index to grow into; skip rather than strand tokens.
160 if (staking.totalStaked() == 0) amount = 0;
161 
162 uint256 toStaking;
163 if (amount != 0) {
164 rebateToCaller = (amount * CALLER_REBATE_BPS) / BPS;
165 toStaking = amount - rebateToCaller;
166 if (toStaking != 0) {
167 k401.mint(address(staking), toStaking);
168 staking.notifyEmission(toStaking);
169 }
170 if (rebateToCaller != 0) k401.mint(msg.sender, rebateToCaller);
171 }
172 
173 minted = amount;
174 lastMinted = amount;
175 lastRateWad = rateWad;
176 totalEmitted += amount;
177 
178 unchecked {
179 epoch += 1;
180 }
181 nextEpochTs = block.timestamp + EPOCH_LENGTH;
182 
183 emit Rebase(epoch, premiumWad, rateWad, amount, toStaking, rebateToCaller, msg.sender);
184 }
185}
186 

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