Core Contracts

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

Incorrect Period Initialization in TimeWeightedAverage Library Causes Denial-of-Service in Boost Calculations

Summary

The TimeWeightedAverage library is designed to manage time-weighted averages through the creation and updating of periods that track a value’s evolution over time. In this system, each period captures key metrics—including its start time, end time, elapsed duration, and weighted sum to support accurate reward and governance calculations across several protocol components (e.g., veRAACToken, BoostCalculator, dual-gauge contracts).

A critical bug exists in the createPeriod function: upon period creation, the library initializes self.totalDuration with the full period duration (for example, the boost window length, typically 7 days) instead of starting it at 0. The intended design is for totalDuration to be incrementally updated in the updateValue function based solely on the actual elapsed time between updates. However, because createPeriod pre-populates totalDuration with the entire duration, subsequent updates erroneously extend the effective period length. This bug interferes with the period expiration logic, preventing new periods from being created when the boost window should expire. In practice, this results in a Denial-of-Service (DoS) scenario for boost updates, as further changes (such as additional increases in locked tokens) fail once the cumulative duration exceeds the intended boost window.

Vulnerability Details

TimeWeightedAverage Library – Period Structure

The library uses the following Period struct to capture time-related metrics:

struct Period {
uint256 startTime; // Beginning timestamp of the period
uint256 endTime; // End timestamp of the period
uint256 lastUpdateTime; // Last timestamp the value was updated
uint256 value; // Current value being tracked
uint256 weightedSum; // Running sum of time-weighted values
uint256 totalDuration; // Total duration of accumulated values
uint256 weight; // Weight applied to period (scaled by 1e18)
}

The Bug in createPeriod

The createPeriod function is intended to initialize a new period once the current boost window expires. It uses a constraint to prevent overlapping periods within a single boost window:

if (self.startTime != 0 && startTime < self.startTime + self.totalDuration) {
revert PeriodNotElapsed();
}

However, during period creation, the function incorrectly sets:

self.totalDuration = duration;

This line mistakenly initializes the totalDuration with the entire provided duration (e.g., a 7-day boost window) rather than starting it at zero. The correct approach would be to initialize totalDuration to 0 and allow the updateValue function to accumulate elapsed time.

The updateValue Function

The updateValue function updates the period’s state as new values are recorded:

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;
}

Each update adds the elapsed time (duration) to totalDuration. Because totalDuration was initialized to the boost window duration instead of zero, every call to updateValue erroneously extends the period’s effective duration.

Integration with BoostCalculator

The BoostCalculator’s updateBoostPeriod function relies on the TimeWeightedAverage’s period to determine whether a new period should be created:

function updateBoostPeriod(BoostState storage state) internal {
if (state.boostWindow == 0) revert InvalidBoostWindow();
if (state.maxBoost < state.minBoost) revert InvalidBoostBounds();
uint256 currentTime = block.timestamp;
uint256 periodStart = state.boostPeriod.startTime;
// 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;
}
// If no period exists, create initial period starting from current block
TimeWeightedAverage.createPeriod(
state.boostPeriod, currentTime, state.boostWindow, state.votingPower, state.maxBoost
);
}

Due to the bug in createPeriod, the condition for creating a new period never triggers at the intended time. For instance, if a boost window is 7 days and the period’s totalDuration is incorrectly initialized to 7 days, then each subsequent update adds additional time (e.g., 2 days per update). After several updates, the effective expiration of the period is pushed far beyond the intended 7 days (e.g., 13 days), thereby preventing the creation of a new period when required.

Proof of Concept

Scenario Walkthrough

  1. Initial Lock and Period Creation:

    • Alice locks her RAAC tokens in the veRAACToken contract.

    • The lock function is called, which, among other things, triggers a boost state update via:

      _updateBoostState(msg.sender, amount);
    • This update eventually calls BoostCalculator.updateBoostPeriod, which in turn calls TimeWeightedAverage.createPeriod.

    • Bug: Instead of initializing totalDuration to 0, it is set to the boost window (7 days).

  2. Subsequent Updates:

    • Two days later, Alice calls the increase function to add more tokens, which again calls _updateBoostState.

    • This time, BoostCalculator.updateBoostPeriod calls updateValue on the existing period.

    • Effect: updateValue calculates the elapsed time (2 days) and adds it to totalDuration.

    • The period’s totalDuration becomes 7 days + 2 days = 9 days.

  3. Further Updates:

    • Alice repeats the increase after every 2 days. After three such updates (at 2, 4, and 6 days later), the cumulative additional time equals 6 days.

    • Consequently, totalDuration becomes 7 days (initial) + 6 days = 13 days.

  4. Denial of Service:

    • When the boost window is supposed to expire (after 7 days), alice calls the increase function again to add more tokens, which again calls _updateBoostState.

    • This time, BoostCalculator.updateBoostPeriod calls createPeriod because the system checks:

      if (currentTime >= periodStart + state.boostWindow)
    • In the createPeriod, the system checks:

    if (self.startTime != 0 && startTime < self.startTime + self.totalDuration)
    • However, because periodStart + totalDuration is now 13 days, the condition fails, preventing new period creation.

    • Result: Further boost updates are blocked, causing a DoS in boost state updates across the protocol.

Code PoC (to above scenario)

To demonstrate this vulnerability, the following Proof of Concept (PoC) is provided. The PoC is written using the Foundry tool.

  1. Step 1: Create a Foundry project and place all the contracts in the src directory.

  2. Step 2: Create a test directory and a mocks folder within the src directory (or use an existing mocks folder).

  3. Step 3: Create all necessary mock contracts, if required.

  4. Step 4: Create a test file (with any name) in the test directory.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {veRAACToken} from "../src/core/tokens/veRAACToken.sol";
import {RAACToken} from "../src/core/tokens/RAACToken.sol";
import {TimeWeightedAverage} from "../src/libraries/math/TimeWeightedAverage.sol";
import {LockManager} from "../src/libraries/governance/LockManager.sol";
import {IveRAACToken} from "../src/interfaces/core/tokens/IveRAACToken.sol";
contract VeRAACTokenTest is Test {
veRAACToken veRaacToken;
RAACToken raacToken;
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address RAAC_MINTER = makeAddr("RAAC_MINTER");
uint256 initialRaacSwapTaxRateInBps = 200; // 2%, 10000 - 100%
uint256 initialRaacBurnTaxRateInBps = 150; // 1.5%, 10000 - 100%
address VE_RAAC_OWNER = makeAddr("VE_RAAC_OWNER");
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
function setUp() public {
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.startPrank(VE_RAAC_OWNER);
veRaacToken = new veRAACToken(address(raacToken));
vm.stopPrank();
}
}
  1. Step 5: Add the following test PoC in the test file, after the setUp function.

function testNoMoreTimeWeightedAveragePeriodsCanBeCreatedAterFirstPeriodCreation() public {
uint256 TOTAL_LOCK_AMOUNT = 10_000_000e18;
uint256 LOCK_AMOUNT = 1_000_000e18;
uint256 LOCK_DURATION = 1460 days;
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(RAAC_MINTER);
vm.stopPrank();
vm.startPrank(RAAC_MINTER);
raacToken.mint(ALICE, TOTAL_LOCK_AMOUNT);
vm.stopPrank();
vm.startPrank(ALICE);
raacToken.approve(address(veRaacToken), TOTAL_LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT, LOCK_DURATION);
vm.stopPrank();
vm.warp(block.timestamp + 2 days);
vm.startPrank(ALICE);
veRaacToken.increase(LOCK_AMOUNT);
vm.stopPrank();
vm.warp(block.timestamp + 2 days);
vm.startPrank(ALICE);
veRaacToken.increase(LOCK_AMOUNT);
vm.stopPrank();
vm.warp(block.timestamp + 2 days);
vm.startPrank(ALICE);
veRaacToken.increase(LOCK_AMOUNT);
vm.stopPrank();
// at this point, the 7 days boost window will get elapsed
vm.warp(block.timestamp + 2 days);
// now internally a new period should be created because current boost window has expired.
// but due to the bug below call will get revert
vm.startPrank(ALICE);
vm.expectRevert(bytes4(keccak256("PeriodNotElapsed()")));
veRaacToken.increase(LOCK_AMOUNT);
vm.stopPrank();
// the above call has reverted because
// the accumulated totalDuration member value in the boost period
// is updated to 13 days how?
// initially the totalDuration was set to the duration and the duration
// is 7 days boost winow, so total accumulation would be 7 + 6 = 13
// let's prove the point
vm.warp(block.timestamp + 5 days - 1);
vm.startPrank(ALICE);
vm.expectRevert(bytes4(keccak256("PeriodNotElapsed()")));
veRaacToken.increase(LOCK_AMOUNT);
vm.stopPrank();
// now let's increase the locked amount by warping buggy accumulated totalDuration
vm.warp(block.timestamp + 5 days);
// below call will successfully pass.
vm.startPrank(ALICE);
veRaacToken.increase(LOCK_AMOUNT);
vm.stopPrank();
}
  1. Step 6: To run the test, execute the following commands in your terminal

forge test --mt testNoMoreTimeWeightedAveragePeriodsCanBeCreatedAterFirstPeriodCreation -vv
  1. Step 7: Review the output.

[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/VeRAACTokenTest.t.sol:VeRAACTokenTest
[PASS] testNoMoreTimeWeightedAveragePeriodsCanBeCreatedAterFirstPeriodCreation() (gas: 919301)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.01ms (1.70ms CPU time)
Ran 1 test suite in 14.48ms (7.01ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As demonstrated, the test confirms that users will face a Denail of Service.

Impact

  • Denial-of-Service (DoS):
    The failure to create new periods on time effectively locks the boost state. Subsequent operations that rely on timely period resets (such as voting power recalculations and reward distributions) will fail, leading to a system-wide DoS.

  • Inaccurate Reward Distribution:
    Boost-related rewards are calculated based on these time-weighted averages. An extended period causes miscalculations in boost factors, leading to incorrect reward allocations for users.

  • Governance Manipulation:
    In protocols where governance weight is tied to boost calculations, a DoS or incorrect boost state can result in skewed voting power, thereby undermining the fairness of governance decisions.

  • Systemic Risk:
    Since the TimeWeightedAverage library is used across multiple contracts (e.g., veRAACToken, BoostCalculator, dual-gauge contracts, governance contracts), the vulnerability has far-reaching implications that may affect the entire protocol’s integrity and trustworthiness.

Tools Used

  • Manual Review

  • Foundry

Recommendations

The following diff illustrates the recommended changes to correct the period initialization logic in the createPeriod function:

function createPeriod(
Period storage self,
uint256 startTime,
uint256 duration,
uint256 initialValue,
uint256 weight
) internal {
if (self.startTime != 0 && startTime < self.startTime + self.totalDuration) {
revert PeriodNotElapsed();
}
if (duration == 0) revert ZeroDuration();
if (weight == 0) revert ZeroWeight();
self.startTime = startTime;
self.endTime = startTime + duration;
self.lastUpdateTime = startTime;
self.value = initialValue;
self.weightedSum = 0;
- self.totalDuration = duration;
+ self.totalDuration = 0;
self.weight = weight;
emit PeriodCreated(startTime, duration, initialValue);
}
Updates

Lead Judging Commences

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

veRAACToken can be DOSed when totalDuration exceeds boostWindow in TimeWeightedAverage, preventing new users from locking tokens until extended duration elapses

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

veRAACToken can be DOSed when totalDuration exceeds boostWindow in TimeWeightedAverage, preventing new users from locking tokens until extended duration elapses

Support

FAQs

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