Boost _ Folks Finance 34050 - [Smart Contract - High] Vulnerability in getLoanLiquidity leads to undervaluing stable debt

Submitted on Sun Aug 04 2024 18:02:43 GMT-0400 (Atlantic Standard Time) by @A2Security for Boost | Folks Finance

Report ID: #34050

Report type: Smart Contract

Report severity: High

Target: https://testnet.snowtrace.io/address/0x2cAa1315bd676FbecABFC3195000c642f503f1C9

Impacts:

  • Protocol insolvency

  • Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

Description

Brief Description

There is a flaw in the implementation of the liquidity calculations for loans where the effective borrow value will be significantly undervalued for stable borrows. Specifically, the getLiquidity() function incorrectly calculates the effective borrow value due to a mistake in the order of indexes when calling balance.calcBorrowBalance(). The getLiquidity() function is crucial because it used at the end of functions that reduces user loan health (including withdraw) to check if loan is healthy .

Impact

Worst case scenario this bug could be used by attacker, to directly steal funds from the protocol, by undervaluing his debt(with accrued interest) and then withdraw a large portion of his collateral (see poc) however this requires enough time untill enough interest is accrued for the balance to be devalued enough for the attacker to be able to withdraw a large amount of his collateral and because of this we think the high severity is fair.

Description

The function calcStableBorrowBalance() is used in getLoanLiquidity() to calculate the effective borrowbalance of a userLoan if he has stable debt. contracts/hub/logic/UserLoanLogic.sol

        effectiveValue = 0;
        poolsLength = loan.borPools.length;
        for (uint8 i = 0; i < poolsLength; i++) {
            poolId = loan.borPools[i];

            LoanManagerState.UserLoanBorrow memory loanBorrow = loan.borrows[poolId];
            balance = loanBorrow.lastStableUpdateTimestamp > 0
@>                ? calcStableBorrowBalance(loanBorrow.balance, loanBorrow.lastInterestIndex, loanBorrow.stableInterestRate, block.timestamp - loanBorrow.lastStableUpdateTimestamp)
                : calcVariableBorrowBalance(loanBorrow.balance, loanBorrow.lastInterestIndex, pools[poolId].getUpdatedVariableBorrowInterestIndex());
            priceFeed = oracleManager.processPriceFeed(poolId);
            effectiveValue += MathUtils.calcBorrowAssetLoanValue(balance, priceFeed.price, priceFeed.decimals, loanPools[poolId].borrowFactor);
        }
        loanLiquidity.effectiveBorrowValue = effectiveValue;

There is a bug in the order of parameter passed to calcBorrowBalance() leading to the balance being undervalued (Oldindex is divided by newIndex instead of the correct order)

    function calcStableBorrowBalance(uint256 balance, uint256 loanInterestIndex, uint256 loanInterestRate, uint256 stableBorrowChangeDelta) private pure returns (uint256) {
        uint256 stableBorrowInterestIndex = MathUtils.calcBorrowInterestIndex(loanInterestRate, loanInterestIndex, stableBorrowChangeDelta);
        return balance.calcBorrowBalance(loanInterestIndex, stableBorrowInterestIndex);
    }

and this is how the calcStableBorrowBalance() is implemented: contracts/hub/logic/UserLoanLogic.sol

    function calcStableBorrowBalance(uint256 balance, uint256 loanInterestIndex, uint256 loanInterestRate, uint256 stableBorrowChangeDelta) private pure returns (uint256) {
        uint256 stableBorrowInterestIndex = MathUtils.calcBorrowInterestIndex(loanInterestRate, loanInterestIndex, stableBorrowChangeDelta);
        return balance.calcBorrowBalance(loanInterestIndex, stableBorrowInterestIndex);
    }

Tools Used

Manual Review

Recommended Mitigation Steps: To mitigate this issue simply fix the order of param in calcStableBorrowBalance():

    function calcStableBorrowBalance(uint256 balance, uint256 loanInterestIndex, uint256 loanInterestRate, uint256 stableBorrowChangeDelta) private pure returns (uint256) {
        uint256 stableBorrowInterestIndex = MathUtils.calcBorrowInterestIndex(loanInterestRate, loanInterestIndex, stableBorrowChangeDelta);
-        return balance.calcBorrowBalance(loanInterestIndex, stableBorrowInterestIndex);
+        return balance.calcBorrowBalance(stableBorrowInterestIndex, loanInterestIndex);
    }

Proof of concept

Proof Of Concept

RESULT:

Ran 1 test for test/pocs/forktest.t.sol:Pocs2
[PASS] test_poc_02() (gas: 1329432)
Logs:
  amount gained by withdrawal 7000 usdc
  amounts Bob stole 1999 usdc

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 524.96ms (11.99ms CPU time)

Ran 1 test suite in 529.44ms (524.96ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

please first add the following file to test/pocs/base_test.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import "@forge-std/Test.sol";
import "contracts/hub/Hub.sol";
import "contracts/spoke/SpokeToken.sol";
import "contracts/spoke/SpokeCommon.sol";
import "contracts/hub/HubPool.sol";
import "contracts/hub/LoanManager.sol";
import "contracts/bridge/libraries/Messages.sol";
import "contracts/hub/AccountManager.sol";
import "contracts/bridge/BridgeRouter.sol";
import "contracts/bridge/HubAdapter.sol";
import "contracts/oracle/modules/NodeManager.sol";
import "contracts/hub/OracleManager.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract baseTest is Test {
    using SafeERC20 for address;
    // Hub public hub;

    uint256 mainnetFork;
    bytes32 public constant PREFIX = "HUB_ADAPTER_V1";
    address constant HUB_ADDRESS = 0xaE4C62510F4d930a5C8796dbfB8C4Bc7b9B62140; // Assuming this is the Hub address
    address constant SPOKE_COMMON = 0x6628cE08b54e9C8358bE94f716D93AdDcca45b00;
    address constant SPOKE_USDC = 0x89df7db4af48Ec7A84DE09F755ade9AF1940420b;
    address constant HUBPOOL_USDC = 0x1968237f3a7D256D08BcAb212D7ae28fEda72c34;
    address constant HUBPOOL_AVAX = 0xd90B7614551E799Cdef87463143eCe2efd4054f9;
    address constant SPOKE_AVAX = 0xBFf8b4e5f92eDD0A5f72b4b0E23cCa2Cc476ce2a;
    address constant LOAN_MANAGER = 0x2cAa1315bd676FbecABFC3195000c642f503f1C9;
    address constant ACCOUNT_MANAGER = 0x3324B5BF2b5C85999C6DAf2f77b5a29aB74197cc;
    address constant USDC_TOKEN = 0x5425890298aed601595a70AB815c96711a31Bc65;
    address constant ADAPTER = 0xf472ab58969709De9FfEFaeFFd24F9e90cf8DbF9;
    address constant LISTING_ROLE = 0x16870a6A85cD152229B97d018194d66740f932d6;
    address constant BRIDGE_ROUTER_HUB = 0xa9491a1f4f058832e5742b76eE3f1F1fD7bb6837;
    address constant ORACLE_MANAGER = 0x46c425F4Ec43b25B6222bcc05De051e6D3845165;
    address constant NODE_MANAGER = 0xA758c321DF6Cd949A8E074B22362a4366DB1b725;
    uint16 constant STABELE_LOAN_TYPE_ID = 1;
    uint16 constant VARIABLE_LOAN_TYPE_ID = 2;
    uint16 constant CHAIN_ID = 1; // Assuming Ethereum mainnet

    Hub hub;
    SpokeCommon spokeCommon;
    AccountManager accountManager;
    SpokeToken spokeUsdc;
    SpokeToken spokeAvax;
    HubPool hubPoolUsdc;
    HubPool hubPoolAvax;
    LoanManager loanManager;
    HubAdapter adapter;
    BridgeRouter bridgeRouterHub;
    NodeManager nodeManager;
    OracleManager oracleManager;
    Messages.MessageParams DUMMY_MESSAGE_PARAMS = Messages.MessageParams({adapterId: 1, returnAdapterId: 1, receiverValue: 0, gasLimit: 0, returnGasLimit: 0});
    ///// users account ids :
    address bob = makeAddr("bob"