Index tokens have existed on the Ethereum blockchain for a while. Protocols such as Indexed Finance and Index Cooperative have created different versions of index tokens with different underlying tokens. So why do we need another protocol that creates index tokens? DeFi Pulse Index (DPI) and BDI share a lot of similarities such as the underlying tokens and weight distribution in the beginning. However, instead of holding underlying tokens, BasketDAO’s BDI token holds interest-bearing tokens from trusted protocols. BDI holds cTokens/aTokens/yvTokens/xTokens. This is significant because BDI holders and BASK holders will benefit from the yield generated by the underlying interest-bearing tokens. There is also no management fee. So not only do BDI holders not have to pay a fee, they are getting paid for holding the token!
How to mint BDI tokens
Currently the code is not available on GitHub, but BasketDAO has published the protocol’s contract addresses here and we can find the source code on Etherscan.
In order to mint BDI tokens, a user has to deposit ETH into the protocol’s DelayedMinter
. After depositing ETH, the user has to wait for 20 minutes before he can mint BDI tokens and he only has a 20 minutes window to do so. Failing to mint BDIs within the 20 minutes window results in the user having to either deposit more ETH to activate the deposit window again or withdraw his ETH balance entirely.
Depositing ETH through
DelayedMinter#deposit
function deposit() public payable { require(msg.value > 0, "!value"); deposits[msg.sender] = deposits[msg.sender].add(msg.value); timestampWhenDeposited[msg.sender] = block.timestamp; }
Minting BDI through
DelayedMinter#mintWithETH
. This function accepts the following as arguments (the UI calculates all the dataDelayedMinter
needs):DEX routers to use
calldata
for the DEX functionsunderlying tokens
token weights (calculated off-chain)
constituents (interest-bearing tokens)
minimum mint amount
deadline
2a. The deposit time has to be within the 20 minutes window after the initial delay.
block.timestamp >= _depositTime + mintDelaySeconds && block.timestamp <= _depositTime + maxMintDelaySeconds;
2b. The total underlying weights must be between 0.999 and 1.
uint256 sum = 0; for (uint256 i = 0; i < underlyingsWeights.length; i++) { sum = sum.add(underlyingsWeights[i]); } assert(sum <= 1e18); assert(sum >= 999e15);
2c. The function wraps the previously deposited ETH and swaps it into constituent tokens by calling the router with the provided
calldata
.// 1. Turn ETH into wrapped ETH IWETH(WETH).deposit{ value: deposits[msg.sender] }(); // 2. mmParams is encoded into bytes and then decoded back to MMParams for the internal _mintBDIWithWETH function call MMParams memory mmParams = MMParams({ routers: routers, routerCalldata: routerCalldata, constituents: constituents, underlyings: underlyings, underlyingsWeights: underlyingsWeights }); // 3. Get total constituent amount in BDI and total BDI supply (, uint256[] memory tokenAmountsInBasket) = BDPI.getAssetsAndBalances(); uint256 basketTotalSupply = BDPI.totalSupply(); /* 4. Swap ETH for underlying tokens, convert them into * interest-bearing tokens, calculate the amount of BDI to * mint. * * WETH to send = total WETH amount x token weight * * _toConstituent converts a token into its yield- * bearing version. For example, if the constituent is a * cToken, it calls Compound's * ICToken(_ctoken).mint(balance) * * _approveConstituentAndGetMintAmount approves BDI to * spend its constituent balance and gets the mint * amount. * mint amount = * The minimum of * BDI total supply x * market maker's constituent balance ÷ * BDI's constituent balance * in the constituents array */ for (uint256 i = 0; i < mmParams.constituents.length; i++) { // Convert them from WETH to their respective tokens wethToSend = _wethAmount.mul(mmParams.underlyingsWeights[i]).div(1e18); IWETH(WETH).approve(mmParams.routers[i], wethToSend); (success, ) = mmParams.routers[i].call(mmParams.routerCalldata[i]); require(success, "!swap"); // Convert to from respective token to constituent _toConstituent(mmParams.underlyings[i], mmParams.constituents[i]); // Approve constituent and calculate mint amount bdpiToMint = _approveConstituentAndGetMintAmount( mmParams.constituents[i], basketTotalSupply, tokenAmountsInBasket[i], bdpiToMint ); }
2d. It calculates the amount of each token to transfer to BDI, which is equal to the current token balance times the ratio of mint amount to total BDI supply).
function viewMint(uint256 _amountOut) public view returns (uint256[] memory _amountsIn) { uint256 totalLp = totalSupply(); _amountsIn = new uint256[](assets.length); // Precise math int128 amountOut128 = _amountOut.divu(1e18).add(uint256(1).divu(1e18)); int128 totalLp128 = totalLp.divu(1e18).add(uint256(1).divu(1e18)); int128 ratio128 = amountOut128.div(totalLp128); uint256 _amountToTransfer; for (uint256 i = 0; i < assets.length; i++) { _amountToTransfer = ratio128.mulu(ERC20(assets[i]).balanceOf(address(this))); _amountsIn[i] = _amountToTransfer; } } ... uint256[] memory _amountsToTransfer = viewMint(_amountOut);
2e. Transfer constituent tokens from the minter to the BDI contract and mint BDI tokens.
for (uint256 i = 0; i < assets.length; i++) { ERC20(assets[i]).safeTransferFrom(msg.sender, address(this), _amountsToTransfer[i]); } // Some bots are a charged a 5% fee to mint and burn. if (hasRole(MARKET_MAKER, msg.sender)) { _mint(msg.sender, _amountOut); return; }
How BasketDAO manages different modules
BasketDAO uses the contract UpgradeableProxy
contract as a proxy to delegate calls to its logic’s implementation. Currently, its implementation is set to the contract Logic
. Logic
has a mapping called approvedModules
that keeps track of the list of modules that are approved to be used. Users with the role storage.access.timelock
role can approve or revoke a module by calling approveModule
and revokeModule
. After approving a module, users with the role storage.access.timelock or storage.access.governance
can call execute
to delegate a function call to the approved module with the provided bytes data. Currently, functions are executed by a multisig wallet and are time-locked. They can be viewed here.
Currently, RebalancingV5, YieldFarmingV1
and YearnV1
are approved.
Rebalancing BDI’s underlying assets
Rebalancing BDI’s underlying assets can be done by calling RebalancingV5#rebalance
. Let’s assume governance decides to decrease the weight of cCOMP and increase the weight of xSUSHI. The function first converts cCOMP back to COMP, then swaps COMP for SUSHI on 0x or 1inch, and finally stakes SUSHI into SushiBar to receive xSUSHI.
The aggregator must be 0x or 1inch.
assert(_aggregator == ZERO_X || _aggregator == ONE_INCH);
The from and to assets must be supported by BDI (in
assets
array).Convert cCOMP back to COMP.
// _fromCToken redeems COMP from Compound by doing // require(ICToken(_ctoken).redeem(_amount) == 0, "!ctoken-redeem"); if (_isCToken(_from)) { (fromUnderlying, fromUnderlyingAmount) = _fromCToken(_from, _amount); }
Swap COMP for SUSHI. The SUSHI amount received must be greater than the minimum provided by the user.
IERC20(fromUnderlying).safeApprove(_aggregator, fromUnderlyingAmount); (bool success, ) = _aggregator.call(_data); // Difference between SUSHI balance after and before swap uint256 _toAmount = _after.sub(_before); require(_toAmount >= _minRecvAmount, "!min-amount");
Stake SUSHI for xSUSHI in SushiBar.
IERC20(SUSHI).safeApprove(XSUSHI, _toAmount); ISushiBar(XSUSHI).enter(_toAmount);
Yield farming
The BDI pool is entitled to the benefit of yield farming for some of the constituent tokens it holds. Let’s look at the scenario where there are COMP tokens to be claimed for lending cCOMP to Compound.
In the beginning of BDI’s lifecycle. YieldFarmingV1
needs to enter Compound markets in order to be able to lend cCOMP.
function enterMarkets(address[] memory _markets) external {
IComptroller(COMPTROLLER).enterMarkets(_markets);
}
If the contract holds any COMP tokens, YieldFarmingV1#toCToken
can be called to mint cCOMP. At the same time, the minter can also convert ETH into cCOMP.
function toCToken(address _token) external {
_requireAssetData(_token);
// Only doing UNI or COMP for CTokens
require(_token == UNI || _token == COMP, "!valid-to-ctoken");
address _ctoken = _getTokenToCToken(_token);
uint256 balance = IERC20(_token).balanceOf(address(this));
require(balance > 0, "!token-bal");
IERC20(_token).safeApprove(_ctoken, 0);
IERC20(_token).safeApprove(_ctoken, balance);
require(ICToken(_ctoken).mint(balance) == 0, "!ctoken-mint");
_overrideAssetData(_token, _ctoken);
}
The BDI contract accrues interest by lending COMP as well as receives COMP tokens as rewards. COMP tokens can be claimed by calling YieldFarmingV1#claimComp
and the COMP rewards are sent to the fee recipient address. Harvestable yield is used to purchase tokens and sent back to BDI. In the future, it can also be used to buy BASK and distributed to xBASK holders if the holders vote for it.
function claimComp() external {
address feeRecipient = _readSlotAddress(FEE_RECIPIENT);
require(feeRecipient != address(0), "!fee-recipient");
uint256 _before = IERC20(COMP).balanceOf(address(this));
// Claims comp
address[] memory cTokens = new address[](2);
cTokens[0] = CUNI;
cTokens[1] = CCOMP;
IComptroller(COMPTROLLER).claimComp(address(this), cTokens);
// Calculates how much was given
uint256 _after = IERC20(COMP).balanceOf(address(this));
uint256 _amount = _after.sub(_before);
// Transfers to fee recipient
IERC20(COMP).safeTransfer(feeRecipient, _amount);
}
Integration with Yearn Finance
Some underlying tokens are deposited into Yearn vaults to earn auto-compounded yield. BasketDAO uses yveCrv, yvBOOST, yvCrvLink and various Yearn V2 vaults. Let’s look at the scenario for CRV tokens. CRV is locked into the Yearn's yveCRV vault to mint yveCRV. The minted yveCRV is then deposited into Yearn’s yvBOOST vault to earn a continuous share of Curve’s trading fees. yvBOOST automatically harvests 3Crv rewards and sells them for more yveCRV.
During a mint operation, these are the executed steps:
Check if the underlying token is CRV and if the constituent is yvBOOST.
if (_underlying == CRV && _constituent == yvBOOST) { _toYveCRV(); _toYveBoost(); }
If condition 1 holds true, converts CRV into yveCRV.
IveCurveVault(yveCRV).deposit(balance);
Deposit yveCRV into Yearn’s yvBOOST’s vault.
IYearn(yvBOOST).deposit(balance);
If the BDI pool already holds some yveCRV, YearnV1#toYvBoost
can also be called to deposit yveCRV into the yvBOOST vault.
There are no harvest actions to be done because Yearn vaults auto-compound.
Closing Thoughts
Picking DeFi tokens that will beat the market is hard and for many investors the easiest way is to invest in an index token. It sounds even better to own an index token that generates yield for you, while knowing the yield comes from only battle-tested protocols. BasketDAO has upped the game of index investing by introducing a brand new mechanism of holding interest-bearing tokens.