uint256 public constant override CORRUPTED_CLAIM_WINDOW = 180 days;
uint32 internal _firstGoodFaithCorruptedAt;
uint32 public override corruptedClaimDeadline;
function flagOutcome(PoolStates.Outcome newOutcome, bool goodFaith_, address attacker_) external onlyModerator {
...
if (willBeGoodFaithCorrupted) {
if (_firstGoodFaithCorruptedAt == 0) {
_firstGoodFaithCorruptedAt = uint32(block.timestamp);
}
corruptedClaimDeadline = uint32(_firstGoodFaithCorruptedAt + CORRUPTED_CLAIM_WINDOW);
} else {
corruptedClaimDeadline = 0;
}
}
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 {Test} from "forge-std/Test.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {IConfidencePool} from "src/interfaces/IConfidencePool.sol";
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {MockAgreement} from "test/mocks/MockAgreement.sol";
import {MockAttackRegistry} from "test/mocks/MockAttackRegistry.sol";
import {MockERC20} from "test/mocks/MockERC20.sol";
import {MockSafeHarborRegistry} from "test/mocks/MockSafeHarborRegistry.sol";
contract ConfidencePoolCorruptedDeadlineBoundaryPoC is Test {
uint256 internal constant ONE = 1e18;
uint256 internal constant ATTACKER_STAKE_ENTITLEMENT = 100 * ONE;
uint256 internal constant BONUS_ENTITLEMENT = 50 * ONE;
uint256 internal constant TOTAL_BOUNTY_ENTITLEMENT = ATTACKER_STAKE_ENTITLEMENT + BONUS_ENTITLEMENT;
uint256 internal constant FIRST_WRAPPING_FLAG_TIME = uint256(type(uint32).max) - 180 days + 1;
address internal constant DEFAULT_SCOPE_ACCOUNT = address(0xC0FFEE);
address internal moderator = makeAddr("moderator");
address internal recovery = makeAddr("recovery");
address internal attacker = makeAddr("attacker");
address internal staker = makeAddr("staker");
address internal bonusContributor = makeAddr("bonusContributor");
MockERC20 internal token;
MockAttackRegistry internal attackRegistry;
MockSafeHarborRegistry internal safeHarborRegistry;
MockAgreement internal agreement;
ConfidencePool internal pool;
function setUp() external {
token = new MockERC20();
attackRegistry = new MockAttackRegistry();
safeHarborRegistry = new MockSafeHarborRegistry();
agreement = new MockAgreement(address(this));
agreement.setContractInScope(DEFAULT_SCOPE_ACCOUNT, true);
safeHarborRegistry.setAttackRegistry(address(attackRegistry));
safeHarborRegistry.setAgreementValid(address(agreement), true);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.NEW_DEPLOYMENT);
}
function testGoodFaithCorruptedDeadlineWrapsNearUint32CeilingAndRedirectsBounty() external {
uint256 flagTime = FIRST_WRAPPING_FLAG_TIME;
vm.warp(flagTime - 1 days);
_deployPool();
_stake(staker, ATTACKER_STAKE_ENTITLEMENT);
_contributeBonus(bonusContributor, BONUS_ENTITLEMENT);
vm.warp(flagTime);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.UNDER_ATTACK);
pool.pokeRiskWindow();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.CORRUPTED);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, true, attacker);
assertEq(pool.bountyEntitlement(), TOTAL_BOUNTY_ENTITLEMENT, "attacker should receive whole pool");
assertLt(pool.corruptedClaimDeadline(), block.timestamp, "deadline wrapped into the past");
vm.prank(attacker);
vm.expectRevert(IConfidencePool.ClaimWindowExpired.selector);
pool.claimAttackerBounty();
uint256 recoveryBalanceBefore = token.balanceOf(recovery);
pool.sweepUnclaimedCorrupted();
assertEq(
token.balanceOf(recovery) - recoveryBalanceBefore,
TOTAL_BOUNTY_ENTITLEMENT,
"sweep redirects full bounty to recovery"
);
assertEq(token.balanceOf(attacker), 0, "named attacker receives nothing");
assertEq(pool.bountyClaimed(), TOTAL_BOUNTY_ENTITLEMENT, "bounty is marked claimed after sweep");
}
function _deployPool() internal {
ConfidencePool implementation = new ConfidencePool();
pool = ConfidencePool(Clones.clone(address(implementation)));
pool.initialize(
address(agreement),
address(token),
address(safeHarborRegistry),
moderator,
block.timestamp + 31 days,
ONE,
recovery,
address(this),
_defaultScope()
);
}
function _stake(address user, uint256 amount) internal {
token.mint(user, amount);
vm.startPrank(user);
token.approve(address(pool), amount);
pool.stake(amount);
vm.stopPrank();
}
function _contributeBonus(address user, uint256 amount) internal {
token.mint(user, amount);
vm.startPrank(user);
token.approve(address(pool), amount);
pool.contributeBonus(amount);
vm.stopPrank();
}
function _defaultScope() internal pure returns (address[] memory accounts) {
accounts = new address[](1);
accounts[0] = DEFAULT_SCOPE_ACCOUNT;
}
}