Boost _ Folks Finance 33978 - [Smart Contract - Critical] Attacker can Inflate effectiveCollateralValue

Submitted on Sat Aug 03 2024 06:43:07 GMT-0400 (Atlantic Standard Time) by @arno for Boost | Folks Finance

Report ID: #33978

Report type: Smart Contract

Report severity: Critical

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

A user can exploit the LoanManager::deposit() function to deposit a 0 token amount for a specific loanID, resulting in the colPools array containing duplicate loanIDs of the same pool. This causes the UserLoanLogic::getLoanLiquidity function, which calculates effectiveCollateralValue by iterating through all poolIds in the colPools array, to return an incorrect and inflated effectiveCollateralValue. This vulnerability can be exploited in various scenarios, leading to significant issues within the protocol.

Vulnerability Details

In the protocol, users can deposit collateral using the LoanManager::deposit() function, which allows them to pass any amount, including 0.

Code Snippet: LoanManager::deposit()

function deposit(
    bytes32 loanId,
    bytes32 accountId,
    uint8 poolId,
    uint256 amount
) external override onlyRole(HUB_ROLE) nonReentrant {
    if (!isUserLoanActive(loanId)) revert UnknownUserLoan(loanId);
    if (!isUserLoanOwner(loanId, accountId)) revert NotAccountOwner(loanId, accountId);

    LoanManagerLogic.executeDeposit(
        _userLoans,
        _loanTypes,
        _pools,
        _userPoolRewards,
        DataTypes.ExecuteDepositParams({ loanId: loanId, poolId: poolId, amount: amount })
    );
}

Code Snippet: executeDeposit Function in LoanManagerLogic

function executeDeposit(
    mapping(bytes32 => LoanManagerState.UserLoan) storage userLoans,
    mapping(uint16 loanTypeId => LoanManagerState.LoanType) storage loanTypes,
    mapping(uint8 => IHubPool) storage pools,
    mapping(bytes32 accountId => mapping(uint8 poolId => LoanManagerState.UserPoolRewards)) storage userPoolRewards,
    DataTypes.ExecuteDepositParams memory params
) external {
    LoanManagerState.UserLoan storage userLoan = userLoans[params.loanId];
    LoanManagerState.LoanType storage loanType = loanTypes[userLoan.loanTypeId];
    LoanManagerState.LoanPool storage loanPool = loanType.pools[params.poolId];

    if (loanType.isDeprecated) revert LoanManagerState.LoanTypeDeprecated(userLoan.loanTypeId);
    if (!loanPool.isAdded) revert LoanManagerState.LoanPoolUnknown(userLoan.loanTypeId, params.poolId);
    if (loanPool.isDeprecated) revert LoanManagerState.LoanPoolDeprecated(userLoan.loanTypeId, params.poolId);

    IHubPool pool = pools[params.poolId];
    DataTypes.DepositPoolParams memory depositPoolParams = pool.updatePoolWithDeposit(params.amount);

    if (
        loanPool.isCollateralCapReached(
            depositPoolParams.priceFeed,
            depositPoolParams.fAmount,
            depositPoolParams.depositInterestIndex
        )
    ) revert CollateralCapReached(params.poolId);

    RewardLogic.updateRewardIndexes(loanPool, params.poolId);
    RewardLogic.updateUserCollateralReward(userPoolRewards, userLoan, loanPool, params.poolId);

    userLoan.increaseCollateral(params.poolId, depositPoolParams.fAmount);
    loanPool.increaseCollateral(depositPoolParams.fAmount);

    emit Deposit(params.loanId, params.poolId, params.amount, depositPoolParams.fAmount);
}

Calculation of fAmount

The fAmount is calculated based on the amount input:

depositPoolParams.fAmount = amount.toFAmount(depositInterestIndex);

Rounding Issue in fAmount

When the amount is not 0, the fAmount can round down to 0 as well in certain cases:

function toFAmount(uint256 underlyingAmount, uint256 depositInterestIndexAtT) internal pure returns (uint256) {
    return underlyingAmount.mulDiv(ONE_18_DP, depositInterestIndexAtT);
}

increaseCollateral Function

The increaseCollateral function then adds the poolId to the colPools array if the balance was previously 0:

function increaseCollateral(LoanManagerState.UserLoan storage loan, uint8 poolId, uint256 fAmount) external {
    if (loan.collaterals[poolId].balance == 0) loan.colPools.push(poolId);
    loan.collaterals[poolId].balance += fAmount;
}

This indicates that when a user deposits for the first time into the pool, if the balance is 0, the poolId is pushed to the colPools array, and the associated balance for that pool is increased. By passing a 0 token amount, this function will keep pushing the poolId to the colPools array, causing it to contain duplicate pool IDs. This can be exploited to inflate the effectiveCollateralValue in the UserLoanLogic::getLoanLiquidity() function.

Code Snippet: Inflating effectiveCollateralValue

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

    balance = loan.collaterals[poolId].balance.toUnderlingAmount(
        pools[poolId].getUpdatedDepositInterestIndex()
    );
    priceFeed = oracleManager.processPriceFeed(poolId);
    effectiveValue += MathUtils.calcCollateralAssetLoanValue(
        balance,
        priceFeed.price,
        priceFeed.decimals,
        loanPools[poolId].collateralFactor
    );
}
loanLiquidity.effectiveCollateralValue = effectiveValue;

Attack Path

  1. Alice deposits a small amount of collateral in Pool A and Pool B.

  2. Bob discovers that by depositing 0 tokens multiple times, the same Pool ID is repeatedly added to his colPools array.

  3. Bob exploits this by inflating his effectiveCollateralValue, allowing him to:

    • Borrow significantly more than he should be able to.

    • Bypass the isLoanOverCollateralized checks, enabling him to withdraw collateral while having outstanding loans.

    • Avoid liquidation since the inflated effectiveCollateralValue prevents his loan from being flagged as under-collateralized.

Impact Details

If exploited, this vulnerability could lead to significant financial losses within the protocol. The inflated effectiveCollateralValue could allow users to borrow more than they should, withdraw collateral they aren't entitled to, or avoid liquidation, potentially resulting in insolvency of the protocol and loss of funds for all users.

References

Proof of concept

Proof of Concept

import { expect } from "chai";
import { ethers } from "hardhat";
import { PANIC_CODES } from "@nomicfoundation/hardhat-chai-matchers/panic";
import { loadFixture, reset, time } from "@nomicfoundation/hardhat-toolbox/network-helpers";
import {