if (willBeGoodFaithCorrupted) {
if (_firstGoodFaithCorruptedAt == 0) {
_firstGoodFaithCorruptedAt = uint32(block.timestamp);
}
corruptedClaimDeadline =
uint32(
_firstGoodFaithCorruptedAt
+ CORRUPTED_CLAIM_WINDOW
);
}
The wrapped value is trusted by both beneficiary paths:
function claimAttackerBounty() external nonReentrant {
if (block.timestamp > corruptedClaimDeadline) {
revert ClaimWindowExpired();
}
}
function sweepUnclaimedCorrupted() external nonReentrant {
if (block.timestamp <= corruptedClaimDeadline) {
revert ClaimWindowNotExpired();
}
uint256 amount = stakeToken.balanceOf(address(this));
stakeToken.safeTransfer(recoveryAddress, amount);
}
pragma solidity 0.8.26;
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {ConfidencePoolFactory} from "src/ConfidencePoolFactory.sol";
import {IConfidencePool} from "src/interfaces/IConfidencePool.sol";
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
contract GoodFaithClaimDeadlineTruncationImmediatelyRedirectsTheAttackersFullBountyToRecoveryPoC is
BaseConfidencePoolTest
{
uint256 internal constant CLAIM_WINDOW = 180 days;
function test_PoC_ExactFirstWrapRedirectsFullBountyToRecovery() external {
uint256 lastSafeTimestamp = uint256(type(uint32).max) - CLAIM_WINDOW;
uint256 firstWrappingTimestamp = lastSafeTimestamp + 1;
ConfidencePool safePool = _createFactoryPoolAt(lastSafeTimestamp);
_fundAndFlagGoodFaith(safePool, attacker);
assertEq(safePool.corruptedClaimDeadline(), type(uint32).max);
vm.expectRevert(IConfidencePool.ClaimWindowNotExpired.selector);
safePool.sweepUnclaimedCorrupted();
vm.prank(attacker);
safePool.claimAttackerBounty();
assertEq(token.balanceOf(attacker), 100 ether);
address wrappedAttacker = makeAddr("wrappedAttacker");
ConfidencePool wrappedPool = _createFactoryPoolAt(firstWrappingTimestamp);
_fundAndFlagGoodFaith(wrappedPool, wrappedAttacker);
assertEq(wrappedPool.corruptedClaimDeadline(), 0);
vm.prank(wrappedAttacker);
vm.expectRevert(IConfidencePool.ClaimWindowExpired.selector);
wrappedPool.claimAttackerBounty();
vm.expectRevert(IConfidencePool.MustClaimBountyFirst.selector);
wrappedPool.claimCorrupted();
uint256 recoveryBefore = token.balanceOf(recovery);
wrappedPool.sweepUnclaimedCorrupted();
assertEq(token.balanceOf(recovery) - recoveryBefore, 100 ether);
}
function _createFactoryPoolAt(uint256 timestamp) internal returns (ConfidencePool created) {
vm.warp(timestamp);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.NEW_DEPLOYMENT);
ConfidencePool implementation = new ConfidencePool();
ConfidencePoolFactory factoryImplementation = new ConfidencePoolFactory();
ERC1967Proxy proxy = new ERC1967Proxy(
address(factoryImplementation),
abi.encodeCall(
ConfidencePoolFactory.initialize,
(address(safeHarborRegistry), address(implementation), moderator)
)
);
ConfidencePoolFactory factory = ConfidencePoolFactory(address(proxy));
factory.setStakeTokenAllowed(address(token), true);
address poolAddress = factory.createPool(
agreement, address(token), timestamp + 31 days, ONE, recovery, _defaultScope()
);
created = ConfidencePool(poolAddress);
}
function _fundAndFlagGoodFaith(ConfidencePool target, address namedAttacker) internal {
token.mint(alice, 100 ether);
vm.startPrank(alice);
token.approve(address(target), 100 ether);
target.stake(100 ether);
vm.stopPrank();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.UNDER_ATTACK);
target.pokeRiskWindow();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
vm.prank(moderator);
target.flagOutcome(PoolStates.Outcome.CORRUPTED, true, namedAttacker);
}
}
Use a wider type for the first-good-faith timestamp and derived deadline. The public interface should be updated to match the wider deadline type.
- uint32 private _firstGoodFaithCorruptedAt;
- uint32 public override corruptedClaimDeadline;
+ uint64 private _firstGoodFaithCorruptedAt;
+ uint64 public override corruptedClaimDeadline;
if (willBeGoodFaithCorrupted) {
if (_firstGoodFaithCorruptedAt == 0) {
- _firstGoodFaithCorruptedAt = uint32(block.timestamp);
+ _firstGoodFaithCorruptedAt = uint64(block.timestamp);
}
- corruptedClaimDeadline =
- uint32(
- _firstGoodFaithCorruptedAt
- + CORRUPTED_CLAIM_WINDOW
- );
+ corruptedClaimDeadline =
+ _firstGoodFaithCorruptedAt
+ + uint64(CORRUPTED_CLAIM_WINDOW);
}
As a smaller but more restrictive alternative, retain uint32 and reject any derived deadline exceeding type(uint32).max before performing the cast.