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

Pool can be created from a non-binding agreement, causing the pool to track the wrong risk lifecycle

Author Revealed upon completion

Root + Impact

Description

The expected behavior is that a pool should track the lifecycle of the binding BattleChain agreement for the scoped contracts it insures.

Right now, pool creation validates only:

  1. The agreement is factory-created (isAgreementValid), and

  2. Each account is in that agreement’s scope (isContractInScope).

The issue is that these checks do not prove the provided agreement is the binding agreement for those contracts. If a valid-but-non-binding agreement is used, the pool can monitor the wrong agreement state and resolve incorrectly.

// src/ConfidencePoolFactory.sol
function createPool(...) external whenNotPaused returns (address pool) {
...
if (!safeHarborRegistry.isAgreementValid(agreement)) revert InvalidAgreement();
if (IAgreement(agreement).owner() != msg.sender) revert UnauthorizedCreator();
>> // @missing: resolve each scoped contract via AttackRegistry.getAgreementForContract(account)
// and ensure it equals `agreement`
...
}

Supporting semantics from BattleChain interfaces:

// lib/.../interface/IAgreement.sol
// @note true here does NOT mean this agreement is binding for that contract
function isContractInScope(address contractAddress) external view returns (bool);
// lib/.../interface/IBattleChainSafeHarborRegistry.sol
// @note true here only means "factory-created agreement"
function isAgreementValid(address agreementAddress) external view returns (bool);

And the pool later reads state by agreement address:

// src/ConfidencePool.sol
function _getAgreementState() internal view returns (IAttackRegistry.ContractState) {
...
return IAttackRegistry(attackRegistry).getAgreementState(agreement);
// @uses pool's configured agreement directly
}

Risk

Likelihood:

  • BattleChain explicitly separates “in scope” from “binding agreement”, so this mismatch condition is structurally possible.

  • A pool creator controls which valid agreement address is passed into createPool.

Impact:

  • The pool can observe a quiet lifecycle (NOT_DEPLOYED/non-terminal) while the real binding agreement transitions to UNDER_ATTACK/CORRUPTED.

  • This breaks the insurance intent: resolution can follow the wrong lifecycle and produce an incorrect payout outcome.

Proof of Concept

// Scenario (state-level PoC)
// 1) Contract C has binding Agreement A in AttackRegistry.getAgreementForContract(C)
// 2) Attacker creates/uses Agreement B (factory-valid), also containing C in scope
// 3) Attacker creates pool with agreement = B (passes createPool checks)
// 4) Real incident occurs on C -> Agreement A goes UNDER_ATTACK/CORRUPTED
// 5) Pool reads getAgreementState(B), not getAgreementForContract(C)
// 6) Pool follows B lifecycle (can remain non-risk), leading to incorrect resolution path

Recommended Mitigation

At pool creation, verify binding agreement consistency for each scoped account:

  1. Resolve current attack registry from safeHarborRegistry.getAttackRegistry()

  2. For each account in accounts, require:
    IAttackRegistry(attackRegistry).getAgreementForContract(account) == agreement

  3. Revert on mismatch

This keeps pool lifecycle tracking aligned with BattleChain’s canonical binding-resolution model.

Support

FAQs

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

Give us feedback!