Core Contracts

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

Missing Application of Weight in TimeWeightedAverage Calculations: Underweighted Accumulation Leading to Inaccurate Averages and Potential DoS

Summary

The TimeWeightedAverage library is responsible for tracking time-weighted values across defined periods. It does so by maintaining a running weighted sum and total elapsed duration to eventually compute a time-weighted average using the formula:

Time Weighted Average = Weighted Sum / Total Duration

and where the weighted sum for a constant value is intended to be:

Weighted Sum = Value * Weight * Duration

However, in the current implementation of the library, the external weight provided (e.g., weight = 2.5) is stored in the period struct but is never applied in the calculation of the time weighted sum in either the updateValue or calculateAverage functions. This omission means that the effective value is not scaled by the intended weight, causing the computed average to be incorrect. Over time, especially in systems that continuously update these values (such as reward or governance systems), this miscalculation can lead to a Denial-of-Service (DoS) scenario or significant misallocation of rewards.

Vulnerability Details

Affected Functions

1. TimeWeightedAverage::updateValue

This function updates the period's state by calculating the time elapsed since the last update and then adding the product of the current value and elapsed duration to the running weighted sum. The function is defined as follows:

function updateValue(Period storage self, uint256 newValue, uint256 timestamp) internal {
if (timestamp < self.startTime || timestamp > self.endTime) {
revert InvalidTime();
}
unchecked {
uint256 duration = timestamp - self.lastUpdateTime;
if (duration > 0) {
uint256 timeWeightedValue = self.value * duration;
if (timeWeightedValue / duration != self.value) revert ValueOverflow();
@> self.weightedSum += timeWeightedValue;
self.totalDuration += duration;
}
}
self.value = newValue;
self.lastUpdateTime = timestamp;
}

Issue:

  • The calculation timeWeightedValue = self.value * duration does not incorporate self.weight.

  • The intended calculation should multiply the value by the weight as well, i.e.,

    timeWeightedValue = (self.value * self.weight) * duration

2. TimeWeightedAverage::calculateAverage

This function calculates the time weighted average up to a given timestamp by including any pending time since the last update. Its current implementation is as follows:

function calculateAverage(Period storage self, uint256 timestamp) internal view returns (uint256) {
if (timestamp <= self.startTime) return self.value;
uint256 endTime = timestamp > self.endTime ? self.endTime : timestamp;
uint256 totalWeightedSum = self.weightedSum;
if (endTime > self.lastUpdateTime) {
uint256 duration = endTime - self.lastUpdateTime;
uint256 timeWeightedValue = self.value * duration;
if (duration > 0 && timeWeightedValue / duration != self.value) revert ValueOverflow();
@> // @info: timeWeightedValue is not multiplied by the weight
@> totalWeightedSum += timeWeightedValue;
}
return totalWeightedSum / (endTime - self.startTime);
}

Issue:

  • The line calculating timeWeightedValue does not include the weight.

  • The formula should reflect:

    timeWeightedValue = (self.value * self.weight) * duration

Proof of Concept

Scenario Walkthrough

Consider the following scenario involving Alice:

  1. Initial Setup:

    • Alice locks tokens with a fixed value of 10.

    • A weight of 2.5 is provided externally.

    • The period is defined with:

      • startTime = 0

      • endTime = 10

      • initialValue = 10

      • weight = 2.5

  2. Expected Calculation:

    • Intended Time Weighted Sum:
      The effective value should be:

      Effective Value = 10 * 2.5 = 25

      For a full duration of 10 time units:

      Intended Weighted Sum = 25 * 10 = 250

    • Intended Time Weighted Average:

      Average = 250 / 10 = 25

  3. Actual Calculation with Bug:

    • The functions calculate:

      timeWeightedValue = 10 * 10 = 100

    • The computed average becomes:

      Average = 100 / 10 = 10

    • Result: Alice’s boost or reward calculation based on this average is severely underestimated.

  4. Repeated Updates:

    • As Alice performs multiple updates (via updateValue) over time, the unweighted calculations continue, leading to further divergence from the intended metrics.

    • This may eventually cause the system to block further updates (DoS) or misallocate rewards.

Complete Function Definitions in Context

updateValue Function:

function updateValue(Period storage self, uint256 newValue, uint256 timestamp) internal {
if (timestamp < self.startTime || timestamp > self.endTime) {
revert InvalidTime();
}
unchecked {
uint256 duration = timestamp - self.lastUpdateTime;
if (duration > 0) {
uint256 timeWeightedValue = self.value * duration; // Missing: * self.weight
if (timeWeightedValue / duration != self.value) revert ValueOverflow();
@> self.weightedSum += timeWeightedValue;
self.totalDuration += duration;
}
}
self.value = newValue;
self.lastUpdateTime = timestamp;
}

calculateAverage Function:

function calculateAverage(Period storage self, uint256 timestamp) internal view returns (uint256) {
if (timestamp <= self.startTime) return self.value;
uint256 endTime = timestamp > self.endTime ? self.endTime : timestamp;
uint256 totalWeightedSum = self.weightedSum;
if (endTime > self.lastUpdateTime) {
uint256 duration = endTime - self.lastUpdateTime;
uint256 timeWeightedValue = self.value * duration; // Missing: * self.weight
if (duration > 0 && timeWeightedValue / duration != self.value) revert ValueOverflow();
@> // @info: timeWeightedValue is not multiplied by the weight
totalWeightedSum += timeWeightedValue;
}
return totalWeightedSum / (endTime - self.startTime);
}

Code PoC

  • TODO **********Anil don't forget to paste PoC's Test**************

Impact

  • Short-Term Impact:

    • Immediate miscalculation of averages leading to inaccurate reward distribution.

    • Misrepresentation of boost factors or voting power based on the undervalued time weighted average.

  • Long-Term Impact:

    • Persistent inaccuracies in critical financial or governance metrics can destabilize the protocol.

    • Over time, users may lose trust in the system if rewards and governance outcomes do not reflect their true stake.

    • In extreme cases, the accumulation of errors might cause a Denial-of-Service (DoS) for components relying on period expirations, affecting multiple facets of the protocol.

Tools Used

  • Manual Review

  • Foundry

Recommendations

The following diffs outline the recommended changes to correctly incorporate the weight into the time weighted sum calculations.

Diff for updateValue Function

function updateValue(Period storage self, uint256 newValue, uint256 timestamp) internal {
if (timestamp < self.startTime || timestamp > self.endTime) {
revert InvalidTime();
}
unchecked {
uint256 duration = timestamp - self.lastUpdateTime;
if (duration > 0) {
- uint256 timeWeightedValue = self.value * duration;
- if (timeWeightedValue / duration != self.value) revert ValueOverflow();
- self.weightedSum += timeWeightedValue;
+ uint256 timeWeightedValue = self.value * self.weight * duration;
+ if (duration > 0 && timeWeightedValue / (self.weight * duration) != self.value) revert ValueOverflow();
+ self.weightedSum += timeWeightedValue;
}
self.totalDuration += duration;
}
self.value = newValue;
self.lastUpdateTime = timestamp;
}

Diff for calculateAverage Function

function calculateAverage(Period storage self, uint256 timestamp) internal view returns (uint256) {
if (timestamp <= self.startTime) return self.value;
uint256 endTime = timestamp > self.endTime ? self.endTime : timestamp;
uint256 totalWeightedSum = self.weightedSum;
if (endTime > self.lastUpdateTime) {
uint256 duration = endTime - self.lastUpdateTime;
- uint256 timeWeightedValue = self.value * duration;
- if (duration > 0 && timeWeightedValue / duration != self.value) revert ValueOverflow();
- // @info: timeWeightedValue is not multiplied by the weight
- totalWeightedSum += timeWeightedValue;
+ uint256 timeWeightedValue = self.value * self.weight * duration;
+ if (duration > 0 && timeWeightedValue / (self.weight * duration) != self.value) revert ValueOverflow();
+ totalWeightedSum += timeWeightedValue;
}
return totalWeightedSum / (endTime - self.startTime);
}

Diff for calculateTimeWeightedAverage Function

function calculateTimeWeightedAverage(PeriodParams[] memory periods, uint256 timestamp)
public
pure
returns (uint256 weightedAverage)
{
uint256 totalWeightedSum;
uint256 totalDuration;
// We will iterate through each period and calculate the time-weighted average
for (uint256 i = 0; i < periods.length; i++) {
if (timestamp <= periods[i].startTime) continue;
uint256 endTime = timestamp > periods[i].endTime ? periods[i].endTime : timestamp;
uint256 duration = endTime - periods[i].startTime;
unchecked {
// Calculate time-weighted value by multiplying value by duration
// This represents the area under the curve for this period
uint256 timeWeightedValue = periods[i].value * duration;
if (timeWeightedValue / duration != periods[i].value) revert ValueOverflow();
totalWeightedSum += timeWeightedValue * periods[i].weight;
totalDuration += duration;
}
}
- return totalDuration == 0 ? 0 : totalWeightedSum / (totalDuration * 1e18);
+ // don't update this function with these modificications...
+ // if intended to reduce precision for simplicity and is a design choice
+ return totalDuration == 0 ? 0 : totalWeightedSum / totalDuration;
}

Prominent Formulas to Include

  • Time Weighted Sum Formula:

    Time Weighted Sum = (Value * Weight) * Duration

  • Time Weighted Average Formula:

    Time Weighted Average = Time Weighted Sum / Total Duration

Implementing these changes ensures that the provided weight is properly applied in all calculations, thereby maintaining accurate time weighted averages that reflect the intended significance of the underlying values.

Updates

Lead Judging Commences

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

TimeWeightedAverage library fails to consistently apply weights in calculations, causing incorrect time-weighted averages

Support

FAQs

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