Dissecting DeFi Protocols

Share this post

Dissecting BasketDAO's BDI token

0xkowloon.substack.com

Dissecting BasketDAO's BDI token

Capital-efficient Baskets on Ethereum

0xkowloon
Jul 9, 2021
2
Share this post

Dissecting BasketDAO's BDI token

0xkowloon.substack.com

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.

  1. 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;
    }
  2. Minting BDI through DelayedMinter#mintWithETH. This function accepts the following as arguments (the UI calculates all the data DelayedMinter needs):

    • DEX routers to use

    • calldata for the DEX functions

    • underlying 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.

  1. The aggregator must be 0x or 1inch.

    assert(_aggregator == ZERO_X || _aggregator == ONE_INCH);
  2. The from and to assets must be supported by BDI (in assets array).

  3. 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);
    }
  4. 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");
  5. 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:

  1. Check if the underlying token is CRV and if the constituent is yvBOOST.

    if (_underlying == CRV && _constituent == yvBOOST) {
        _toYveCRV();
        _toYveBoost();
    }
  2. If condition 1 holds true, converts CRV into yveCRV.

    IveCurveVault(yveCRV).deposit(balance);
  3. 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.

Share this post

Dissecting BasketDAO's BDI token

0xkowloon.substack.com
Comments
TopNewCommunity

No posts

Ready for more?

© 2023 0xkowloon
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing