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 thewhitelistArray
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));