Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shaggy Lava Mustang - Market may not increase total reserves during times of high volume/frequent transactions #44

Open
sherlock-admin3 opened this issue Dec 21, 2024 · 0 comments
Labels
Sponsor Confirmed The sponsor acknowledged this issue is valid Won't Fix The sponsor confirmed this issue will not be fixed

Comments

@sherlock-admin3
Copy link
Contributor

Shaggy Lava Mustang

Medium

Market may not increase total reserves during times of high volume/frequent transactions

Summary

The issue arises from the fact that rate variables are set on a per-second basis, and the accrualBlockTimestamp variable is updated (i.e., the interest accrual window moves forward) even if no interest is added due to precision loss.

Vulnerability Detail

In the BaseJumpRateModelV2.sol, multiplierPerTimestamp is divided by timestampsPerYear to calculate the multiplier per second. Considering that baseRatePerYear is likely to have the same or similar values as in the Compound v2 protocol, this timestamp-based calculation results in values approximately 15 times lower compared to the original "block-per-year" formula.

    function updateJumpRateModelInternal(
        uint256 baseRatePerYear,
        uint256 multiplierPerYear,
        uint256 jumpMultiplierPerYear,
        uint256 kink_
    ) internal {
        baseRatePerTimestamp = ((baseRatePerYear * BASE) / timestampsPerYear) / BASE;
>>      multiplierPerTimestamp = (multiplierPerYear * BASE) / (timestampsPerYear * kink_);
        jumpMultiplierPerTimestamp = ((jumpMultiplierPerYear * BASE) / timestampsPerYear) / BASE;
        kink = kink_;

        emit NewInterestParams(baseRatePerTimestamp, multiplierPerTimestamp, jumpMultiplierPerTimestamp, kink);
    }

These lower values, combined with the fact that the Sonic network produces at least 1 block per second, make the Machi protocol more prone to precision loss. This issue is particularly significant during high-frequency interactions with the protocol, as these trigger CToken.accrueInterest calculations each time.

Particularly in CToken.accrueInterest(), the value to add to reserves is calculated as the interest accumulated multiplied by the reserve factor. The reserve factor is expected to have a value less than 1e18, as it represents the percentage of interest that must be allocated to reserves.

        uint256 totalReservesNew =
            mul_ScalarTruncateAddUInt(Exp({mantissa: reserveFactorMantissa}), interestAccumulated, reservesPrior);

This can lead to truncation and result in zero if the interest accumulated between subsequent calls to accrueInterest is too low, resulting in just a few wei.

For example, the reserve factor for cWBTC in the Compound v2 protocol is 0.3 (link). This means that if the interest accumulated is less than 4 wei, the result will be zero due to truncation: 3 wei * 0.3 = 0.9 => 0.

PoC

Consider the cWBTC market with the following parameters:

1 SONIC = 1 USD
1 WBTC = 100,000 USD

totalCash: 100 WBTC (or 10,000,000 USD)
reserveFactor: 0.3
multiplierPerYear: 0.25e18
utilizationRate: 5%

Total borrows: 5 WBTC.

Insert this test into CToken.t.sol:

function test_accrueInterest_PoC() public {
    vm.prank(admin);
    cWbtcDelegator._setReserveFactor(300000000000000000); // 0.3

    uint256 wBTCInitialBalance = 100 * (10 ** wbtc.decimals());
    deal(address(wbtc), bob, wBTCInitialBalance);

    // Supply wBTC as collateral
    vm.startPrank(bob);
    wbtc.approve(address(cWbtcDelegator), type(uint256).max);
    cWbtcDelegator.mint(wBTCInitialBalance);
    vm.stopPrank();

    uint aliceCollateral = 1 * 1e6 ether;
    deal(alice, aliceCollateral);

    // deposit and enter market
    vm.startPrank(alice);
    cSonic.mintAsCollateral{value: aliceCollateral}();

    // Fetch initial liquidity and shortfall
    (, uint256 liquidity, uint256 shortfall) = comptroller.getAccountLiquidity(alice);
    // console.log("Initial liquidity/shortfall    :", liquidity, shortfall);

    // Fetch wBTC price
    uint wbtcPrice = priceOracle.getUnderlyingPrice(CToken(address(cWbtcDelegator)));
    uint borrowableWBTC = (liquidity * 1e36) / wbtcPrice / 1e18;

    // Borrow max wBTC
    cWbtcDelegator.borrow(borrowableWBTC);

    uint utilizationRate = (cWbtcDelegator.totalBorrows() * 1e18) / (cWbtcDelegator.totalBorrows() + cWbtcDelegator.getCash());
    console.log("Utilization Rate (%)            :", utilizationRate * 100 / 1e18);

    uint totalReserves = cWbtcDelegator.totalReserves();
    console.log("totalReserves (wBTC)            :", totalReserves);

    // Fetch initial borrow balance
    uint initialBorrowBalance = cWbtcDelegator.borrowBalanceCurrent(alice);

    {

    uint secs = 86_400;
    uint period = 10;

    // vm.warp(block.timestamp + secs);
    // cWbtcDelegator.accrueInterest(); 

    for (uint256 i = 0; i < secs / period; ++i) {
        vm.warp(block.timestamp + period);
        cWbtcDelegator.accrueInterest(); // Accrue interest on the wBTC market
    }

    console.log("");
    console.log(secs, "sec passed");
    console.log("");

    }

    // Fetch updated borrow balance
    uint updatedBorrowBalance = cWbtcDelegator.borrowBalanceCurrent(alice);

    // Calculate accrued interest
    uint interestAccrued = updatedBorrowBalance - initialBorrowBalance;
    console.log("Borrower interest accrued (wBTC):", interestAccrued);

    uint updatedTotalReserves = cWbtcDelegator.totalReserves();
    console.log("totalReserves (wBTC)            :", updatedTotalReserves);

    vm.stopPrank();
}

Output when test is run with the accrueInterest called once per 10 seconds for 1 day:

$ forge test --match-test test_accrueInterest_PoC -vvv
Ran 1 test for test/CToken.t.sol:CTokenTest
[PASS] test_accrueInterest_PoC() (gas: 141057857)
Logs:
  Utilization Rate (%)           : 5
  totalReserves (wBTC)           : 0

  86400 sec passed

  Borrower interest accrued (wBTC): 21404
  totalReserves (wBTC)           : 0

As can be seen, reserves were not increased and stayed at 0.
At the same time, a real totalReserves increase must be 21404 * 0.3 = 6421.

Impact

Market (CToken) may not accrue total reserves.

Preconditions:

  • High volume of transactions (once every 10 to 15 seconds).
  • Market utilization rate is no more than 5 - 10 %.

Code Snippet

https://github.com/sherlock-audit/2024-12-mach-finance/blob/main/contracts/src/BaseJumpRateModelV2.sol#L155

https://github.com/sherlock-audit/2024-12-mach-finance/blob/main/contracts/src/CToken.sol#L359-L360

Recommendation

Do not update accrualBlockTimestamp if calculations result in zero.

@sherlock-admin3 sherlock-admin3 added Won't Fix The sponsor confirmed this issue will not be fixed Sponsor Confirmed The sponsor acknowledged this issue is valid labels Dec 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Sponsor Confirmed The sponsor acknowledged this issue is valid Won't Fix The sponsor confirmed this issue will not be fixed
Projects
None yet
Development

No branches or pull requests

1 participant