Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: low
Valid

Precision Loss Issue in FeeCollector Contract

Summary

The FeeCollector contract has a precision loss issue in the _calculatePendingRewards function. This issue arises due to Solidity’s integer division, which truncates decimal values, leading to inaccurate reward distributions.

_calculatePendingRewards

Vulnerability Details

The _calculatePendingRewards function calculates a user's share of the total distributed rewards using integer division:

uint256 share = (totalDistributed * userVotingPower) / totalVotingPower;

Since Solidity does not support floating-point arithmetic, this operation discards fractional values, resulting in incorrect rewards being transferred.

Foundry Test:

├─ [38016] FeeCollector::claimRewards(LendingPoolTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
│ ├─ [1644] veRAACToken::getVotingPower(LendingPoolTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) [staticcall]
│ │ └─ ← [Return] 29761315 [2.976e7]
│ ├─ [374] veRAACToken::getTotalVotingPower() [staticcall]
│ │ └─ ← [Return] 250684931506849315 [2.506e17]
│ ├─ [0] console::log("totalDistributed is :", 437500000000000 [4.375e14]) [staticcall]
│ │ └─ ← [Stop]
│ ├─ [3281] ERC20Mock::transfer(LendingPoolTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 51939 [5.193e4])
│ │ ├─ emit Transfer(from: FeeCollector: [0x51a240271AB8AB9f9a21C82d9a85396b704E164d], to: LendingPoolTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], value: 51939 [5.193e4])
│ │ └─ ← [Return] true
│ ├─ emit RewardClaimed(user: LendingPoolTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], amount: 51939 [5.193e4])
│ └─ ← [Return] 51939 [5.193e4]
└─ ← [Stop]

totalDistributed = 437500000000000

userVotingPower = 29761315

totalVotingPower = 250684931506849315

437500000000000 * 29761315 = 13018325312500000000000

13018325312500000000000 / 250684931506849315 = 51,931.999880464480888508196688651

The User was expected to make as much as = 51,931.999880464480888508196688651

but the Due to the Issue they are getting = 51,931

Impact

  • Users receive lower rewards than they should.

  • The issue accumulates over time, leading to significant unclaimed rewards.

  • Can cause dissatisfaction among users and undermine trust in the reward mechanism.

Observed Behavior

  • Expected reward: 51,939.999880464480888508196688651

  • Actual reward transferred: 51,939

Tools Used

  • Manual code review

  • Solidity debugging tools (e.g., Hardhat, Foundry)

  • Console logging (console.log in Foundry) to observe precision loss

POC : FOUNDRY

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/src/Test.sol";
import "../../contracts/core/pools/LendingPool/LendingPool.sol";
import "../../contracts/core/collectors/FeeCollector.sol";
import "../../contracts/core/collectors/Treasury.sol";
import "../../contracts/mocks/core/tokens/ERC20Mock.sol";
import "../../contracts/core/tokens/veRAACToken.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract LendingPoolTest is Test {
FeeCollector feeCollector;
veRAACToken veraactoken;
ERC20Mock token;
LendingPool lendingPool;
Treasury treasury;
address admin;
address user;
uint256 public swapTaxRate = 100; // 1% swap tax (100 basis points)
uint256 public burnTaxRate = 50; // 0.5% burn tax (50 basis points)
address public TokenAddress;
uint256 constant WAD = 1e18;
uint256 constant RAY = 1e27;
uint256 STARTING_BALANCE = 100 ether;
uint256 amount = 10 ether;
function setUp() public {
admin = vm.addr(1);
user = vm.addr(2);
vm.startPrank(admin);
token = new ERC20Mock("ERC20Mock", "ERC");
TokenAddress = address(token);
veraactoken = new veRAACToken(TokenAddress);
treasury = new Treasury(admin);
feeCollector = new FeeCollector(TokenAddress, address(veraactoken), address(treasury), admin, admin);
token.mint(TokenAddress, STARTING_BALANCE);
token.mint(admin, STARTING_BALANCE);
//token.mint(address(this), amount);
token.mint(address(treasury), STARTING_BALANCE);
IERC20(TokenAddress).balanceOf(admin);
IERC20(TokenAddress).balanceOf(TokenAddress);
IERC20(TokenAddress).balanceOf(address(this));
// lendingPool = new LendingPool(); // Deploy the contract or use an address
vm.stopPrank();
}
function testClaimReward() public {
token.mint(address(this), amount);
IERC20(address(token)).transfer(address(this), amount);
IERC20(address(token)).approve(address(feeCollector), amount);
IERC20(address(token)).approve(address(veraactoken), amount);
IERC20(TokenAddress).balanceOf(address(this));
uint256 _amount = 1 ether;
uint256 duration = 365 days + 1 days;
feeCollector.collectFee(625000000000000, 1);
veraactoken.lock(_amount, duration);
skip(365 days + 1 days);
vm.startPrank(admin);
feeCollector.distributeCollectedFees();
vm.stopPrank();
feeCollector.claimRewards(address(this));
}
}

POC Setup:

Install forge-std and add it in the node\_modules folder and create a folder called foundry and create a file called feeCollector.t.sol and paste my poc in the file and you are good to go
.

The Hole idea was that when the test run the token reward that was supposed to be sent to the user is "51,931.999880464480888508196688651" but Due to Precision issue in solidity it will send "51931" to the user living some part of their reward in the contract : "999880464480888508196688651"

Recommendations

Solution 1: Scale Up Before Division

Multiplying values by a higher factor before performing the division helps maintain precision:

uint256 share = (totalDistributed * userVotingPower * 1e18) / totalVotingPower;
share = share / 1e18; // Scale back down

This prevents truncation errors by retaining more precise values during calculations.

Solution 2: Use a Fixed-Point Math Library

Utilizing libraries such as PRBMath or FixedPointMath ensures accurate calculations without significant precision loss.

Implementing one of these solutions will improve the fairness and accuracy of reward distributions in the contract.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

FeeCollector::_calculatePendingRewards suffers from precision loss in integer division, causing users to receive fewer rewards than they're entitled to

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.