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

[L-01] External calls inside loop in _replaceScope() - large scope hits gas limit or depends on agreement liveness

Author Revealed upon completion

Root + Impact

Description

_replaceScope() validates each account in the new scope by calling an external contract. This is the correct approach for ensuring scope accounts are genuinely part of the underlying agreement IAgreement(agreement).isContractInScope(account) is the on-chain source of truth for account membership.

However, because the call happens inside a loop with no upper bound on iterations, the cumulative gas cost grows linearly with scope size. Each iteration makes an external STATICCALL/CALL to the agreement contract, meaning _replaceScope() becomes both gas-intensive and dependent on agreement liveness for every execution.

// ConfidencePool.sol line 764-773
for (uint256 i; i < accounts.length; ++i) {
address account = accounts[i];
if (isAccountInScope[account]) revert DuplicateAccount(account);
// @> External call to agreement contract on every iteration
if (!IAgreement(agreement).isContractInScope(account)) {
revert AccountNotInAgreementScope(account);
}
_scopeAccounts.push(account);
isAccountInScope[account] = true;
}

Risk

Likelihood:

  • Any pool covering an agreement with a moderately large number of BattleChain contracts (dozens or more) will encounter elevated gas costs on initialize() or setPoolScope(), with the block gas limit as an eventual hard ceiling.

  • If the agreement contract is paused, self-destructed, or its registry pointer migrated between scope updates, every isContractInScope() call reverts, permanently blocking scope changes without any on-chain recovery path.

Impact:

  • Pool creation (initialize()) can fail outright for agreements with large scopes, preventing sponsors from deploying confidence pools for those agreements at all.

  • setPoolScope() can become permanently uncallable if the agreement contract becomes unavailable, leaving the pool locked to its last-set scope with no way to correct it.

Proof of Concept

To demonstrate the gas exhaustion risk, consider a sponsor creating a pool for an agreement that covers 200 BattleChain contracts. Each call to agreement.isContractInScope(account) incurs a minimum CALL overhead plus the agreement-side execution cost. As the account count grows, the cumulative gas approaches and eventually exceeds the block gas limit, causing the transaction to revert, which means the pool can never be initialized for that agreement.

// Sponsor calls factory.createPool(agreement, ..., accounts) where accounts.length == 200.
// Inside _replaceScope(), the fill loop runs 200 iterations:
// - SLOAD isAccountInScope[account]: ~2100 gas (cold)
// - CALL to agreement.isContractInScope(): ~700 gas overhead + agreement execution
// - SSTORE isAccountInScope[account] = true: ~20 000 gas (cold write)
// - _scopeAccounts.push(): ~20 000 gas (cold storage write)
// Per iteration minimum: ~42 800 gas
// 200 accounts × 42 800 gas = 8 560 000 gas - exceeds most L2 block gas limits.
// Transaction reverts; the pool cannot be created for this agreement.
// If the pool was previously initialized with 200 accounts and setPoolScope() is called
// to replace them, the clearing loop adds another 200 × ~5000 gas = 1 000 000 gas,
// making setPoolScope() permanently revert for any large replacement.

Recommended Mitigation

Add a MAX_SCOPE_SIZE constant and validate at the start of _replaceScope(). Also see L-02 for the unbounded array issue:

+ uint256 private constant MAX_SCOPE_SIZE = 50;
function _replaceScope(address[] calldata accounts) internal {
if (accounts.length == 0) revert EmptyScope();
+ if (accounts.length > MAX_SCOPE_SIZE) revert ScopeTooLarge();
...
}

For larger agreements, consider accepting a sponsor-signed Merkle proof of membership instead of per-account external calls, reducing the on-chain validation to a single hash check.

Support

FAQs

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

Give us feedback!