pragma solidity 0.8.26;
import {Test} from "forge-std/Test.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.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";
import {MockAgreement} from "test/mocks/MockAgreement.sol";
import {MockSafeHarborRegistry} from "test/mocks/MockSafeHarborRegistry.sol";
import {MockBindingAwareAttackRegistry} from "test/mocks/MockBindingAwareAttackRegistry.sol";
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
contract BindingAgreementMismatchTest is Test {
uint256 internal constant ONE = 1e18;
address internal constant SCOPE_ACCOUNT = address(0xC0FFEE);
MockERC20 internal token;
MockSafeHarborRegistry internal registry;
MockBindingAwareAttackRegistry internal attackRegistry;
MockAgreement internal agreementA;
MockAgreement internal agreementB;
ConfidencePoolFactory internal factory;
address internal moderator = makeAddr("moderator");
address internal agreementOwner = makeAddr("agreementOwner");
address internal recovery = makeAddr("recovery");
address internal staker = makeAddr("staker");
function setUp() public {
token = new MockERC20();
registry = new MockSafeHarborRegistry();
attackRegistry = new MockBindingAwareAttackRegistry();
agreementA = new MockAgreement(agreementOwner);
agreementB = new MockAgreement(agreementOwner);
agreementA.setContractInScope(SCOPE_ACCOUNT, true);
agreementB.setContractInScope(SCOPE_ACCOUNT, true);
registry.setAttackRegistry(address(attackRegistry));
registry.setAgreementValid(address(agreementA), true);
registry.setAgreementValid(address(agreementB), true);
ConfidencePool implementation = new ConfidencePool();
ConfidencePoolFactory factoryImpl = new ConfidencePoolFactory();
ERC1967Proxy proxy = new ERC1967Proxy(
address(factoryImpl),
abi.encodeCall(ConfidencePoolFactory.initialize, (address(registry), address(implementation), moderator))
);
factory = ConfidencePoolFactory(address(proxy));
factory.setStakeTokenAllowed(address(token), true);
}
function testBindingAgreementStateDisablesWithdrawEvenIfConfiguredAgreementLooksPreRisk() external {
address[] memory accounts = new address[](1);
accounts[0] = SCOPE_ACCOUNT;
attackRegistry.setBindingAgreement(SCOPE_ACCOUNT, address(agreementB));
attackRegistry.setAgreementState(address(agreementA), IAttackRegistry.ContractState.ATTACK_REQUESTED);
attackRegistry.setAgreementState(address(agreementB), IAttackRegistry.ContractState.UNDER_ATTACK);
vm.prank(agreementOwner);
address poolAddr =
factory.createPool(address(agreementA), address(token), block.timestamp + 31 days, ONE, recovery, accounts);
ConfidencePool pool = ConfidencePool(poolAddr);
token.mint(staker, 100 * ONE);
vm.startPrank(staker);
token.approve(address(pool), 100 * ONE);
pool.stake(100 * ONE);
vm.expectRevert(IConfidencePool.WithdrawsDisabled.selector);
pool.withdraw();
vm.stopPrank();
assertEq(
attackRegistry.getAgreementForContract(SCOPE_ACCOUNT), address(agreementB), "binding agreement differs"
);
}
function testBindingAgreementCorruptionBlocksStakeAndPreventsWrongExpiryPath() external {
address[] memory accounts = new address[](1);
accounts[0] = SCOPE_ACCOUNT;
attackRegistry.setBindingAgreement(SCOPE_ACCOUNT, address(agreementB));
attackRegistry.setAgreementState(address(agreementA), IAttackRegistry.ContractState.ATTACK_REQUESTED);
attackRegistry.setAgreementState(address(agreementB), IAttackRegistry.ContractState.CORRUPTED);
vm.prank(agreementOwner);
address poolAddr =
factory.createPool(address(agreementA), address(token), block.timestamp + 31 days, ONE, recovery, accounts);
ConfidencePool pool = ConfidencePool(poolAddr);
token.mint(staker, 100 * ONE);
vm.startPrank(staker);
token.approve(address(pool), 100 * ONE);
vm.expectRevert(IConfidencePool.StakingClosed.selector);
pool.stake(100 * ONE);
vm.stopPrank();
assertEq(
attackRegistry.getAgreementForContract(SCOPE_ACCOUNT), address(agreementB), "binding agreement differs"
);
}
}
function _getAgreementState() internal view returns (IAttackRegistry.ContractState) {
address attackRegistry = safeHarborRegistry.getAttackRegistry();
if (attackRegistry == address(0)) revert InvalidAgreement();
- return IAttackRegistry(attackRegistry).getAgreementState(agreement);
+ address resolvedAgreement = _resolveBindingAgreement(attackRegistry);
+ return IAttackRegistry(attackRegistry).getAgreementState(resolvedAgreement);
}
+function _resolveBindingAgreement(address attackRegistry) internal view returns (address resolvedAgreement) {
+ resolvedAgreement = agreement;
+
+ address[] memory accounts = _scopeAccounts;
+ for (uint256 i; i < accounts.length; ++i) {
+ (bool ok, bytes memory data) = attackRegistry.staticcall(
+ abi.encodeCall(IAttackRegistry.getAgreementForContract, (accounts[i]))
+ );
+
+ // Best-effort fallback for mocks or legacy registries that do not expose the selector.
+ if (!ok || data.length < 32) continue;
+
+ address bindingAgreement = abi.decode(data, (address));
+ if (bindingAgreement == address(0)) continue;
+
+ if (resolvedAgreement != agreement && resolvedAgreement != bindingAgreement) {
+ revert InvalidAgreement();
+ }
+
+ resolvedAgreement = bindingAgreement;
+ }
+}