One word. $DEGEN (GAWD don’t you all love this word).
Indexed Finance enables the creation of a token that represents ownership of multiple underlying assets weighted by capitalization. It is designed to replicate the behavior of index funds.
The protocol is managed by holders of its governance token $NDX, which is used to vote on proposals for protocol updates and high level index management such as the definition of market sectors and the creation of new management strategies. As of today, all Indexed Finance’s strategies are based on the square root of market capitalizations in order to avoid a token with a very large market capitalization from being weighted too heavily in the index pool.
Each index consists of the current constituent list and a secondary candidate list. The current constituent list represents the underlying tokens that are included in the index pool. The candidate list represents tokens that are not yet included in the index pool, but are ready to be included if a token in the current constituent list underperforms or if a candidate’s performance justifies its inclusion.
The asset composition of an index token changes constantly as index pools regularly rebalance the portfolio. Portfolio targets are set using on-chain data from Uniswap and pre-set rules defined in smart contracts. Each underlying token is assigned a desired weight and an actual weight. When reindexing happens, the index pool’s balance in that underlying token has to increase or decrease depending on whether the actual weight is greater than or less than the desired weight.
After confirming the need for reindexing, the pool has to be buy or sell tokens via decentralized exchanges or auctions. Any attempts to reach the target weight will cause significant loss to the pool due to slippages in trading pools with small liquidity. In order to solve this problem, index pools rebalance themselves by creating small arbitrage opportunities that incentivize traders to trade against the pool. Actual weights eventually move toward the desired weights as trades happen. Weight adjustment occurs at a maximum of once per hour.
Let’s take the example of a $DEGEN index token’s lifecycle.
Initializing the pool for $DEGEN
The contract PoolInitializer.sol
is the contract where an index pool is created. Before an index pool is created, users can contribute tokens to the initializer and this contract uses Uniswap’s price oracle to determine their ETH value. Contributors will be credited for the tokens’ moving average ETH value. When all the tokens needed are acquired, the index pool will be created and the contract will receive the initial token supply.
On
initialize
, the contract is provided with the pool’s address, list of underlying tokens and each token’s desired amount._poolAddress = poolAddress; uint256 len = tokens.length; require(amounts.length == len, "ERR_ARR_LEN"); _tokens = tokens; for (uint256 i = 0; i < len; i++) { _remainingDesiredAmounts[tokens[i]] = amounts[i]; }
Users can deposit underlying tokens into the pool initializer by calling
contributeTokens
, which accepts the token address, amount to deposit and the minimum credit amount./* Get remaining amount available for deposit */ uint256 desiredAmount = _remainingDesiredAmounts[token]; /* Get the token's time-weighted ETH value between 20 minutes and 2 days old */ credit = oracle.computeAverageEthForTokens( token, amountIn, SHORT_TWAP_MIN_TIME_ELAPSED, /* 20 minutes */ SHORT_TWAP_MAX_TIME_ELAPSED /* 2 days */ ); /** Transfer the token from the contributor to the pool initializer * Deduct the token's remaining desired amount * Add ETH credits to the contributor * Add total credit in ETH */ IERC20(token).safeTransferFrom(msg.sender, address(this), amountIn); _remainingDesiredAmounts[token] = desiredAmount.sub(amountIn); _credits[msg.sender] = _credits[msg.sender].add(credit); _totalCredit = _totalCredit.add(credit);
Contributors can also call
contributeTokens
with an array of tokens and amounts as arguments to deposit different underlying tokens at once.The method
finish
can be called when the index pool is ready to be created (all remaining desired amounts are equal to 0). It transfers all the underlying tokens to the index pool contract./* Looping over all tokens and transfer the pool initializer's token balances to the actual pool */ for (uint256 i = 0; i < len; i++) { address token = _tokens[i]; tokens[i] = token; uint256 balance = IERC20(token).balanceOf(address(this)); balances[i] = balance; IERC20(token).safeApprove(_poolAddress, balance); require( _remainingDesiredAmounts[token] == 0, "ERR_PENDING_TOKENS" ); }
It calculates each token’s denormalized weight.
/* Get each token's value in ETH and calculate each of their weight in the pool */ uint256 valueSum; uint144[] memory ethValues = oracle.computeAverageEthForTokens( tokens, balances, SHORT_TWAP_MIN_TIME_ELAPSED, SHORT_TWAP_MAX_TIME_ELAPSED ); for (uint256 i = 0; i < len; i++) { valueSum = valueSum.add(ethValues[i]); } for (uint256 i = 0; i < len; i++) { denormalizedWeights[i] = _denormalizeFractionalWeight( FixedPoint.fraction(uint112(ethValues[i]), uint112(valueSum)) ); }
It initializes the index pool and the unbound token seller (the contract that sells undesired tokens, more on this later). The minimum number of tokens is 2 and the maximum is 10. The minimum weight of each token is 1%. The minimum balance is
(10 ** 18) / (10 ** 12)
./* * Loops through token to make sure denormalizedWeights[i] >= MIN_WEIGHT and <= MAX_WEIGHT * Make sure each balance >= MIN_BALANCE * Store the tokens * Pull the tokens into the index pool * Mint the index tokens and transfer them to the market square root controller */ IIndexPool(poolAddress).initialize( tokens, balances, denormalizedWeights, msg.sender, sellerAddress ); IUnboundTokenSeller(sellerAddress).initialize( poolAddress, defaultSellerPremium );
It sets the finished flag to true, which means contributors are now allowed to claim their $DEGEN by calling
claimTokens
.uint256 credit = _credits[account]; uint256 amountOut = (TOKENS_MINTED.mul(credit)).div(_totalCredit); _credits[account] = 0; IERC20(_poolAddress).safeTransfer(account, amountOut);
Minting $DEGEN
A user can mint an index pool token through two ways. By providing all of the underlying tokens or by providing one of the underlying tokens and the contract will swap the incoming token into the remaining tokens with a swap fee.
Mint by providing all of the underlying tokens (
joinPool
)/* ratio = index token to mint ÷ total supply */ uint256 ratio = bdiv(poolAmountOut, poolTotal); for (uint256 i = 0; i < maxAmountsIn.length; i++) { address t = _tokens[i]; (Record memory record, uint256 realBalance) = _getInputToken(t); /* amount to transfer = ratio x token balance (or min balance) */ uint256 tokenAmountIn = bmul(ratio, record.balance); /* Update token weight */ _updateInputToken(t, record, badd(realBalance, tokenAmountIn)); /* Transfer token to the index pool */ _pullUnderlying(t, msg.sender, tokenAmountIn); } /* Mint index token and transfer to msg.sender */ _mintPoolShare(poolAmountOut); _pushPoolShare(msg.sender, poolAmountOut);
Mint by providing one of the underlying tokens (
joinswapExternAmountIn
orjoinswapPoolAmountOut
). The pool implicitly charges a trading fee in its index token amount conversion./* Calculate the number of index tokens to mint */ /* * normalized weight = token weight ÷ total weight * fee = (1 - normalized weight) x swap fee */ uint256 poolAmountOut = calcPoolOutGivenSingleIn( inRecord.balance, inRecord.denorm, _totalSupply, _totalWeight, tokenAmountIn, _swapFee ); /* Update token weight */ _updateInputToken(t, record, badd(realBalance, tokenAmountIn)); /* Transfer token to the index pool */ _pullUnderlying(t, msg.sender, tokenAmountIn);
Re-indexing $DEGEN
Every week, the tokens in the index pool are either re-weighed or re-indexed. Re-indexing tokens is to select the top tokens in the index pool’s category and weigh them by their market capitalizations. It is done by calling MarketCapSqrtController.sol
’s method reindexPool
.
It checks the index pool’s last re-weigh was more than a week ago.
require(now - meta.lastReweigh >= 1 weeks, "ERR_POOL_REWEIGH_DELAY");
It gets the top category tokens.
/* Each index pool has a category, which consists of tokens in or not in the index pool */ address[] storage categoryTokens = _categoryTokens[categoryID]; /* In MarketCapSortedTokenCategories.sol... */ tokens = new address[](num); for (uint256 i = 0; i < num; i++) tokens[i] = categoryTokens[i];
It calculates each token’s time-weighted average price.
PriceLibrary.TwoWayAveragePrice[] memory prices = oracle.computeTwoWayAveragePrices( tokens, 1 days, 1.5 weeks );
It computes each token’s weight.
/* Compute each token's market square roots */ for (uint256 i = 0; i < len; i++) { uint256 totalSupply = IERC20(tokens[i]).totalSupply(); uint256 marketCap = averagePrices[i].computeAverageEthForTokens(totalSupply); sqrts[i] = uint112(marketCap.sqrt()); } /* Sum the weights */ uint112 rootSum; uint256 len = sqrts.length; for (uint256 i = 0; i < len; i++) rootSum += sqrts[i]; /* Calculate each token's weight */ weights = new FixedPoint.uq112x112[](len); for (uint256 i = 0; i < len; i++) { weights[i] = FixedPoint.fraction(sqrts[i], rootSum); } /* Calculate minimum token balance in ETH and denormalize weights */ for (uint256 i = 0; i < size; i++) { minimumBalances[i] = prices[i].computeAverageTokensForEth(totalValue) / 100; denormalizedWeights[i] = _denormalizeFractionalWeight(weights[i]); }
It reads current tokens in the pool and mark the ones that are represented in re-indexing.
uint256 tLen = _tokens.length; bool[] memory receivedIndices = new bool[](tLen); for (uint256 i = 0; i < tokens.length; i++) { records[i] = _records[tokens[i]]; if (records[i].bound) receivedIndices[records[i].index] = true; }
It sets any current token that is being unbound to have 0 desired denormalized weight.
for (uint256 i = 0; i < tLen; i++) { if (!receivedIndices[i]) { _setDesiredDenorm(_tokens[i], 0); } }
It sets the desired denormalized weight for the tokens that are bound in this round of re-indexing.
/* * A token must have at least a weight of 1 / 100. * Bind token if it is not bound already. */ for (uint256 i = 0; i < tokens.length; i++) { address token = tokens[i]; uint96 denorm = desiredDenorms[i]; if (denorm < MIN_WEIGHT) denorm = uint96(MIN_WEIGHT); if (!records[i].bound) { _bind(token, minimumBalances[i], denorm); } else { _setDesiredDenorm(token, denorm); } }
Re-weighing $DEGEN
Re-weighing tokens is to change each token in the index pool’s weight by the square root of its market capitalization. It works almost the same as re-indexing except no token bounding/unbounding happens.
Swapping tokens to reach desired weights
Desired weights are reached by creating small arbitrage opportunities so that arbitrageurs will swap desired tokens for undesired tokens. IndexPool.sol
has the method swapExactAmount(In/Out)
that arbitrageurs can call.
It makes sure input token amount is not greater than the max pool ratio per token.
require( tokenAmountIn <= bmul(inRecord.balance, MAX_IN_RATIO), "ERR_MAX_IN_RATIO" );
It calculates the spot price.
/* * numerator = token in balance ÷ token in weight * denominator = token out balance ÷ token out weight * ratio = numerator ÷ denominator * scale = 1 ÷ (1 - swap fee) * spot price = ratio x scale */ uint256 spotPriceBefore = calcSpotPrice( inRecord.balance, inRecord.denorm, outRecord.balance, outRecord.denorm, _swapFee );
It calculates the token amount to return to the arbitrageur.
/* * weight ratio = token in weight ÷ token out weight * adjusted token in amount = token in amount x (1 - swap fee) * y = token balance in ÷ (token balance in + adjusted token in amount) * token amount out = token balance out x (1 - y ** weight ratio) * / uint256 tokenAmountOut = calcOutGivenIn( inRecord.balance, inRecord.denorm, outRecord.balance, outRecord.denorm, tokenAmountIn, _swapFee );
It transfers input token to the contract and output token to the arbitrageur.
_pullUnderlying(tokenIn, msg.sender, tokenAmountIn); _pushUnderlying(tokenOut, msg.sender, tokenAmountOut);
It updates both tokens’ balances.
realInBalance = badd(realInBalance, tokenAmountIn); _updateInputToken(tokenIn, inRecord, realInBalance); if (inRecord.ready) { inRecord.balance = realInBalance; } outRecord.balance = bsub(outRecord.balance, tokenAmountOut); _records[tokenOut].balance = outRecord.balance;
It decreases the output token’s normalized weight.
_decreaseDenorm(outRecord, tokenOut);
It makes sure the spot price after is more expensive than the spot price before as the purchase of input token makes the index pool closer to its desired weights.
require(spotPriceAfter >= spotPriceBefore, "ERR_MATH_APPROX_2");
Selling unbound tokens
When a token’s weight drops lower than the minimum weight, it is removed from the index pool and the remaining tokens will be sent to UnboundTokenSeller.sol
to be sold off on Uniswap. It happens after the method call _decreaseDenorm
that decreases the token’s denormalized weight.
It replaces the token in record with the last token.
uint256 index = record.index; uint256 last = _tokens.length - 1; if (index != last) { _tokens[index] = _tokens[last]; _records[_tokens[index]].index = uint8(index); } _tokens.pop();
It resets the token’s stats.
_records[token] = Record({ bound: false, ready: false, lastDenormUpdate: 0, denorm: 0, desiredDenorm: 0, index: 0, balance: 0 });
It transfers the remaining token to the unbound token seller.
_pushUnderlying(token, address(_unbindHandler), tokenBalance);
Anyone can call one of the swap tokens method in
UnboundTokenSeller.sol
to help the pool to exit the unbound tokens through Uniswap and potentially earn a premium./* Swap! */ uint256[] memory amounts = _uniswapRouter.swapTokensForExactTokens( amountOut, maxAmountIn, path, address(_pool), block.timestamp ); /* See if the caller can earn a premium by calling the swap method */ uint256 maxAmountIn = calcOutGivenIn(tokenOut, tokenIn, amountOut); uint256 amountIn = amounts[0]; if (amountIn < maxAmountIn) { IERC20(tokenIn).safeApprove(address(_uniswapRouter), 0); premiumPaidToCaller = maxAmountIn - amountIn; // Transfer the difference between what the contract was willing to pay and // what it actually paid to the caller. IERC20(tokenIn).safeTransfer(msg.sender, premiumPaidToCaller); }
Update the pool’s balance of the token.
_pool.gulp(tokenOut);
Burning $DEGEN
Exiting from a pool is similar to how entering a pool works. The only difference is that the operation is in reverse. exitPool
does the following (transfers each of the underlying token back to the index token holder):
Remove the user’s share of index tokens.
Burn the index tokens.
Loop through each token, calculate each token’s ratio to be withdrawn.
/* pool share to be burned */ uint256 ratio = bdiv(pAiAfterExitFee, poolTotal); for (uint256 i = 0; i < minAmountsOut.length; i++) { /* Calculate each token's amount to be withdrawn */ uint256 tokenAmountOut = bmul(ratio, record.balance); /* Remove token amount from the pool */ _records[t].balance = bsub(record.balance, tokenAmountOut); _pushUnderlying(t, msg.sender, tokenAmountOut); }
There is also logic that involves the exit fee, but it is currently to 0.
Exiting a pool and only get one of the underlying assets is similar to sending only one token to mint the index token (exitswapPoolAmountIn
).
/* token to be sent */
Record memory outRecord = _getOutputToken(tokenOut);
/* token amount to be sent, with swap fee deducted */
uint256 tokenAmountOut = calcSingleOutGivenPoolIn(
outRecord.balance,
outRecord.denorm,
_totalSupply,
_totalWeight,
poolAmountIn,
_swapFee
);
/* send underlying token to burner, deduct token amount from balance and lower the token's denormalized weight */
_pushUnderlying(tokenOut, msg.sender, tokenAmountOut)
_records[tokenOut].balance = bsub(outRecord.balance, tokenAmountOut);
_decreaseDenorm(outRecord, tokenOut);
/* Burn burner's pool share */
_pullPoolShare(msg.sender, poolAmountIn);
_burnPoolShare(bsub(poolAmountIn, exitFee));
Changing $DEGEN’s token categories
To be added to a category, it should meet the following requirements in addition to any other criteria for the particular category.
1. The token is at least a week old.
2. The token complies with the ERC20 standard (boolean return values not required)
3. No major vulnerabilities have been discovered in the token contract.
4. The token does not have a deflationary supply model.
5. The token's supply can not be arbitrarily inflated or deflated maliciously.
5a. The control model should be considered if the supply can be modified arbitrarily.
The logic lives in the contract MarketCapSortedTokenCategories.sol
and only the contract owner has the right to add or remove a token (addToken, removeToken
).