The fork PoC deploys the pool system locally into a pinned BattleChain testnet fork and uses the live testnet SafeHarborRegistry, demo agreement, agreement owner, and in-scope account at block 16000. It mocks registry lifecycle reads at the live AttackRegistry address to avoid live state mutation. The pinned demo agreement has only one live in-scope account, so the replacement account's isContractInScope() response is mocked only for the replacement-scope branch.
pragma solidity 0.8.26;
import {Test} from "forge-std/Test.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {IAgreement} from "@battlechain/interface/IAgreement.sol";
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {IConfidencePool} from "src/interfaces/IConfidencePool.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {ConfidencePoolFactory} from "src/ConfidencePoolFactory.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {MockERC20} from "test/mocks/MockERC20.sol";
contract ConfidencePoolPokeScopeLockForkPoCTest 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;
address internal constant REPLACEMENT_SCOPE_ACCOUNT = address(0xBEEF);
uint256 internal constant PIN_BLOCK = 16000;
uint256 internal constant ONE = 1e18;
ConfidencePoolFactory internal factory;
ConfidencePool internal pool;
MockERC20 internal stakeToken;
address internal moderator = makeAddr("moderator");
address internal recovery = makeAddr("recovery");
address internal alice = makeAddr("alice");
address internal dave = makeAddr("dave");
function setUp() public {
try vm.envString("BATTLECHAIN_TESTNET_RPC") returns (string memory) {
vm.createSelectFork("battlechain_testnet", PIN_BLOCK);
} catch {
vm.skip(true);
return;
}
ConfidencePool poolImpl = new ConfidencePool();
ConfidencePoolFactory factoryImpl = new ConfidencePoolFactory();
ERC1967Proxy proxy = new ERC1967Proxy(
address(factoryImpl),
abi.encodeCall(ConfidencePoolFactory.initialize, (SAFE_HARBOR_REGISTRY, address(poolImpl), moderator))
);
factory = ConfidencePoolFactory(address(proxy));
stakeToken = new MockERC20();
factory.setStakeTokenAllowed(address(stakeToken), true);
_mockAgreementState(IAttackRegistry.ContractState.NEW_DEPLOYMENT);
address[] memory accounts = new address[](1);
accounts[0] = DEMO_AGREEMENT_IN_SCOPE;
vm.prank(DEMO_AGREEMENT_OWNER);
pool = ConfidencePool(
factory.createPool(DEMO_AGREEMENT, address(stakeToken), block.timestamp + 31 days, ONE, recovery, accounts)
);
}
function testForkPoC_pokeDuringAttackRequestedDoesNotPersistScopeLock() external {
_stake(alice, 100 * ONE);
assertTrue(pool.isAccountInScope(DEMO_AGREEMENT_IN_SCOPE), "initial scope active");
assertFalse(pool.isAccountInScope(REPLACEMENT_SCOPE_ACCOUNT), "replacement not in pool scope");
assertFalse(pool.scopeLocked(), "scope starts unlocked");
_mockAgreementState(IAttackRegistry.ContractState.ATTACK_REQUESTED);
vm.prank(dave);
vm.expectRevert(IConfidencePool.RiskWindowNotReached.selector);
pool.pokeRiskWindow();
assertFalse(pool.scopeLocked(), "reverted poke rolled back scope lock");
_mockAgreementState(IAttackRegistry.ContractState.NOT_DEPLOYED);
_mockReplacementScopeAccountInAgreement();
address[] memory replacementScope = new address[](1);
replacementScope[0] = REPLACEMENT_SCOPE_ACCOUNT;
vm.prank(DEMO_AGREEMENT_OWNER);
pool.setPoolScope(replacementScope);
assertFalse(pool.isAccountInScope(DEMO_AGREEMENT_IN_SCOPE), "original committed scope removed");
assertTrue(pool.isAccountInScope(REPLACEMENT_SCOPE_ACCOUNT), "replacement scope installed");
assertEq(pool.eligibleStake(alice), 100 * ONE, "staker remains committed");
_mockAgreementState(IAttackRegistry.ContractState.UNDER_ATTACK);
pool.pokeRiskWindow();
assertTrue(pool.scopeLocked(), "replacement scope eventually locks");
assertGt(pool.riskWindowStart(), 0, "risk window starts");
vm.prank(alice);
vm.expectRevert(IConfidencePool.WithdrawsDisabled.selector);
pool.withdraw();
_mockAgreementState(IAttackRegistry.ContractState.CORRUPTED);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.CORRUPTED, false, address(0));
uint256 recoveryBefore = stakeToken.balanceOf(recovery);
pool.claimCorrupted();
assertEq(stakeToken.balanceOf(recovery) - recoveryBefore, 100 * ONE, "changed-scope corruption sweeps stake");
assertEq(stakeToken.balanceOf(alice), 0, "alice loses principal");
assertEq(stakeToken.balanceOf(address(pool)), 0, "pool drained");
}
function testForkPoC_CONTROL_successfulStakeDuringAttackRequestedPersistsScopeLock() external {
_mockAgreementState(IAttackRegistry.ContractState.ATTACK_REQUESTED);
_stake(alice, 100 * ONE);
assertTrue(pool.scopeLocked(), "successful gated interaction locks scope");
_mockAgreementState(IAttackRegistry.ContractState.NOT_DEPLOYED);
_mockReplacementScopeAccountInAgreement();
address[] memory replacementScope = new address[](1);
replacementScope[0] = REPLACEMENT_SCOPE_ACCOUNT;
vm.prank(DEMO_AGREEMENT_OWNER);
vm.expectRevert(IConfidencePool.ScopePostLockImmutable.selector);
pool.setPoolScope(replacementScope);
assertTrue(pool.isAccountInScope(DEMO_AGREEMENT_IN_SCOPE), "original scope preserved");
assertFalse(pool.isAccountInScope(REPLACEMENT_SCOPE_ACCOUNT), "replacement scope rejected");
}
function _stake(address user, uint256 amount) internal {
stakeToken.mint(user, amount);
vm.startPrank(user);
stakeToken.approve(address(pool), amount);
pool.stake(amount);
vm.stopPrank();
}
function _mockAgreementState(IAttackRegistry.ContractState state) internal {
vm.clearMockedCalls();
vm.mockCall(
ATTACK_REGISTRY,
abi.encodeCall(IAttackRegistry.getAgreementState, (DEMO_AGREEMENT)),
abi.encode(state)
);
}
function _mockReplacementScopeAccountInAgreement() internal {
vm.mockCall(
DEMO_AGREEMENT,
abi.encodeCall(IAgreement.isContractInScope, (REPLACEMENT_SCOPE_ACCOUNT)),
abi.encode(true)
);
}
}