SKIP TO CONTENT

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

  • SYNCING: ……

K401Lens.sol

Read-only aggregator. Batches the reads the frontend needs into single calls.

265 lines9.4 KBSolidity
Source of src/K401Lens.sol, 265 lines of Solidity
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.24;
3 
4import {K401} from "./K401.sol";
5import {K401Treasury} from "./K401Treasury.sol";
6import {K401Oracle} from "./K401Oracle.sol";
7import {K401Distributor} from "./K401Distributor.sol";
8import {K401Staking} from "./K401Staking.sol";
9import {K401SeatRegistry} from "./K401SeatRegistry.sol";
10import {K401Seat6551} from "./K401Seat6551.sol";
11import {K401StockDesk} from "./K401StockDesk.sol";
12import {K401BondDepository} from "./K401BondDepository.sol";
13import {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 */
21contract 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.