Dissecting DeFi Protocols

Share this post

Dissecting Inverse Finance's Anchor protocol

0xkowloon.substack.com

Dissecting Inverse Finance's Anchor protocol

0xkowloon
Mar 21, 2021
2
Share this post

Dissecting Inverse Finance's Anchor protocol

0xkowloon.substack.com
I heard you can buy this on OpenSea.

Inverse Finance’s Anchor protocol is a crypto native federal reserve that offers borrowing, lending, stablecoins and synthetic assets. It is a fork of Compound Finance and the main contracts are not changed. The only change to the protocol is the PriceOracle implementation which plugs into Chainlink feeds.

Users can supply their ETH to the protocol for borrowers in order to earn interest. They can also enable their provided ETH as collaterals so that they can borrow DOLA (Inverse Finance’s stablecoin). A user can borrow up to 55% of his deposited collateral, which equates to a 180% collateralization ratio. Liquidators can liquidate debts that are under-collateralized and earn a 13% bonus on the collateral purchased. When a liquidation happens, the borrower will lose his collateral up to the value of his debt, and can keep the rest of the collateral and his borrowed DOLA.

Depositing ETH

A user can deposit ETH into the protocol to receive cTokens by calling the CEther.sol contract’s payable method mint.

  1. It first accrues interest.

    // blockDelta is the number of blocks between the current // block and the last block where interest accrual happened.
    
    (MathError mathErr, uint blockDelta) = subUInt(currentBlockNumber, accrualBlockNumberPrior);

    1a. It gets the previous financial data and the borrow rate from the contract.

    // previous ETH balance = current contract ETH balance - ETH sent to contract
    uint cashPrior = subUInt(address(this).balance, msg.value);
    uint borrowsPrior = totalBorrows;
    uint reservesPrior = totalReserves;
    uint borrowIndexPrior = borrowIndex;
    
    uint borrowRateMantissa = interestRateModel.getBorrowRate(cashPrior, borrowsPrior, reservesPrior);

    1b. It gets the current borrow rate (If the capital utilization rate is not higher than kink, which is a utilization point at which the jump multiplier is applied).

    // borrow rate = utilization rate x multiplier per block + base rate per block
    return util.mul(multiplierPerBlock).div(1e18).add(baseRatePerBlock);

    1c. It gets the current borrow rate (If the capital utilization rate is higher than kink).

    // borrow rate = (kink x multiplier per block + base rate per block) + ((utilization rate - kink) x jump multiplier per block)
    uint normalRate = kink.mul(multiplierPerBlock).div(1e18).add(baseRatePerBlock);
    uint excessUtil = util.sub(kink);
    return excessUtil.mul(jumpMultiplierPerBlock).div(1e18).add(normalRate);

    1d. It calculates the simple interest factor, interest accumulated and the new financial data.

    // simple interest factor = borrow rate x number of blocks since last update
    (mathErr, simpleInterestFactor) = mulScalar(Exp({mantissa: borrowRateMantissa}), blockDelta);
    
    // interest accumulated = simple interest factor x prior total borrow amount
    (mathErr, interestAccumulated) = mulScalarTruncate(simpleInterestFactor, borrowsPrior);
    
    // new total borrow amount = interest accumulated + prior total borrow amount
    (mathErr, totalBorrowsNew) = addUInt(interestAccumulated, borrowsPrior);
    
    // new total reserves amount = reserve factor x interest accumulated + prior total reserves
    (mathErr, totalReservesNew) = mulScalarTruncateAddUInt(Exp({mantissa: reserveFactorMantissa}), interestAccumulated, reservesPrior);
    
    // new borrow index = simple interestAccumulated factor x prior borrow index + prior borrow index
    (mathErr, borrowIndexNew) = mulScalarTruncateAddUInt(simpleInterestFactor, borrowIndexPrior, borrowIndexPrior);
  2. It mints cTokens.

    2a. It gets the exchange rate of ETH to cEther.

    // cash plus borrow minus reserves = total cash + total borrows - total reserves
    (mathErr, cashPlusBorrowsMinusReserves) = addThenSubUInt(totalCash, totalBorrows, totalReserves);
    
    // exchange rate = cash plus borrow minus reserves / total supply of cEther
    (mathErr, exchangeRate) = getExp(cashPlusBorrowsMinusReserves, _totalSupply);

    2b. It gets the actual amount to mint.

    // mintTokens = actualMintAmount / exchangeRate
    (vars.mathErr, vars.mintTokens) = divScalarByExpTruncate(vars.actualMintAmount, Exp({mantissa: vars.exchangeRateMantissa}));

    2c. It adds the amount to the account’s balance.

    // new total supply = total supply + tokens to mint
    (vars.mathErr, vars.totalSupplyNew) = addUInt(totalSupply, vars.mintTokens);
    
    // new account tokens = account tokens + tokens to mint
    (vars.mathErr, vars.accountTokensNew) = addUInt(accountTokens[minter], vars.mintTokens);
    
    totalSupply = vars.totalSupplyNew;
    accountTokens[minter] = vars.accountTokensNew;

Allowing your deposits to be turned into collaterals

A user can turn his cTokens into collaterals by calling the ComptrollerG6.sol contract’s method enterMarkets. The user provides an array of cTokens addresses as the markets to enter and the method loops through each of them to the borrowers’ “assets in” for liquidity calculations.

  1. It turns on the user’s account membership for the particular market.

    marketToJoin.accountMembership[borrower] = true;
  2. It adds the cToken to the user’s account assets.

    accountAssets[borrower].push(cToken);

Borrowing DOLA against your collaterals

After becoming a market’s member, a user can now borrow DOLA against his collaterals. It is done so by calling the CErc20.sol contract’s borrow method.

  1. It first accrues interest (It calls the same method as the first operation Depositing ETH).

  2. It checks the user’s borrow eligibility (looping through each market the user wants to enter)

    2a. It gets the market’s snapshot.

    (oErr, vars.cTokenBalance, vars.borrowBalance, vars.exchangeRateMantissa) = asset.getAccountSnapshot(account);

    2b. It gets the market’s collateral factor and exchange rate.

    vars.collateralFactor = Exp({mantissa: markets[address(asset)].collateralFactorMantissa});
    vars.exchangeRate = Exp({mantissa: vars.exchangeRateMantissa});

    2c. It gets the asset market price from its oracle.

    vars.oraclePriceMantissa = oracle.getUnderlyingPrice(asset);
    vars.oraclePrice = Exp({mantissa: vars.oraclePriceMantissa});

    2d. It calculates the sum of collateral and the sum of borrow plus effects.

    // Pre-compute a conversion factor from tokens -> ether (normalized price value)
    vars.tokensToDenom = mul_(mul_(vars.collateralFactor, vars.exchangeRate), vars.oraclePrice);
    
    // sumCollateral += tokensToDenom * cTokenBalance
    vars.sumCollateral = mul_ScalarTruncateAddUInt(vars.tokensToDenom, vars.cTokenBalance, vars.sumCollateral);
    
    // sumBorrowPlusEffects += oraclePrice * borrowBalance
    vars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(vars.oraclePrice, vars.borrowBalance, vars.sumBorrowPlusEffects);
    
    // Calculate effects of interacting with cTokenModify
    if (asset == cTokenModify) {
        // redeem effect
        // sumBorrowPlusEffects += tokensToDenom * redeemTokens
        vars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(vars.tokensToDenom, redeemTokens, vars.sumBorrowPlusEffects);
    
        // borrow effect
        // sumBorrowPlusEffects += oraclePrice * borrowAmount
        vars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(vars.oraclePrice, borrowAmount, vars.sumBorrowPlusEffects);
    }

    2e. It checks the sum of collateral is enough to cover the hypothetical sum of borrow plus effects and it reverts if there is a shortfall.

    // The last returned value is "shortfall".
    if (vars.sumCollateral > vars.sumBorrowPlusEffects) {
        return (Error.NO_ERROR, vars.sumCollateral - vars.sumBorrowPlusEffects, 0);
    } else {
        return (Error.NO_ERROR, 0, vars.sumBorrowPlusEffects - vars.sumCollateral);
    }
    
    
    if (shortfall > 0) {
        return uint(Error.INSUFFICIENT_LIQUIDITY);
    }
  3. It checks the market has enough cash reserves for borrowing.

    if (getCashPrior() < borrowAmount) {
        return fail(Error.TOKEN_INSUFFICIENT_CASH, FailureInfo.BORROW_CASH_NOT_AVAILABLE);
    }
  4. It calculates the market’s new total borrow amount by adding the new borrow amount to the existing borrow amounts.

    (vars.mathErr, vars.accountBorrows) = borrowBalanceStoredInternal(borrower);
    (vars.mathErr, vars.accountBorrowsNew) = addUInt(vars.accountBorrows, borrowAmount);
    (vars.mathErr, vars.totalBorrowsNew) = addUInt(totalBorrows, borrowAmount);
  5. It transfers the loan to the borrower and accounts for the change.

    token.transfer(borrower, borrowAmount);
    
    accountBorrows[borrower].principal = vars.accountBorrowsNew;
    accountBorrows[borrower].interestIndex = borrowIndex;

Repaying a loan

A user can repay his outstanding debt by calling CErc20.sol’s repayBorrow method.

  1. It first accrues interest just like depositing ETH or borrowing DOLA.

  2. It checks if repaying a loan is allowed.

    uint allowed = comptroller.repayBorrowAllowed(address(this), payer, borrower, repayAmount);
    if (allowed != 0) {
        return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.REPAY_BORROW_COMPTROLLER_REJECTION, allowed), 0);
    }
  3. It calculates the outstanding debt amount.

    // recent borrow balance = borrower's borrow balance x market's borrow index ÷ borrower's borrow index
    
    vars.borrowerIndex = accountBorrows[borrower].interestIndex;
    BorrowSnapshot storage borrowSnapshot = accountBorrows[account];
    (mathErr, principalTimesIndex) = mulUInt(borrowSnapshot.principal, borrowIndex);
    (mathErr, recentBorrowBalance) = divUInt(principalTimesIndex, borrowSnapshot.interestIndex);
  4. It transfers the repay amount from the user back to the borrow market. Programming in Solidity in general is very defensive, it is good to check your mathematical operation does what you expect it to do. Overflow can and will happen.

    EIP20NonStandardInterface token = EIP20NonStandardInterface(underlying);
    uint balanceBefore = EIP20Interface(underlying).balanceOf(address(this));
    token.transferFrom(from, address(this), amount);
    uint balanceAfter = EIP20Interface(underlying).balanceOf(address(this));
    require(balanceAfter >= balanceBefore, "TOKEN_TRANSFER_IN_OVERFLOW");
  5. It updates the market’s stats.

    (vars.mathErr, vars.accountBorrowsNew) = subUInt(vars.accountBorrows, vars.actualRepayAmount);
    (vars.mathErr, vars.totalBorrowsNew) = subUInt(totalBorrows, vars.actualRepayAmount);
    accountBorrows[borrower].principal = vars.accountBorrowsNew;
    accountBorrows[borrower].interestIndex = borrowIndex;
    totalBorrows = vars.totalBorrowsNew;

Liquidating a loan

Liquidators (usually bots) can liquidate an under-collateralized loan by calling CErc20.sol’s method liquidateBorrow.

  1. It first accrues interest. Every time when something happens, interest has to be accrued.

  2. Then the market in which to seize collateral from the borrower also has to accrue interest.

    cTokenCollateral.accrueInterest();
  3. It then checks the liquidation is allowed. The same method getAccountLiquidityInternal from borrowing is used to calculate if the debt position has any shortfalls.

    (Error err, , uint shortfall) = getAccountLiquidityInternal(borrower);
    if (err != Error.NO_ERROR) {
        return uint(err);
    }
    if (shortfall == 0) {
        return uint(Error.INSUFFICIENT_SHORTFALL);
    }
  4. The method fetches the borrower’s borrow balance and makes sure the liquidator does not pay more than the borrow balance times the close factor (ranging between 0.05 and 0.9).

    uint borrowBalance = CToken(cTokenBorrowed).borrowBalanceStored(borrower);
    uint maxClose = mul_ScalarTruncate(Exp({mantissa: closeFactorMantissa}), borrowBalance);
    if (repayAmount > maxClose) {
        return uint(Error.TOO_MUCH_REPAY);
    }
  5. A borrower cannot liquidate himself.

    if (borrower == liquidator) {
        return (fail(Error.INVALID_ACCOUNT_PAIR, FailureInfo.LIQUIDATE_LIQUIDATOR_IS_BORROWER), 0);
    }
  6. It calls repayBorrowFresh, which contains the same logic for when a borrower repays his own loan.

  7. It calculates the amount of collateral to seize from the borrower.

    // Get DOLA price
    uint priceBorrowedMantissa = oracle.getUnderlyingPrice(CToken(cTokenBorrowed));
    // Get ETH price
    uint priceCollateralMantissa = oracle.getUnderlyingPrice(CToken(cTokenCollateral));
    
    // seize amount = actual repay amount x liquidation incentive x price borrowed ÷ price collateral
    // seize tokens = seize amount ÷ exchange rate
    
    uint exchangeRateMantissa = CToken(cTokenCollateral).exchangeRateStored(); // Note: reverts on error
    uint seizeTokens;
    Exp memory numerator;
    Exp memory denominator;
    Exp memory ratio;
    
    numerator = mul_(Exp({mantissa: liquidationIncentiveMantissa}), Exp({mantissa: priceBorrowedMantissa}));
    denominator = mul_(Exp({mantissa: priceCollateralMantissa}), Exp({mantissa: exchangeRateMantissa}));
    ratio = div_(numerator, denominator);
    
    seizeTokens = mul_ScalarTruncate(ratio, actualRepayAmount);
  8. It seizes the tokens from the borrower.

    // new borrower token balance = current borrower token balance - seized tokens
    
    // new liquidator token balance = current liquidator token balance + seized tokens
    
    (mathErr, borrowerTokensNew) = subUInt(accountTokens[borrower], seizeTokens);
    (mathErr, liquidatorTokensNew) = addUInt(accountTokens[liquidator], seizeTokens);
    
    accountTokens[borrower] = borrowerTokensNew;
    accountTokens[liquidator] = liquidatorTokensNew;

The FED

DOLA is a stablecoin that pegs to the USD. The stabilizer, Curve metapool and the Fed together attempt to stabilize DOLA price.

The chair of the contract Fed.sol has the right to exercise expansionary and contractionary monetary policy on DOLA supply.

Monetary expansion

The chair can call the method expansion to mint DOLA as well as its cToken, which increases DOLA supply.

function expansion(uint amount) public {
    require(msg.sender == chair, "ONLY CHAIR");
    underlying.mint(address(this), amount);
    require(ctoken.mint(amount) == 0, 'Supplying failed');
    supply = supply.add(amount);
    emit Expansion(amount);
}

Monetary contraction

The chair can call the method contraction to burn DOLA as well as its cToken, which decreases DOLA supply.

function contraction(uint amount) public {
    require(msg.sender == chair, "ONLY CHAIR");
    require(amount <= supply, "AMOUNT TOO BIG"); // can't burn profits
    require(ctoken.redeemUnderlying(amount) == 0, "Redeem failed");
    underlying.burn(amount);
    supply = supply.sub(amount);
    emit Contraction(amount);
}

Taking profit

If the Fed’s underlying asset balance in a cToken contract is greater than the DOLA supply it created, it has the option to take profit and sends it to gov.

function takeProfit() public {
    uint underlyingBalance = ctoken.balanceOfUnderlying(address(this));
    uint profit = underlyingBalance.sub(supply);
    if(profit > 0) {
        require(ctoken.redeemUnderlying(profit) == 0, "Redeem failed");
        underlying.transfer(gov, profit);
    }
}
Share this post

Dissecting Inverse Finance's Anchor protocol

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