K401Lens.sol
Read-only aggregator. Batches the reads the frontend needs into single calls.
265 lines9.4 KBSolidity
| 1 | // SPDX-License-Identifier: MIT |
| 2 | pragma solidity ^0.8.24; |
| 3 | |
| 4 | import {K401} from "./K401.sol"; |
| 5 | import {K401Treasury} from "./K401Treasury.sol"; |
| 6 | import {K401Oracle} from "./K401Oracle.sol"; |
| 7 | import {K401Distributor} from "./K401Distributor.sol"; |
| 8 | import {K401Staking} from "./K401Staking.sol"; |
| 9 | import {K401SeatRegistry} from "./K401SeatRegistry.sol"; |
| 10 | import {K401Seat6551} from "./K401Seat6551.sol"; |
| 11 | import {K401StockDesk} from "./K401StockDesk.sol"; |
| 12 | import {K401BondDepository} from "./K401BondDepository.sol"; |
| 13 | import {IERC20Like, SeatMode} from "./interfaces/IK401Interfaces.sol"; |
| 14 | |
| 15 | /** |
| 16 | * @title K401Lens |
| 17 | * @notice Read-only aggregator. One call per screen instead of a dozen round trips. |
| 18 | * Every getter here is non-reverting: a stale oracle surfaces as `oracleOk == false` |
| 19 | * rather than as a failed RPC call. |
| 20 | */ |
| 21 | contract K401Lens { |
| 22 | uint8 internal constant EQUITY_SYMBOL_COUNT = 11; |
| 23 | uint256 internal constant SLOTS = 3; |
| 24 | uint256 internal constant WAD = 1e18; |
| 25 | |
| 26 | K401 public immutable k401; |
| 27 | K401Treasury public immutable treasury; |
| 28 | K401Oracle public immutable oracle; |
| 29 | K401Distributor public immutable distributor; |
| 30 | K401Staking public immutable staking; |
| 31 | K401SeatRegistry public immutable registry; |
| 32 | K401Seat6551 public immutable seat6551; |
| 33 | K401StockDesk public immutable stockDesk; |
| 34 | K401BondDepository public immutable bonds; |
| 35 | |
| 36 | struct ProtocolStats { |
| 37 | uint256 nav; |
| 38 | uint256 rfv; |
| 39 | uint256 marketPrice; |
| 40 | uint256 premium; |
| 41 | uint256 epochRate; |
| 42 | uint256 apy; |
| 43 | uint256 totalSupply; |
| 44 | uint256 seatCount; |
| 45 | uint256 treasuryUsdg; |
| 46 | uint256 treasuryEquityUsd; |
| 47 | uint256 nextEpochTs; |
| 48 | uint256 index; |
| 49 | uint256 totalStaked; |
| 50 | bool oracleOk; |
| 51 | } |
| 52 | |
| 53 | struct Holding { |
| 54 | uint8 symbol; |
| 55 | address token; |
| 56 | uint256 qty; |
| 57 | uint256 usdValue; |
| 58 | uint256 claimable; |
| 59 | } |
| 60 | |
| 61 | struct Seat { |
| 62 | uint256 tokenId; |
| 63 | uint8 tier; |
| 64 | uint256 multiplier; |
| 65 | SeatMode mode; |
| 66 | uint8[3] stockSymbols; |
| 67 | uint16[3] stockWeightsBps; |
| 68 | uint256 pendingRebase; |
| 69 | address tbaAddress; |
| 70 | bool tbaDeployed; |
| 71 | uint256 clockInTs; |
| 72 | uint256 unlockAt; |
| 73 | bool locked; |
| 74 | uint256 lifetimeStockUsd; |
| 75 | Holding[] holdings; |
| 76 | } |
| 77 | |
| 78 | struct BondMarket { |
| 79 | uint256 id; |
| 80 | address principal; |
| 81 | bool isLp; |
| 82 | bool active; |
| 83 | uint256 price; |
| 84 | uint256 discountBps; |
| 85 | uint256 capacityLeft; |
| 86 | uint256 vestingDays; |
| 87 | bool priceOk; |
| 88 | } |
| 89 | |
| 90 | struct LeaderboardRow { |
| 91 | address account; |
| 92 | uint256 seats; |
| 93 | uint256 avgTierX100; |
| 94 | uint256 lifetimeStockUsd; |
| 95 | } |
| 96 | |
| 97 | constructor( |
| 98 | address k401_, |
| 99 | address treasury_, |
| 100 | address oracle_, |
| 101 | address distributor_, |
| 102 | address staking_, |
| 103 | address registry_, |
| 104 | address seat6551_, |
| 105 | address stockDesk_, |
| 106 | address bonds_ |
| 107 | ) { |
| 108 | k401 = K401(payable(k401_)); |
| 109 | treasury = K401Treasury(treasury_); |
| 110 | oracle = K401Oracle(oracle_); |
| 111 | distributor = K401Distributor(distributor_); |
| 112 | staking = K401Staking(staking_); |
| 113 | registry = K401SeatRegistry(registry_); |
| 114 | seat6551 = K401Seat6551(seat6551_); |
| 115 | stockDesk = K401StockDesk(stockDesk_); |
| 116 | bonds = K401BondDepository(bonds_); |
| 117 | } |
| 118 | |
| 119 | /*////////////////////////////////////////////////////////////// |
| 120 | PROTOCOL STATS |
| 121 | //////////////////////////////////////////////////////////////*/ |
| 122 | |
| 123 | function getProtocolStats() external view returns (ProtocolStats memory s) { |
| 124 | (uint256 price, bool ok) = oracle.peek(); |
| 125 | s.oracleOk = ok; |
| 126 | s.marketPrice = price; |
| 127 | s.nav = treasury.nav(); |
| 128 | s.rfv = treasury.rfv(); |
| 129 | s.premium = s.nav == 0 ? 0 : (price * WAD) / s.nav; |
| 130 | s.epochRate = distributor.rateFor(s.premium); |
| 131 | s.apy = ok ? _apy(s.epochRate) : 0; |
| 132 | s.totalSupply = k401.totalSupply(); |
| 133 | s.seatCount = k401.totalSeats(); |
| 134 | s.treasuryUsdg = treasury.reserveValueWad(); |
| 135 | s.treasuryEquityUsd = treasury.equityValueWad(); |
| 136 | s.nextEpochTs = distributor.nextEpochTs(); |
| 137 | s.index = staking.index(); |
| 138 | s.totalStaked = staking.totalStaked(); |
| 139 | } |
| 140 | |
| 141 | function _apy(uint256 rateWad) internal pure returns (uint256) { |
| 142 | if (rateWad == 0) return 0; |
| 143 | uint256 base = WAD + rateWad; |
| 144 | uint256 result = WAD; |
| 145 | uint256 n = 1095; |
| 146 | while (n != 0) { |
| 147 | if (n & 1 == 1) result = (result * base) / WAD; |
| 148 | base = (base * base) / WAD; |
| 149 | n >>= 1; |
| 150 | } |
| 151 | return result - WAD; |
| 152 | } |
| 153 | |
| 154 | /*////////////////////////////////////////////////////////////// |
| 155 | SEATS |
| 156 | //////////////////////////////////////////////////////////////*/ |
| 157 | |
| 158 | /// @notice Every Seat held by `owner`, plus every Seat they have clocked in. |
| 159 | /// (Clocked-in Seats stay in the wallet, so both lists coincide by design; |
| 160 | /// the staked list is used to surface cooldown state.) |
| 161 | function getSeats(address owner) external view returns (Seat[] memory out) { |
| 162 | uint256[] memory ids = k401.seatsOf(owner); |
| 163 | out = new Seat[](ids.length); |
| 164 | uint256 pendingTotal = staking.pending(owner); |
| 165 | uint256 stakedCount = staking.stakedCountOf(owner); |
| 166 | for (uint256 i; i < ids.length; ++i) { |
| 167 | out[i] = _seat(ids[i], stakedCount == 0 ? 0 : pendingTotal / stakedCount); |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | function getSeat(uint256 tokenId) external view returns (Seat memory) { |
| 172 | return _seat(tokenId, 0); |
| 173 | } |
| 174 | |
| 175 | function _seat(uint256 tokenId, uint256 pendingShare) internal view returns (Seat memory s) { |
| 176 | s.tokenId = tokenId; |
| 177 | s.tier = registry.tierOf(tokenId); |
| 178 | s.multiplier = registry.multiplierOf(tokenId); |
| 179 | s.mode = registry.modeOf(tokenId); |
| 180 | (s.stockSymbols, s.stockWeightsBps) = registry.stocksOf(tokenId); |
| 181 | s.clockInTs = registry.clockInTsOf(tokenId); |
| 182 | s.lifetimeStockUsd = registry.lifetimeStockUsd(tokenId); |
| 183 | s.tbaAddress = seat6551.tbaOf(tokenId); |
| 184 | s.tbaDeployed = s.tbaAddress.code.length != 0; |
| 185 | s.locked = k401.isSeatLocked(tokenId); |
| 186 | s.unlockAt = staking.unlockAt(tokenId); |
| 187 | s.pendingRebase = s.mode == SeatMode.ON_THE_CLOCK ? pendingShare : 0; |
| 188 | |
| 189 | uint256 n; |
| 190 | Holding[] memory tmp = new Holding[](EQUITY_SYMBOL_COUNT); |
| 191 | for (uint8 sym; sym < EQUITY_SYMBOL_COUNT; ++sym) { |
| 192 | address token = stockDesk.equityToken(sym); |
| 193 | if (token == address(0)) continue; |
| 194 | uint256 qty = IERC20Like(token).balanceOf(s.tbaAddress); |
| 195 | uint256 claim = stockDesk.claimable(tokenId, sym); |
| 196 | if (qty == 0 && claim == 0) continue; |
| 197 | uint256 usd; |
| 198 | uint8 dec = IERC20Like(token).decimals(); |
| 199 | uint256 qtyWad = dec <= 18 ? qty * (10 ** (18 - dec)) : qty / (10 ** (dec - 18)); |
| 200 | try stockDesk.feedPriceWad(sym) returns (uint256 p) { |
| 201 | usd = (qtyWad * p) / WAD; |
| 202 | } catch {} |
| 203 | tmp[n++] = Holding({symbol: sym, token: token, qty: qty, usdValue: usd, claimable: claim}); |
| 204 | } |
| 205 | s.holdings = new Holding[](n); |
| 206 | for (uint256 i; i < n; ++i) { |
| 207 | s.holdings[i] = tmp[i]; |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | /*////////////////////////////////////////////////////////////// |
| 212 | BOND MARKETS |
| 213 | //////////////////////////////////////////////////////////////*/ |
| 214 | |
| 215 | function getBondMarkets() external view returns (BondMarket[] memory out) { |
| 216 | uint256 n = bonds.marketCount(); |
| 217 | out = new BondMarket[](n); |
| 218 | for (uint256 i; i < n; ++i) { |
| 219 | (address principal, bool isLp, bool active, uint16 discountBps,,,,,) = bonds.markets(i); |
| 220 | (uint256 price, bool ok) = bonds.bondPriceView(i); |
| 221 | out[i] = BondMarket({ |
| 222 | id: i, |
| 223 | principal: principal, |
| 224 | isLp: isLp, |
| 225 | active: active, |
| 226 | price: price, |
| 227 | discountBps: discountBps, |
| 228 | capacityLeft: bonds.capacityLeft(i), |
| 229 | vestingDays: bonds.VESTING_TERM() / 1 days, |
| 230 | priceOk: ok |
| 231 | }); |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | /*////////////////////////////////////////////////////////////// |
| 236 | LEADERBOARD |
| 237 | //////////////////////////////////////////////////////////////*/ |
| 238 | |
| 239 | /** |
| 240 | * @notice Ranking data for a candidate set. |
| 241 | * @dev There is no on-chain holder enumeration in DN404 (or in any ERC20), so the |
| 242 | * candidate list must come from the indexer. The frontend passes the addresses |
| 243 | * it knows about and sorts the result off-chain. |
| 244 | */ |
| 245 | function getLeaderboard(address[] calldata candidates) external view returns (LeaderboardRow[] memory rows) { |
| 246 | rows = new LeaderboardRow[](candidates.length); |
| 247 | for (uint256 i; i < candidates.length; ++i) { |
| 248 | address a = candidates[i]; |
| 249 | uint256[] memory ids = k401.seatsOf(a); |
| 250 | uint256 tierSum; |
| 251 | uint256 stockUsd; |
| 252 | for (uint256 j; j < ids.length; ++j) { |
| 253 | tierSum += registry.tierOf(ids[j]); |
| 254 | stockUsd += registry.lifetimeStockUsd(ids[j]); |
| 255 | } |
| 256 | rows[i] = LeaderboardRow({ |
| 257 | account: a, |
| 258 | seats: ids.length, |
| 259 | avgTierX100: ids.length == 0 ? 0 : (tierSum * 100) / ids.length, |
| 260 | lifetimeStockUsd: stockUsd |
| 261 | }); |
| 262 | } |
| 263 | } |
| 264 | } |
| 265 |
Click any line number to deep-link to it — the target line highlights on load.