Dissecting DeFi Protocols

Share this post

Dissecting Rari Capital's Fuse Pool

0xkowloon.substack.com

Discover more from Dissecting DeFi Protocols

You will find articles that dissect DeFi codebases here.
Continue reading
Sign in

Dissecting Rari Capital's Fuse Pool

An open interest rate protocol that supports any asset

0xkowloon
Apr 13, 2021
2
Share this post

Dissecting Rari Capital's Fuse Pool

0xkowloon.substack.com
Share

Before jumping into the code, let’s spend a few minutes to watch this tutorial and read this article. Rari Capital’s Fuse is an open interest rate protocol that allows pool creators to choose all the parameters they want, such as interest rate curves and oracles. Fuse is a fork of Compound Finance’s protocol and Rari Capital’s developers have made some changes to the protocol to make this work. In this article, we will look at the diff between Compound Finance’s compound-protocol master branch and Rari Capital’s compound-protocol with the tag fuse-v1.1.0, which I believe to be the latest Fuse version. The diff can be found here.

New initialization parameters

The first modification is a pool creator can create a cToken with the additional parameters reserveFactorMantissa and adminFeeMantissa. The reserve factor controls how much interest for a given asset is routed to that asset’s reserve pool instead of the lenders. It protects lenders against borrower default and liquidation malfunction. The admin fee controls how much interest is routed to the admin instead of the lenders.

This can be seen in the fork’s cToken contracts.

These new parameters cannot be set arbitrarily and they have to be within the limit of the maximum fraction of interest that can be set aside for reserves and fees.

uint internal constant reserveFactorPlusFeesMaxMantissa = 1e18;

In _setAdminFeeFresh, _setFuseFeeFresh and _setReserveFactorFresh, the value being set has to be within bound. Reserve factor + admin fee + fuse fee must be less than 1.

if (newReserveFactorMantissa + adminFeeMantissa + fuseFeeMantissa > reserveFactorPlusFeesMaxMantissa) {
    return fail(Error.BAD_INPUT, FailureInfo.SET_X_BOUNDS_CHECK);
}

The new parameters are included in the supply rate per block calculation.

function supplyRatePerBlock() external view returns (uint) {
    return interestRateModel.getSupplyRate(getCashPrior(), totalBorrows, totalReserves + totalFuseFees + totalAdminFees, reserveFactorMantissa + fuseFeeMantissa + adminFeeMantissa);
}

Along with the new ratio variables, there are also two new variables totalFuseFees and totalAdminFees that keep track of the pool’s fees accumulated. They are included in the calculation of borrow rate per block, supply rate per block (code above), underlying asset to cToken exchange rate, borrow rate and utilization rate.

function borrowRatePerBlock() external view returns (uint) {
    return interestRateModel.getBorrowRate(getCashPrior(), totalBorrows, totalReserves + totalFuseFees + totalAdminFees);
}

function exchangeRateStoredInternal() internal view returns (MathError, uint) {
  /* Other logic... */
  uint totalCash = getCashPrior();
  uint cashPlusBorrowsMinusReserves;
  Exp memory exchangeRate;
  MathError mathErr;

  (mathErr, cashPlusBorrowsMinusReserves) = addThenSubUInt(totalCash, totalBorrows, totalReserves + totalFuseFees + totalAdminFees);
  if (mathErr != MathError.NO_ERROR) {
      return (mathErr, 0);
  }

  (mathErr, exchangeRate) = getExp(cashPlusBorrowsMinusReserves, _totalSupply);
}

function accrueInterest() public returns (uint) {
  /* Other logic... */
  uint borrowRateMantissa = interestRateModel.getBorrowRate(cashPrior, totalBorrows, totalReserves + totalFuseFees + totalAdminFees);
}

function borrowFresh(address payable borrower, uint borrowAmount) internal returns (uint) {
  /* Other logic... */
  uint256 utilizationRate = vars.totalBorrowsNew == 0 ? 0 : vars.totalBorrowsNew * 1e18 / (cashPrior + totalBorrows - (totalReserves + totalFuseFees + totalAdminFees));
}

Whenever interest is being accrued, totalFuseFees and totalAdminFees are also updated with the formulas

/* totalFuseFeesNew = interestAccumulated x fuseFee + totalFuseFees */
/* totalAdminFeesNew = interestAccumulated x adminFee + totalAdminFees */

(mathErr, totalFuseFeesNew) = mulScalarTruncateAddUInt(Exp({mantissa: fuseFeeMantissa}), interestAccumulated, totalFuseFees);
(mathErr, totalAdminFeesNew) = mulScalarTruncateAddUInt(Exp({mantissa: adminFeeMantissa}), interestAccumulated, totalAdminFees);

Limits

A Fuse pool has limits on minting cTokens, borrowing pool assets and capital utilization rate.

mintWithinLimits

This Comptroller function calculates the number of cToken that will be circulating after the current mint operation, then calculates the cToken’s value in ETH. If the value breaches the max ETH value, the mint operation fails.

(MathError mathErr, uint newUnderlyingBalance) = mulScalarTruncateAddUInt(Exp({mantissa: exchangeRateMantissa}), accountTokens, mintAmount);
uint newEthBalance;
(mathErr, newEthBalance) = mulScalarTruncate(Exp({mantissa: oracle.getUnderlyingPrice(CToken(cToken))}), newUnderlyingBalance);
if (newEthBalance > maxSupplyEth) return uint(Error.SUPPLY_ABOVE_MAX);

borrowWithinLimits

The Comptroller function calculates the cToken borrow amount value in ETH. If it is less than the minimum borrow value in ETH, the borrow operation fails.

uint oraclePriceMantissa = oracle.getUnderlyingPrice(CToken(cToken));
(MathError mathErr, uint borrowBalanceEth) = mulScalarTruncate(Exp({mantissa: oraclePriceMantissa}), accountBorrowsNew);
if (borrowBalanceEth < fuseAdmin.minBorrowEth()) return uint(Error.BORROW_BELOW_MIN);

maxUtilizationRate

Fuse admin has a max utilization rate.

uint maxUtilizationRate = fuseAdmin.maxUtilizationRate();

During the borrow operation, it makes sure the utilization rate after adding the current borrow amount is not greater than the max utilization rate.

/*
 * utilization rate = new total borrow amount ÷ (cash prior + old total borrow amount - (total reserves + total fuse fees + total admin fees))
 */
if (maxUtilizationRate < uint(-1)) {
    uint256 utilizationRate = vars.totalBorrowsNew == 0 ? 0 : vars.totalBorrowsNew * 1e18 / (cashPrior + totalBorrows - (totalReserves + totalFuseFees + totalAdminFees));
    if (utilizationRate > maxUtilizationRate) return fail(Error.UTILIZATION_ABOVE_MAX, FailureInfo.NEW_UTILIZATION_RATE_ABOVE_MAX);
}

New administration management

The second modification is the protocol’s administration management. In Compound’s version, the administrative check is a simple require(msg.sender == admin, “BLAH”). In Rari Capital’s version has a new function hasAdminRights, which checks the contract caller to be the admin and he did not renounce his admin right, or the contract caller to be the fuseAdmin and he did not renounce his fuse admin right.

function hasAdminRights() internal view returns (bool) {
    return (msg.sender == admin && adminHasRights) || (msg.sender == address(fuseAdmin) && fuseAdminHasRights);
}

An admin/fuse admin can renounce his right by calling the external function _renounceAdminRights/_renounceFuseAdminRights. Their behaviors are exactly the same, so I will just show _renounceFuseAdminRights here.

function _renounceFuseAdminRights() external returns (uint) {
    // Check caller = admin
    if (!hasAdminRights()) {
        return fail(Error.UNAUTHORIZED, FailureInfo.RENOUNCE_ADMIN_RIGHTS_OWNER_CHECK);
    }

    // Check that rights have not already been renounced
    if (!fuseAdminHasRights) return uint(Error.NO_ERROR);

    // Set fuseAdminHasRights to false
    fuseAdminHasRights = false;

    // Emit FuseAdminRightsRenounced()
    emit FuseAdminRightsRenounced();

    return uint(Error.NO_ERROR);
}

It sets the fuseAdminHasRights flag to false, which will make hasAdminRights return false.

Withdrawing fuse/admin fees

Fuse and admin fees can be withdrawn to the fuse/admin addresses by calling the function _withdrawFuseFees and _withdrawAdminFees. It checks the withdraw amount is not greater than the cash reserves and the total fuse/admin fees, then transfers it to fuse/admin.

/* X can be FUSE or ADMIN */
/* account can be fuseAdmin or admin */
/* withdraw amount checks */

if (getCashPrior() < withdrawAmount) {
    return fail(Error.TOKEN_INSUFFICIENT_CASH, FailureInfo.WITHDRAW_X_FEES_CASH_NOT_AVAILABLE);
}

if (withdrawAmount > totalFuseFees) {
    return fail(Error.BAD_INPUT, FailureInfo.WITHDRAW_X_FEES_VALIDATION);
}

/* update stored fees and then transfer withdraw amount */
totalXFeesNew = totalXFees - withdrawAmount;
totalXFees = totalXFeesNew;

doTransferOut(address(account), withdrawAmount);

Supplier whitelist?

Rari Capital’s fork introduced several variables related to whitelisted accounts.

  • enforceWhitelist (whether or not the supplier whitelist is enforced)

  • whitelist (Maps addresses to booleans indicating if they are allowed to supply assets)

  • whitelistArray (An array of all whitelisted accounts)

  • whitelistIndexes (Indexes of account addresses in the whitelistArray array)

The Comptroller function _setWhitelistEnforcement can be called to toggle the enforceWhitelist boolean.

The Comptroller function _setWhitelistStatuses can be called to set the whitelist statuses for a list of suppliers.

However, I am not able to find where this whitelist is being enforced in the codebase.

Admins can unsupport markets

An admin can unsupport a market by calling the function _unsupportMarket. It removes the cToken from its list of markets if the cToken is not in use.

/* Make sure the cToken is listed in the market */
if (!markets[address(cToken)].isListed) return fail(Error.MARKET_NOT_LISTED, FailureInfo.UNSUPPORT_MARKET_DOES_NOT_EXIST);

/* Make sure the cToken is not in use */
if (cToken.totalSupply() > 0) return fail(Error.NONZERO_TOTAL_SUPPLY, FailureInfo.UNSUPPORT_MARKET_IN_USE);

/* Delete the cToken from the markets mapping */
delete markets[address(cToken)];

/* Find the market's index in _allMarkets for removal */
CToken[] memory _allMarkets = allMarkets;
uint len = _allMarkets.length;
uint assetIndex = len;
for (uint i = 0; i < len; i++) {
    if (_allMarkets[i] == cToken) {
        assetIndex = i;
        break;
    }
}

/* Remove the cToken from allMarkets array */
allMarkets[assetIndex] = allMarkets[allMarkets.length - 1];
allMarkets.length--;

/* Remove the cToken's reference to its underlying asset */
cTokensByUnderlying[cToken.isCEther() ? address(0) : CErc20(address(cToken)).underlying()] = CToken(address(0));
2
Share this post

Dissecting Rari Capital's Fuse Pool

0xkowloon.substack.com
Share
Comments
Top
New
Community

No posts

Ready for more?

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