Summary
The createPeriod
function in the TimeWeightedAverage
library includes a validation check to ensure that a new period cannot be created before the previous period has elapsed. However, this check relies on self.startTime + self.totalDuration
, where totalDuration
is incremented in the updateValue
function. This design introduces a vulnerability where totalDuration
can be extended beyond the intended endTime
of the period, causing the validation check to fail even when the current time exceeds the endTime
.
Vulnerability Details
createPeriod
Validation Check:
if (self.startTime != 0 && startTime < self.startTime + self.totalDuration) {
revert PeriodNotElapsed();
}
This check ensures that a new period cannot start before the previous period has elapsed. It compares the startTime
of the new period with self.startTime + self.totalDuration
.
updateValue
Duration Extension:
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;
}
}
The totalDuration
is incremented by the elapsed time (duration
) since the last update. This means totalDuration
can grow beyond the intended endTime
of the period. If updateValue
is called with a timestamp
beyond the endTime
, totalDuration
will be extended. As a result, self.startTime + self.totalDuration
will exceed the endTime
, causing the validation check in createPeriod
to fail even when the current time is beyond the endTime
.
POC
add the following test case into TimeWeightedAverage.test.js:
it("createPeriod will revert even after the end time", async () => {
const currentTime = BigInt(await time.latest());
const nextPeriod = ((currentTime / WEEK) + 1n) * WEEK;
await time.setNextBlockTimestamp(Number(currentTime));
await timeWeightedAverage.createPeriod(
nextPeriod,
WEEK,
ethers.parseEther("100"),
ethers.parseEther("1")
);
const updateTime = nextPeriod + WEEK;
await time.setNextBlockTimestamp(Number(updateTime));
await timeWeightedAverage.updateValue(
ethers.parseEther("200"),
updateTime
);
const timeAfterEndtime = nextPeriod + WEEK + DAY;
const period = await timeWeightedAverage.period();
const endTime = period.endTime;
console.log("timeAfterEndtime > endTime:",timeAfterEndtime>endTime);
await time.setNextBlockTimestamp(Number(timeAfterEndtime));
await timeWeightedAverage.createPeriod(
timeAfterEndtime,
WEEK,
ethers.parseEther("100"),
ethers.parseEther("1")
);
});
run npx hardhat test --grep "createPeriod will revert"
imeWeightedAverage
Basic Functionality
timeAfterEndtime > endTime: true
1) createPeriod will revert even after the end time
Error: VM Exception while processing transaction: reverted with custom error 'PeriodNotElapsed()'
at TimeWeightedAverageMock.createPeriod (contracts/libraries/math/TimeWeightedAverage.sol:110)
at TimeWeightedAverageMock.createPeriod (contracts/mocks/libraries/math/TimeWeightedAverageMock.sol:17)
Impact
The impact is Low because the updateValue is only used in test code. The likelihood is High, so the severity is Low.
Tools Used
Manual Review
Recommendations
Consider following fix in createPeriod:
function createPeriod(
Period storage self,
uint256 startTime,
uint256 duration,
uint256 initialValue,
uint256 weight
) internal {
if (self.startTime != 0 && startTime < self.endTime) {
revert PeriodNotElapsed();
}