IndexToken
contracts/src/index/IndexToken.sol — the heart of the protocol. A fixed-supply, fee-on-transfer ERC-20 (cloned per launch via EIP-1167) that embeds a fully on-chain, per-stock dividend accumulator: no keeper, no snapshots, no Merkle proofs. Extends FeeToken + ERC20Burnable + ReentrancyGuard (all upgradeable-style for the clone pattern, behind an immutable implementation).
The dividend accumulator
The design is a magnified dividend-per-share index (the classic MAGNITUDE = 2¹²⁸ construction), one per basket stock:
mapping(address stock => uint256) public magnifiedDividendPerShare;
mapping(address stock => mapping(address account => int256)) public magnifiedCorrections;
mapping(address stock => mapping(address account => uint256)) public withdrawnDividends;
mapping(address stock => uint256) public stockReserve; // stock held for dividends
mapping(address account => bool) public isDividendExcluded;
mapping(address account => uint256) public dividendBalanceOf; // eligible balance
uint256 public dividendTotalSupply; // sum of eligible balances
distribute(address stock)(permissionless,nonReentrant): folds any newly received stock balance (balanceOf(stock) − stockReserve[stock]) intomagnifiedDividendPerShare[stock], pro-rata overdividendTotalSupply. Called by the FeeProcessor after buying stocks, but anyone can call it — e.g. if someone donates stock tokens to the index.distributeAll()loops the basket.- Corrections in the transfer hook. The ERC-20
_updateoverride maintainsmagnifiedCorrectionsso that every account's accrued amount is exact across arbitrary transfers, mints, and burns.balanceOfstays completely standard — this is not a reflection token. claimable(address account, address stock) → uint256: view of what's owed.claim()/claim(address stock)(nonReentrant): transfers accrued stock tokens out, updateswithdrawnDividendsandstockReservebefore the transfer (checks-effects-interactions). The single-stock overload guarantees one frozen equity can never block the rest of a holder's dividends.
Exclusions
Accounts that must not accrue dividends are excluded and tracked out of dividendTotalSupply: the token itself, the FeeProcessor, the index's RFV treasury, the protocol treasury — and registered AMM pairs (excluded inside setAmmPair, with reserves reconciled, so pooled liquidity never dilutes holders — audit finding V1).
Fee mechanics (from FeeToken)
- Buy/sell fees (immutable, ≤
MAX_FEE_BPS= 1000) are skimmed in-token on transfers involving a registered AMM pair; accrued fees sit on the token untilreleaseFees()is pulled by the FeeProcessor. setAmmPair(pair, bool)— creator(owner)-gated, one-way in spirit: the pair must be a contract and a genuine v2 pool whosetoken0/token1is this token (prevents pointing at a victim contract), and registration auto-excludes it from dividends.- Fee-exempt set (token, processor, treasuries) is fixed at initialization — no arbitrary exemption list, no blacklist.
Initialization guards
initialize(cfg, indexTreasury, feeProcessor) enforces: non-empty basket ≤ 16, no duplicates, every stock on the registry allow-list, no stock may equal the reserve asset or the token itself (a reserve-equal member would brick the buy path — audit finding V2a), fees ≤ cap, rfvShareBps ≤ 9000.
Supply
Minted once at initialization: 1% to the protocol treasury, 99% to the creator. The token is burnable — IndexTreasury.redeem burns via burnFrom, permanently shrinking supply.
Gas profile
All dividend maintenance is O(basket size) per transfer (bounded by 16); distribute and claim are O(1) per stock. No unbounded loops over holders — the design scales to any holder count and hundreds of concurrent index tokens with zero off-chain infrastructure.