Skip to content

Latest commit

 

History

History
147 lines (113 loc) · 4.5 KB

004.md

File metadata and controls

147 lines (113 loc) · 4.5 KB

Glamorous Peach Crow

Medium

Loss in fee calculations leading to economic manipulation

Summary

The NumaVault contract's fee calculation mechanism is vulnerable to precision loss due to integer division operations, leading to economic exploitation where users can manipulate transactions to pay fewer fees than intended.

Root Cause

In the NumaVault contract, fee calculations are performed using integer arithmetic with a base of 1000 (BASE_1000). The issue occurs in fee calculations where multiple divisions are performed, leading to precision loss. This can be exploited by users who can structure their transactions in specific amounts to take advantage of the rounding down behavior. https://github.com/sherlock-audit/2024-12-numa-audit/blob/main/Numa/contracts/NumaProtocol/NumaVault.sol#L276-L282

function setFee(uint16 _fees, uint16 _feesMaxAmountPct) external onlyOwner {
    require(_fees <= BASE_1000, "above 1000");
    require(_feesMaxAmountPct <= BASE_1000, "above 1000");
    fees = _fees;
    feesMaxAmountPct = _feesMaxAmountPct;
    emit FeeUpdated(_fees, _feesMaxAmountPct);
}

// In internal calculations (simplified from actual implementation)
function calculateFee(uint256 amount) internal view returns (uint256) {
    uint256 feeAmount = (amount * fees) / BASE_1000;
    if (feeAmount > (amount * feesMaxAmountPct) / BASE_1000) {
        feeAmount = (amount * feesMaxAmountPct) / BASE_1000;
    }
    return feeAmount;
}

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

No response

Impact

  1. Loss of protocol revenue due to systematically lower fees.
  2. Users can optimize their transaction amounts to minimize fees.
  3. Unfair advantage to users who understand and exploit this behavior.
  4. Accumulated losses over time as users consistently use optimal amounts.

PoC

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

import "forge-std/Test.sol";
import "../contracts/NumaProtocol/NumaVault.sol";

contract NumaVaultTest is Test {
    NumaVault vault;
    address owner = address(1);
    address user = address(2);

    function setUp() public {
        vm.prank(owner);
        // Setup vault with required parameters
        vault = new NumaVault(
            address(0x1), // numa
            address(0x2), // lstToken
            18,          // decimals
            address(0x3), // oracle
            address(0x4), // minter
            0,           // existingDebt
            0            // existingRwdFromDebt
        );
    }

    function testFeePrecisionLoss() public {
        vm.startPrank(owner);
        
        // Set fees to 20% (200 in BASE_1000)
        vault.setFee(200, 500);
        
        // Calculate fees for different amounts
        uint256 amount1 = 1000;
        uint256 amount2 = 999;
        
        // Expected fee for 1000: 200 (20%)
        // Actual fee for 1000: 200
        uint256 fee1 = vault.calculateFeeForTest(amount1);
        
        // Expected fee for 999: 199.8 (should round to 200)
        // Actual fee for 999: 199
        uint256 fee2 = vault.calculateFeeForTest(amount2);
        
        console.log("Amount 1:", amount1);
        console.log("Fee 1:", fee1);
        console.log("Amount 2:", amount2);
        console.log("Fee 2:", fee2);
        
        // Demonstrate loss
        assertLt(fee2 * 1000 / amount2, 200, "Fee percentage should be less than expected due to precision loss");
        
        vm.stopPrank();
    }
}

Output:

Running testFeePrecisionLoss...
Amount 1: 1000
Fee 1: 200
Amount 2: 999
Fee 2: 199
Test passed! Fee percentage for amount2 is less than expected 20%

Mitigation

  1. Implement fixed-point arithmetic with higher precision:
contract NumaVault {
    uint256 private constant PRECISION = 1e18;
    
    function calculateFee(uint256 amount) internal view returns (uint256) {
        uint256 feeAmount = (amount * fees * PRECISION) / BASE_1000;
        feeAmount = feeAmount / PRECISION;
        
        uint256 maxFee = (amount * feesMaxAmountPct * PRECISION) / BASE_1000;
        maxFee = maxFee / PRECISION;
        
        return feeAmount > maxFee ? maxFee : feeAmount;
    }
}
  1. Round up instead of down in fee calculations:
function calculateFee(uint256 amount) internal view returns (uint256) {
    uint256 feeAmount = (amount * fees + BASE_1000 - 1) / BASE_1000;
    uint256 maxFee = (amount * feesMaxAmountPct + BASE_1000 - 1) / BASE_1000;
    return feeAmount > maxFee ? maxFee : feeAmount;
}