Core Contracts

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

Vesting: Rounding Errors

Summary

The vesting contract calculates vested token amounts using integer division, which truncates fractional values. Over time, these rounding errors accumulate, causing users to receive fewer tokens than entitled. For example, a user owed 100.5 tokens receives only 100, losing 0.5 tokens permanently. This violates fairness and leads to financial loss for beneficiaries.

Code Snippet

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/minters/RAACReleaseOrchestrator/RAACReleaseOrchestrator.sol#L197

Vulnerability Details

Example Scenario

  • Vesting Schedule Setup: imagine that :

Total tokens: 1,000 RAAC

Vesting duration: 100 days

  • Daily Vesting Calculation:

Daily vested amount =

1,000 RAAC × 1 day ÷ 100days =10 RAAC/day

  • Partial Day Vesting:

After 1.5 days, entitled amount = 1.5×10=15 RAAC.

  • Actual Calculation:

uint256 vestedAmount = (1000 * 1.5 days) / 100 days = 15; // Correct
  • After 0.7 days :

vestedAmount = (1000 * 0.7 days) / 100 days = 7; // Truncates 0.7 → 7 RAAC (should be 7 RAAC)
  • No Loss Here, but consider smaller values:

If total tokens = 999 RAAC

Daily vested : 900 ÷ 100 =
9.99 RAAC/day.

After 1 day: 9 RAAC (truncated from 9.99).

Loss: 0.99 RAAC per day.

Impact

  1. Cumulative Loss: Small truncations compound over time, especially for long-duration vesting.

  2. Unclaimed Dust: Residual tokens remain trapped in the contract.

  3. Reputation Damage: Users perceive the protocol as unfair.

Tools Used

Manual review

Recommendations

Use scaled arithmetic to preserve precision during calculations and distribute residual amounts fairly.

  • Step 1: Calculate Vested Amount with Higher Precision
    Multiply token amounts by a scaling factor (e.g., 1e18) during calculations:

function _calculateReleasableAmount(VestingSchedule memory schedule) internal view returns (uint256) {
if (block.timestamp < schedule.startTime + VESTING_CLIFF) return 0;
if (block.timestamp >= schedule.startTime + schedule.duration) {
return schedule.totalAmount - schedule.releasedAmount;
}
uint256 timeFromStart = block.timestamp - schedule.startTime;
// Scale to handle decimals
uint256 vestedAmountScaled = (schedule.totalAmount * 1e18 * timeFromStart) / schedule.duration;
uint256 vestedAmount = vestedAmountScaled / 1e18; // Truncate only once
return vestedAmount - schedule.releasedAmount;
}
  • Step 2: Track Residual Dust and Distribute Fairly
    Add a residual tracker and allocate remaining tokens in the final claim:

// RAACReleaseOrchestrator.sol
mapping(address => uint256) private _residualDust;
function _calculateReleasableAmount(...) internal view returns (uint256) {
...
uint256 vestedAmountScaled = (schedule.totalAmount * 1e18 * timeFromStart) / schedule.duration;
uint256 vestedAmount = vestedAmountScaled / 1e18;
uint256 residual = vestedAmountScaled - (vestedAmount * 1e18); // Track residual (0-0.999... RAAC)
// Store residual for final claim
if (timeFromStart + 1 days >= schedule.duration) {
return (vestedAmount + _residualDust[beneficiary]) - schedule.releasedAmount;
} else {
_residualDust[beneficiary] += residual;
return vestedAmount - schedule.releasedAmount;
}
}

Why This Fix Works

  • Scaled Precision: Intermediate calculations use 1e18 precision, minimizing truncation.

  • Residual Tracking: Captures fractional tokens and allocates them in the final claim.

  • Fair Distribution: Users receive nearly exact amounts, with dust resolved at the end.

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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