Core Contracts

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

Incorrect Total Duration Calculation in TimeWeightedAverage Library leads to incorrect protocol accounting such as in veRAAC boosts.

Summary

In the TimeWeightedAverage library, there's a logical error in the updateValue function where it incorrectly accumulates the total duration of a period, despite this value being fixed when the period is created. This leads to incorrect calculations when computing time-weighted averages such as in boost calculations.

Vulnerability Details

The issue stems from how duration is handled across two functions:
In createPeriod:

function createPeriod(
Period storage self,
uint256 startTime,
uint256 duration,
uint256 initialValue,
uint256 weight
) internal {
// ...
self.totalDuration = duration; // @audit Sets a fixed duration
// ...
}

In updateValue:

function updateValue(
Period storage self,
uint256 newValue,
uint256 timestamp
) internal {
// ...
unchecked {
uint256 duration = timestamp - self.lastUpdateTime;
if (duration > 0) {
// ...
self.totalDuration += duration; // @audit Incorrectly adds to fixed duration
}
}
}

The fundamental problem is that totalDuration represents the total fixed duration of a period (endTime - startTime), but updateValue keeps adding intermediate durations between updates to it. This creates several issues:

  1. The total duration becomes inflated with each value update

  2. This affects any calculation that relies on totalDuration. i.e calculateTimeWeightedAverage

  3. The period's actual duration (endTime - startTime) no longer matches the tracked totalDuration

For instance in veRAACToken, boost update is done when RAACTokens are locked or increased in the lock. Now the _updateBoostState uses the BoostCalculator library which uses the TimeWeightedAverage Library when updating boost period, updateBoostPeriod;

function updateBoostPeriod(BoostState storage state) internal {
// ...
// If no period exists, create initial period starting from current block
if(periodStart > 0) {
// If current period has ended, create new period
if (currentTime >= periodStart + state.boostWindow) {
@> TimeWeightedAverage.createPeriod(
state.boostPeriod,
currentTime,
state.boostWindow,
state.votingPower,
state.maxBoost
);
return;
}
// Update existing period
@> state.boostPeriod.updateValue(state.votingPower, currentTime);
return;
}
// ...
}

From the code above we see that the global boost period, i.e state.boostPeriod is created and also updated. Now by the current implementation, the totalDuration will be incorrectly updated.

Impact

  • Incorrect time-weighted average calculations, (for example calculateTimeWeightedAverage) due to inflated duration

  • Distorted period tracking for all contracts using the TimeWeightedAverage library.

Tools Used

Manual code review

Recommendations

Only update if the totalDuration is zero (I do acknowledge that a period can be created without using createPeriod so make the incrementing conditional)

function updateValue(Period storage self, uint256 newValue, uint256 timestamp) internal {
// ...
unchecked {
uint256 duration = timestamp - self.lastUpdateTime;
if (duration > 0) {
// ...
// @audit-fix added the check.
if (self.totalDuration == 0) self.totalDuration += duration;
}
// ...
Updates

Lead Judging Commences

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

TimeWeightedAverage::createPeriod incorrectly initializes totalDuration to period duration instead of zero, causing double-counting and inflated duration accumulation

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

TimeWeightedAverage::createPeriod incorrectly initializes totalDuration to period duration instead of zero, causing double-counting and inflated duration accumulation

Support

FAQs

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