Vulnerability Details
In the calculateSegmentTimestamps function, each segment's timestamp is calculated based on the previous segment's timestamp plus the current segment's duration. This approach can lead to cumulative errors over multiple segments if there are small inaccuracies or rounding errors in the duration values.
function calculateSegmentTimestamps(LockupDynamic.SegmentWithDuration[] memory segments)
internal
view
returns (LockupDynamic.Segment[] memory segmentsWithTimestamps)
{
uint256 segmentCount = segments.length;
segmentsWithTimestamps = new LockupDynamic.Segment[](segmentCount);
uint40 startTime = uint40(block.timestamp);
unchecked {
segmentsWithTimestamps[0] = LockupDynamic.Segment({
amount: segments[0].amount,
exponent: segments[0].exponent,
timestamp: startTime + segments[0].duration
});
for (uint256 i = 1; i < segmentCount; ++i) {
segmentsWithTimestamps[i] = LockupDynamic.Segment({
amount: segments[i].amount,
exponent: segments[i].exponent,
timestamp: segmentsWithTimestamps[i - 1].timestamp + segments[i].duration
});
}
}
}
Impact
Precision Loss: Small inaccuracies in duration values can accumulate over many segments, leading to a drift between expected and actual segment timestamps.
Unexpected Behavior: This drift can result in incorrect segment timing, potentially causing misalignment with the intended schedule for dynamic lockups.
Financial Implications: For applications where precise timing is crucial (e.g., financial contracts or streaming payments), this drift could lead to unexpected financial outcomes.
Proof of Concept
Assume each segment duration has a small rounding error of 1 second.
For the first segment, the error is 1 second.
For the second segment, the cumulative error is 2 seconds.
For the third segment, the cumulative error is 3 seconds.
Over 100 segments, the cumulative error would be 100 seconds.
Example Calculation
uint40 startTime = uint40(block.timestamp);
uint40 firstTimestamp = startTime + segments[0].duration;
uint40 secondTimestamp = firstTimestamp + segments[1].duration;
uint40 thirdTimestamp = secondTimestamp + segments[2].duration;
uint40 hundredthTimestamp = segmentsWithTimestamps[98].timestamp + segments[99].duration;
Tools Used
Manual Review
Recommendations
Consider the timestamps should be calculated based on the cumulative duration from the start time rather than incrementally adding each duration to the previous timestamp. This approach reduces the potential for accumulated errors.
Proposed solution
function calculateSegmentTimestamps(LockupDynamic.SegmentWithDuration[] memory segments)
internal
view
returns (LockupDynamic.Segment[] memory segmentsWithTimestamps)
{
uint256 segmentCount = segments.length;
segmentsWithTimestamps = new LockupDynamic.Segment[](segmentCount);
uint40 startTime = uint40(block.timestamp);
unchecked {
uint40 cumulativeDuration = 0;
for (uint256 i = 0; i < segmentCount; ++i) {
if (segments[i].duration < 0) {
revert Errors.SablierV2LockupDynamic_InvalidSegmentDuration(i, segments[i].duration);
}
cumulativeDuration += segments[i].duration;
segmentsWithTimestamps[i] = LockupDynamic.Segment({
amount: segments[i].amount,
exponent: segments[i].exponent,
timestamp: startTime + cumulativeDuration
});
}
}
}
By calculating each segment's timestamp based on the start time and cumulative duration, the risk of drift can be mitigated