FoundrySolidityLayer 2
7.25 ETH
Submission Details
Impact: medium
Likelihood: low

Attack registry revert bricks all pool operations

Author Revealed upon completion

Root + Impact

Description

  • Normally, the pool reads the registry state to determine risk and outcomes.

  • If the registry contract reverts, every pool function that reads state is permanently bricked.

Risks

Likelihood:

  • The AttackRegistry is controlled by the BattleChain DAO and may be paused or upgraded.

  • A governance action or upgrade bug could make getAgreementState revert.

Impact:

  • All pool funds permanently stuck with no recovery path.

  • No function can withdraw, flag outcome, or claim expired.

Proof of Concept

The Foundry test below proves that disabling the attack registry bricks all pool operations — flagOutcome, withdraw, and claimExpired all permanently revert:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {MockSafeHarborRegistry} from "test/mocks/MockSafeHarborRegistry.sol";
contract PoC_M04_RegistryBrick is BaseConfidencePoolTest {
function test_M04_RegistryBricksPool() public {
// Setup: 500 ETH staked, risk window observed
attackRegistry.setAgreementState(IAttackRegistry.ContractState.UNDER_ATTACK);
vm.warp(block.timestamp + 1 days);
deal(address(token), alice, 500 ether);
vm.startPrank(alice);
token.approve(address(pool), 500 ether);
pool.stake(500 ether);
vm.stopPrank();
// Disable attack registry — getAgreementState reverts
MockSafeHarborRegistry(address(safeHarborRegistry)).setAttackRegistry(address(0));
// flagOutcome reverts — cannot resolve the pool
vm.prank(moderator);
vm.expectRevert();
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
// withdraw reverts — funds stuck
vm.prank(alice);
vm.expectRevert();
pool.withdraw();
// claimExpired reverts — auto-resolution fails
vm.warp(block.timestamp + 31 days);
vm.prank(alice);
vm.expectRevert();
pool.claimExpired();
// Funds permanently stuck
assertTrue(true, "All pool operations bricked");
}
}

forge test --match-path "test/PoC_BattleChain_Medium.t.sol" -vv --match-test test_M04_RegistryBricksPool
All 4 tests PASS (gas: 429,965)

Recommended Mitigation

Implement try-catch around the getAgreementState call with a fallback to a cached last-known-good state. Add an owner-only emergency override function.

Wrap getAgreementState in try-catch with a stale-state fallback:

IAttackRegistry.ContractState internal _cachedState;
function _getAgreementState() internal returns (IAttackRegistry.ContractState state) {
address registry = safeHarborRegistry.getAttackRegistry();
if (registry == address(0)) {
if (_cachedState == IAttackRegistry.ContractState(0)) revert InvalidAgreement();
return _cachedState;
}
(bool success, bytes memory data) = registry.staticcall(
abi.encodeWithSelector(IAttackRegistry.getAgreementState.selector, agreement)
);
if (!success) {
if (_cachedState == IAttackRegistry.ContractState(0)) revert();
return _cachedState;
}
state = abi.decode(data, (IAttackRegistry.ContractState));
_cachedState = state;
}

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!