ConfidencePool lets a pool commit to a fixed list of BattleChain accounts. Once the registry leaves pre-attack staging, this scope is locked and the design says the pool's own scope remains the binding commitment to stakers.
However, the pool validates and stores scope using only:
and all later settlement reads only:
The pool never verifies that each locked scoped account is still bound to agreement through AttackRegistry.getAgreementForContract(account).
This creates a settlement mismatch. A contract can be locked in the pool scope while later being removed/unbound from the original agreement and bound to another agreement. If that scoped contract is corrupted under the new binding agreement, the pool still checks the stale original agreement. As a result, the moderator cannot mark the pool CORRUPTED, and the expiry path can pay principal plus bonus to stakers even though the pool's locked scoped account was corrupted.
The upstream Safe Harbor interfaces explicitly separate agreement scope from binding. isContractInScope() returning true does not prove that the agreement is the binding agreement for that contract, and isAgreementValid() only proves that an agreement was factory-created. The binding agreement must be resolved through AttackRegistry.getAgreementForContract(account).
Likelihood:
The pool minimum expiry is 30 days, while the upstream agreement commitment window can be much shorter. After the commitment window expires, an agreement owner can remove a BattleChain account from the agreement scope, which syncs unregistration in the AttackRegistry.
scopeLocked only freezes the pool-local _scopeAccounts; it does not freeze or verify the upstream AttackRegistry binding for those accounts.
A removed account can later be bound to another agreement and corrupted there, while the pool continues to settle only against the original agreement's state.
Impact:
A locked pool-scoped contract can be corrupted under its actual binding agreement, but the pool cannot be marked CORRUPTED unless the stale original agreement is also CORRUPTED.
claimExpired() can incorrectly resolve the pool as EXPIRED and pay stakers principal plus bonus.
The intended CORRUPTED distribution is bypassed for a contract that remains in the pool's immutable published scope.
Create the file:
Run:
Expected result:
The test demonstrates:
The pool locks scopedContract in its own immutable scope.
The same account is later unbound from Agreement A and bound to Agreement B.
Agreement B becomes CORRUPTED.
The moderator cannot mark the pool CORRUPTED because the pool checks Agreement A's state.
claimExpired() pays the staker 100e18 principal plus 50e18 bonus, while the whitehat receives nothing.
Do not treat IAgreement.isContractInScope(account) as sufficient proof that the pool's scoped account is governed by the stored agreement.
When creating or updating pool scope, also resolve the current AttackRegistry binding:
Additionally, before locking scope or resolving outcomes, either:
verify that every locked scoped account is still bound to agreement; or
explicitly store the account-to-agreement binding at scope lock and use that stored commitment consistently in settlement logic.
If the intended design is that pool-local scope remains binding even after upstream agreement narrowing, then CORRUPTED resolution cannot be gated only on getAgreementState(agreement). The pool needs a settlement rule that can enforce corruption for the locked scoped account's actual binding agreement.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.