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

Agreement `isContractInScope` desync propagates to pool scope operations

Author Revealed upon completion

Root + Impact

Pool scope operations involving the desynced account are permanently blocked with no recovery path. The root cause lies in the Agreement contract, but the pool is the victim — it correctly validates against a corrupted data source.

Description

  • _replaceScope validates every new pool scope account against IAgreement.isContractInScope. If the agreement reports an account as in-scope, the pool accepts it. The agreement's internal cache and array are supposed to stay in sync.

  • The BattleChain Agreement.addAccounts has a documented duplicate-address footgun: _addToBattleChainScope dedupes via a cache mapping, but s_accounts stores all copies. After removeAccounts, the cache entry is cleared but a duplicate survives in s_accounts. isContractInScope (which reads the cache) returns false while the account logically belongs. Any _replaceScope call including this desynced account reverts with AccountNotInAgreementScope, permanently blocking pool scope operations.

// ConfidencePool.sol:_replaceScope — line 768-770
@> if (!IAgreement(agreement).isContractInScope(account)) {
@> revert AccountNotInAgreementScope(account); // permanently blocks desynced account
@> }

Risk

Likelihood:

Reason 1 — The Agreement duplicate-address footgun is a documented, known issue in the BattleChain dependency. Any pool whose scope includes the desynced account is affected.
Reason 2 — The pool correctly validates against the agreement's stated scope. The root cause is in the Agreement contract; the pool cannot fix the underlying desync.

Impact:

Impact 1 — Pool scope operations that include the desynced account permanently revert. The pool cannot update its scope to include or exclude the affected account.
Impact 2 — Pool creation (`initialize`) that includes the desynced account in the initial scope fails, preventing deployment of pools covering that BattleChain account.

Proof of Concept

Runnable Proof of Concept

function test_LD5_AgreementScopeDesync() public {
address problemAccount = address(0xDECAF);
agreementContract.setContractInScope(problemAccount, true);
address[] memory scope1 = new address[](1);
scope1[0] = problemAccount;
pool.setPoolScope(scope1); // succeeds
// Simulate desync: cache says false
agreementContract.setContractInScope(problemAccount, false);
address[] memory scope2 = new address[](1);
scope2[0] = problemAccount;
vm.expectRevert(); // AccountNotInAgreementScope
pool.setPoolScope(scope2);
}

Forge output:

[PASS] test_LD5_AgreementScopeDesync()
Agreement isContractInScope desync permanently blocks pool scope ops

Recommended Mitigation

Root cause must be fixed in Agreement.addAccounts by deduplicating s_accounts at add time. Pool-side mitigation is limited — the pool correctly validates against the agreement.

Support

FAQs

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

Give us feedback!