FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: high
Likelihood: low

Unsafe downcasting to uint32 in corruptedClaimDeadline calculation can truncate the deadline and permanently lock attacker bounties

Author Revealed upon completion

[L-XX] Unsafe Explicit Downcast in corruptedClaimDeadline Calculation Causes Silent Integer Truncation and Lockup of Bounty Rewards

Root + Impact

Description
The protocol calculates a deterministic 180-day claim window (CORRUPTED_CLAIM_WINDOW) starting from the time a good-faith corrupted status is flagged, giving designated attackers an active period to claim their bounty rewards.
An unsafe explicit downcast from uint256 to uint32 is performed when calculating corruptedClaimDeadline. If the computed timestamp sum exceeds the maximum storage capacity of a uint32 variable, the value silently truncates and wraps around to an early historical timestamp instead of throwing an overflow panic.

if (willBeGoodFaithCorrupted) {
if (_firstGoodFaithCorruptedAt == 0) {
// forge-lint: disable-next-line(unsafe-typecast)
_firstGoodFaithCorruptedAt = uint32(block.timestamp);
}
// forge-lint: disable-next-line(unsafe-typecast)
@> corruptedClaimDeadline = uint32(_firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW);
} else {
corruptedClaimDeadline = 0;
}

Normal Behavior

The protocol is designed to calculate a deterministic 180-day grace period starting from the timestamp a good-faith status is flagged, ensuring that designated bounty claimants have an active window to withdraw their rewards. Under normal conditions, the calculated deadline timestamp should cleanly store in the state and accurately validate future claims against the current block time.

Specific Issue / Problem

An explicit downcast from uint256 to uint32 is performed when calculating corruptedClaimDeadline without validating the maximum size limits of the destination type. If the mathematical sum of the initial timestamp and the grace period exceeds type(uint32).max, the value silently truncates and wraps around to an early historical integer, causing the claim window to instantly expire and permanently locking the associated bounty rewards.

Risk

Likelihood: Low

  • Reason 1: Deployment and operation of the protocol late in the Unix uint32 timeline (approaching February 2106) forces the mathematical sum of the timestamps to exceed 4,294,967,295.

  • Reason 2: Explicit downcasting from uint256 to uint32 deliberately bypasses standard Solidity 0.8.x overflow panic checks, allowing truncation to occur silently without triggering an automatic transaction revert.
    Impact: High

  • Impact 1: Complete denial of service for legitimate claimants attempting to call claimAttackerBounty due to immediate truncation-driven expiration checks.

  • Impact 2: Irrecoverable locking of designated bounty rewards within the pool instance state, violating the structural economic distribution rules of the protocol.

Proof of Concept

The following minimal test illustrates the mathematical truncation flow showing how the corruptedClaimDeadline calculated variable wraps around to the past when operating near the boundary constraints:

function test_corruptedClaimDeadline_overflow_proof() public {
// 1. Set up target bounds using native types
uint32 uint32Max = type(uint32).max;
uint256 CORRUPTED_CLAIM_WINDOW = 180 days; // 15,552,000 seconds
// 2. Simulate a timestamp flagged right before the uint32 boundary limit
uint32 _firstGoodFaithCorruptedAt = uint32(uint256(uint32Max) - (CORRUPTED_CLAIM_WINDOW / 2));
// 3. This mirrors the exact root cause calculation inside the source contract
uint32 corruptedClaimDeadline = uint32(_firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW);
// 4. Prove that the deadline mathematically truncates into the past
// Instead of being in the future, it wraps around to a very small number
console2.log("Flagged Timestamp:", _firstGoodFaithCorruptedAt);
console2.log("Truncated Deadline:", corruptedClaimDeadline);
assertTrue(corruptedClaimDeadline < _firstGoodFaithCorruptedAt);
}

Recommended Mitigation

Validate the upper bounds of the summation parameters using standard uint256 storage precision types prior to implementing the typecast sequence:

if (willBeGoodFaithCorrupted) {
if (_firstGoodFaithCorruptedAt == 0) {
_firstGoodFaithCorruptedAt = uint32(block.timestamp);
}
- // forge-lint: disable-next-line(unsafe-typecast)
- corruptedClaimDeadline = uint32(_firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW);
+ uint256 potentialCorruptedClaimDeadline = uint256(_firstGoodFaithCorruptedAt) + CORRUPTED_CLAIM_WINDOW;
+ if (potentialCorruptedClaimDeadline > type(uint32).max) {
+ revert("Expiry too far");
+ }
+ corruptedClaimDeadline = uint32(potentialCorruptedClaimDeadline);
} else {
corruptedClaimDeadline = 0;
}

Support

FAQs

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

Give us feedback!