Glamorous Peach Crow
Medium
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.
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;
}
No response
No response
No response
- Loss of protocol revenue due to systematically lower fees.
- Users can optimize their transaction amounts to minimize fees.
- Unfair advantage to users who understand and exploit this behavior.
- Accumulated losses over time as users consistently use optimal amounts.
// 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%
- 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;
}
}
- 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;
}