Dissecting Inverse Finance's Anchor protocol
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
.
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);
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.
It turns on the user’s account membership for the particular market.
marketToJoin.accountMembership[borrower] = true;
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.
It first accrues interest (It calls the same method as the first operation Depositing ETH).
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); }
It checks the market has enough cash reserves for borrowing.
if (getCashPrior() < borrowAmount) { return fail(Error.TOKEN_INSUFFICIENT_CASH, FailureInfo.BORROW_CASH_NOT_AVAILABLE); }
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);
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.
It first accrues interest just like depositing ETH or borrowing DOLA.
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); }
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);
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");
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
.
It first accrues interest. Every time when something happens, interest has to be accrued.
Then the market in which to seize collateral from the borrower also has to accrue interest.
cTokenCollateral.accrueInterest();
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); }
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); }
A borrower cannot liquidate himself.
if (borrower == liquidator) { return (fail(Error.INVALID_ACCOUNT_PAIR, FailureInfo.LIQUIDATE_LIQUIDATOR_IS_BORROWER), 0); }
It calls
repayBorrowFresh
, which contains the same logic for when a borrower repays his own loan.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);
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);
}
}