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

Pool-local scope can remain inconsistent after the underlying agreement scope changes

Author Revealed upon completion

Root + Impact

Description

The pool validates and stores its own local scope at creation or through setPoolScope(), but the stored pool scope does not automatically reconcile if the underlying agreement scope changes later.

This creates a scope-consistency gap between:

  • the current scope of the underlying agreement; and

  • the pool-local scope that remains stored and used by the existing confidence pool.

The affected route is:

  • ConfidencePoolFactory.createPool()

  • ConfidencePool.initialize()

  • setPoolScope()

  • flagOutcome()

At pool creation, the factory validates the agreement and the requested scope. The pool then stores the scope locally.

If the underlying agreement later narrows or changes its own scope, the existing pool can continue using the older pool-local scope. This means the pool can remain connected to scope data that no longer matches the current agreement state.

Risk

Likelihood

Medium.

This issue can occur when a pool is created with a valid scope and the underlying agreement scope is later modified or narrowed.

This does not require a non-standard ERC20 token, malicious token behavior, owner privilege, or direct contract modification. The behavior follows from the pool-local scope storage and later agreement-scope drift.

Impact

Medium.

The pool can continue operating with a stale or outdated scope after the underlying agreement has changed.

This can affect settlement reasoning, outcome review, recovery expectations, and user assumptions about which contracts are actually covered by the active agreement-linked pool.

Users and integrations may rely on the agreement's current scope, while the existing pool continues using a previous scope view.

This creates an inconsistency between agreement-level state and pool-level settlement state.

Proof of Concept

The PoC below shows that the pool-local scope remains active even after the underlying agreement scope is changed.

Full PoC test

function testPoolScopeCommitmentSurvivesAgreementNarrowing() public {
address scopedAccount = address(0xCAFE);
address[] memory initialScope = new address[](1);
initialScope[0] = scopedAccount;
// The account is in scope when the pool is created.
mockAgreement.setContractInScope(scopedAccount, true);
ConfidencePool pool = factory.createPool(
agreement,
address(token),
recovery,
expiry,
minStake,
initialScope
);
// At creation time both the agreement and pool agree on scope.
assertTrue(mockAgreement.isContractInScope(scopedAccount));
assertTrue(pool.isAccountInScope(scopedAccount));
// The underlying agreement scope is later narrowed.
mockAgreement.setContractInScope(scopedAccount, false);
// Agreement scope changed.
assertFalse(mockAgreement.isContractInScope(scopedAccount));
// Existing pool-local scope did not change.
assertTrue(pool.isAccountInScope(scopedAccount));
}

Relevant code path

// Scope is validated against the agreement only when the pool scope is created or updated.
require(IAgreement(agreement).isContractInScope(account), "not in scope");
// @> The result is then stored as pool-local scope.
isAccountInScope[account] = true;
// Later, the pool keeps using its own stored scope.
// @> The pool-local scope is not automatically reconciled when the agreement scope changes later.
bool stillInPoolScope = isAccountInScope[account];

The PoC proves that after the agreement scope is changed, the agreement no longer treats the account as in scope, while the already-created pool still keeps the same account as in scope.

Command

forge test --match-path "test/internal/*" --match-test testPoolScopeCommitmentSurvivesAgreementNarrowing -vvv

Observed output

[PASS] testPoolScopeCommitmentSurvivesAgreementNarrowing()
Suite result: ok.

What the PoC proves

Before the agreement scope change:

agreement.isContractInScope(account) = true
pool.isAccountInScope(account) = true

After the agreement scope change:

agreement.isContractInScope(account) = false
pool.isAccountInScope(account) = true

This proves that the pool-local scope can become stale and diverge from the current agreement scope.

Recommended Mitigation

The pool lifecycle should preserve a consistent relationship between agreement-level scope and pool-level settlement scope.

A safe fix should ensure that scope-sensitive settlement logic cannot silently operate on stale scope assumptions after the underlying agreement state has materially changed.

- Allow existing pools to continue using stale pool-local scope after the agreement scope changes.
+ Preserve consistency between agreement-level scope and pool-level settlement scope.
+ Prevent stale scope assumptions from silently affecting existing pool settlement behavior.

The mitigation should be applied at the scope/accounting boundary, so that pool-local commitments and agreement-level scope changes cannot diverge in a way that affects settlement expectations.

A regression test should cover a pool created with an initially valid scope, followed by a later agreement-scope change, and verify that the pool cannot silently continue with inconsistent settlement assumptions.

Support

FAQs

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

Give us feedback!