Frontier
Dissecting Inverse Finance's Frontier protocol
Inverse Finance’s Frontier 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.
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 contractuint 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 blockreturn util.mul(multiplierPerBlock).div(1e18).add(baseRatePerBlock);1c. It gets the current borrow rate (If the capital utilization rate is higher thankink
).// 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;
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);
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 * cTokenBalancevars.sumCollateral = mul_ScalarTruncateAddUInt(vars.tokensToDenom, vars.cTokenBalance, vars.sumCollateral);// sumBorrowPlusEffects += oraclePrice * borrowBalancevars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(vars.oraclePrice, vars.borrowBalance, vars.sumBorrowPlusEffects);// Calculate effects of interacting with cTokenModifyif (asset == cTokenModify) {// redeem effect// sumBorrowPlusEffects += tokensToDenom * redeemTokensvars.sumBorrowPlusEffects = mul_ScalarTruncateAddUInt(vars.tokensToDenom, redeemTokens, vars.sumBorrowPlusEffects);// borrow effect// sumBorrowPlusEffects += oraclePrice * borrowAmountvars.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;
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 indexvars.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;
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 priceuint priceBorrowedMantissa = oracle.getUnderlyingPrice(CToken(cTokenBorrowed));// Get ETH priceuint priceCollateralMantissa = oracle.getUnderlyingPrice(CToken(cTokenCollateral));// seize amount = actual repay amount x liquidation incentive x price borrowed ÷ price collateral// seize tokens = seize amount ÷ exchange rateuint exchangeRateMantissa = CToken(cTokenCollateral).exchangeRateStored(); // Note: reverts on erroruint 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;
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);
}
}
This developer guide has been created by the Inverse.Finance community For questions, join the friendly Discord community.
Credit: 0xkowloon (Discord name)
Last modified 8mo ago