IndexToken
contracts/src/index/IndexToken.sol — a plain, fixed-supply, never-mintable burnable ERC-20 (cloned per launch via EIP-1167) that carries only its basket metadata and the creator's fee-split configuration. It extends ERC20BurnableUpgradeable behind an immutable implementation.
It is deliberately boring: there is no fee logic, no dividend accumulator, and no mint function on the token itself. Fees are taken by the shared IndexFeeHook on the pool; backing lives in the IndexTreasury.
Fixed, never-mintable supply
uint256 constant FIXED_SUPPLY = 1_000_000e18;
The entire supply is minted once, at initialize, to the factory (which seeds it as single-sided liquidity). There is no mint function anywhere — not for the creator, not for bonds, not for governance. Supply can only ever decrease:
ERC20Burnable— holders can burn.IndexTreasury.redeem()callsburnFromto burn on redemption.
Bonds do not mint. They sell index tokens the treasury accumulated via the buyback fee share (see BondDepository).
Basket metadata & the 3-way split config
Set once at initialize and immutable thereafter:
function stocks() external view returns (address[] memory);
function isStock(address) external view returns (bool);
function indexTreasury() external view returns (address);
function rfvShareBps() external view returns (uint16); // USDG-reserve share of each fee
function buybackShareBps() external view returns (uint16); // in-kind index-buyback share
The IndexFeeHook reads rfvShareBps and buybackShareBps on every swap to route the fee. The basket share is implied: NON_PROTOCOL_BPS − rfvShareBps − buybackShareBps.
Per-index governance (no supply power)
address public admin; // default = creator
function setAdmin(address) external; // onlyAdmin — starts a 2-step transfer
function acceptAdmin() external; // pendingAdmin accepts
The admin (creator by default, transferable to a DAO/multisig in two steps) can only enable and configure the index's bond markets within protocol bounds. It has no power over supply — the token is not mintable by anyone.
Initialization guards
initialize(cfg, indexTreasury_, supplyRecipient) enforces:
- non-empty basket,
≤ MAX_BASKET_SIZE(16), - no duplicates,
- every stock on the
FeeRegistryallow-list, - no stock may equal the reserve asset (USDG) or the token itself,
rfvShareBps + buybackShareBps ≤ NON_PROTOCOL_BPS(9000).
IndexConfig (from IIndexToken.sol) is small — the fee schedule and launch curve are protocol constants, so the creator only supplies identity, basket, and split:
struct IndexConfig {
string name;
string symbol;
address creator; // overwritten with msg.sender by the factory
address[] stocks; // 1..16 allow-listed tokenized equities
uint16 rfvShareBps; // USDG-reserve share of each fee (bps of total), immutable
uint16 buybackShareBps; // in-kind index-buyback share (bps of total), immutable
}
Address mining (USDG is always currency0)
The token is not deployed at an arbitrary address: the factory CREATE2-mines the clone salt so the resulting address sorts above USDG. That makes USDG currency0 and the index currency1 on every pool, so the pool price reads directly as USDG-per-index and the single-sided launch curve is well-defined. ~50% of salts qualify, so mining converges in a couple of iterations.
What was removed (and why)
- The fee-on-transfer logic — fees are now taken by the V4 hook on the pool, not on transfer, so the token is a fully standard, composable ERC-20.
- The on-chain dividend accumulator — removed earlier for security (overflow-DoS / griefable maturity).
- The mint authority (former bond-minter) — bonds moved to a treasury-inventory model, so the launch supply is a hard invariant.