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.
The library uses the following Period
struct to capture time-related metrics:
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:
However, during period creation, the function incorrectly sets:
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.
updateValue
FunctionThe updateValue
function updates the period’s state as new values are recorded:
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.
The BoostCalculator’s updateBoostPeriod
function relies on the TimeWeightedAverage’s period to determine whether a new period should be created:
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.
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:
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).
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.
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.
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:
In the createPeriod
, the system checks:
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.
To demonstrate this vulnerability, the following Proof of Concept (PoC) is provided. The PoC is written using the Foundry tool.
Step 1: Create a Foundry project and place all the contracts in the src
directory.
Step 2: Create a test
directory and a mocks
folder within the src
directory (or use an existing mocks folder).
Step 3: Create all necessary mock contracts, if required.
Step 4: Create a test file (with any name) in the test
directory.
Step 5: Add the following test PoC in the test file, after the setUp
function.
Step 6: To run the test, execute the following commands in your terminal
Step 7: Review the output.
As demonstrated, the test confirms that users will face a Denail of Service.
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.
Manual Review
Foundry
The following diff illustrates the recommended changes to correct the period initialization logic in the createPeriod
function:
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.