Skip to main content

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]) into magnifiedDividendPerShare[stock], pro-rata over dividendTotalSupply. 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 _update override maintains magnifiedCorrections so that every account's accrued amount is exact across arbitrary transfers, mints, and burns. balanceOf stays 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, updates withdrawnDividends and stockReserve before 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 until releaseFees() 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 whose token0/token1 is 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.