pragma solidity 0.8.26;
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 NonClaimerSharesNotActuallySweepableTest is BaseConfidencePoolTest {
uint256 internal constant TEN_YEARS = 3650 days;
function testPoC_survivedNonClaimerPrincipalAndBonusRemainReservedForever() external {
_stake(alice, 100 * ONE);
_stake(bob, 100 * ONE);
_contributeBonus(carol, 100 * ONE);
_passThroughUnderAttack();
attackRegistry.setAgreementState(IAttackRegistry.ContractState.PRODUCTION);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
vm.prank(alice);
pool.claimSurvived();
assertEq(pool.totalEligibleStake(), 100 * ONE, "Bob principal remains tracked");
assertEq(pool.claimedBonus(), 50 * ONE, "Alice claimed only her half of the bonus");
assertEq(pool.snapshotTotalBonus() - pool.claimedBonus(), 50 * ONE, "Bob bonus remains unclaimed");
vm.warp(block.timestamp + TEN_YEARS);
vm.expectRevert(IConfidencePool.NothingToSweep.selector);
pool.sweepUnclaimedBonus();
assertEq(pool.totalEligibleStake(), 100 * ONE, "Bob principal is still reserved after 10 years");
assertEq(pool.snapshotTotalBonus() - pool.claimedBonus(), 50 * ONE, "Bob bonus is still reserved");
assertEq(token.balanceOf(address(pool)), 150 * ONE, "Bob principal plus bonus remain locked in pool");
}
}
+ uint256 public constant SURVIVED_EXPIRED_CLAIM_WINDOW = 180 days;
+ uint256 public survivedExpiredClaimDeadline;
function flagOutcome(PoolStates.Outcome newOutcome, bool goodFaith_, address attacker_) external onlyModerator {
...
outcome = newOutcome;
+ if (newOutcome == PoolStates.Outcome.SURVIVED) {
+ survivedExpiredClaimDeadline = block.timestamp + SURVIVED_EXPIRED_CLAIM_WINDOW;
+ }
...
}
function claimExpired() external nonReentrant {
...
if (outcome == PoolStates.Outcome.UNRESOLVED) {
...
if (state == IAttackRegistry.ContractState.PRODUCTION) {
outcome = PoolStates.Outcome.SURVIVED;
+ survivedExpiredClaimDeadline = block.timestamp + SURVIVED_EXPIRED_CLAIM_WINDOW;
} else {
outcome = PoolStates.Outcome.EXPIRED;
+ survivedExpiredClaimDeadline = block.timestamp + SURVIVED_EXPIRED_CLAIM_WINDOW;
}
}
+ if (block.timestamp > survivedExpiredClaimDeadline) revert ClaimWindowExpired();
...
}
function claimSurvived() external nonReentrant {
+ if (block.timestamp > survivedExpiredClaimDeadline) revert ClaimWindowExpired();
...
}
function sweepUnclaimedBonus() external nonReentrant {
...
+ if (survivedExpiredClaimDeadline != 0 && block.timestamp > survivedExpiredClaimDeadline) {
+ uint256 amount = stakeToken.balanceOf(address(this));
+ if (amount == 0) revert NothingToSweep();
+
+ totalEligibleStake = 0;
+ totalBonus = 0;
+
+ stakeToken.safeTransfer(recoveryAddress, amount);
+ emit BonusSwept(msg.sender, recoveryAddress, amount);
+ return;
+ }
...
}