Normal behavior: A pool's published scope is meant to be a trustworthy, on-chain commitment — stakers rely on getScopeAccounts()/isAccountInScope to know exactly which BattleChain contracts their stake is economically backing.
The issue: _replaceScope validates each account only via IAgreement(agreement).isContractInScope(account) — a purely self-declared cache with no ownership verification (Agreement.addAccounts/addOrSetChains never check the caller actually controls the listed address). The BattleChain interface itself documents this isn't sufficient: "A true return does NOT establish that this agreement is the Binding Agreement for the contract... resolve the Binding Agreement via AttackRegistry." The pool never performs that resolution. Confirmed live against the real BattleChain testnet: I deployed my own throwaway Agreement, self-declared the real demo agreement's real, already-bound contract as being in my scope too, and ConfidencePoolFactory.createPool accepted it without complaint — while AttackRegistry.getAgreementForContract still correctly pointed at the real agreement, not mine.
Likelihood: High — creating the deceptive pool requires nothing beyond what's already freely, permissionlessly available: deploy an Agreement via the real AgreementFactory, self-declare any contract as in scope (zero validation), then createPool referencing that self-owned agreement (trivially passes owner() == msg.sender).
Impact: Medium — forcing a fake CORRUPTED flag requires the real DAO to cooperate (approveAttack is a genuine checkpoint before any attack-lifecycle progression), so this doesn't hand an attacker unilateral fund theft. But a DAO-independent path exists: if the DAO simply never acts on the bogus request within 14 days, the agreement auto-promotes straight to PRODUCTION, skipping the active-risk states entirely — riskWindowStart never opens, triggering the already-accepted "no observed risk → bonus swept to sponsor-controlled recoveryAddress" rule (design doc §5). Combined with a spoofed, convincing-looking scope, this lets a sponsor solicit deposits and bonus contributions from parties deceived into thinking they're backing a specific, real protocol, and collect the bonus pot. Independent of any theft, a spoofed pool resolving SURVIVED is a false, misleading on-chain confidence signal about a contract that was never actually covered by this pool at all — undermining the core value proposition of the protocol.
Fork test against live BattleChain testnet (test/fork/BattleChainScopeSpoofing.fork.t.sol):
Cross-check AttackRegistry.getAgreementForContract — but only to reject a known conflict (the contract is already confirmed bound to a different agreement), not to require binding before any attack has been requested. A strict "must already be bound to this agreement" check would break the normal, intended flow of setting scope during pre-attack staging, before requestUnderAttack has ever been called.
This blocks exactly the exploited case (claiming an already-established, real, valuable contract that's already bound elsewhere — the only scenario with real deceptive value) while still allowing fresh, never-yet-attacked contracts to be scoped normally pre-attack. Residual: a contract that has genuinely never called requestUnderAttack under any agreement (boundAgreement == address(0)) can still be claimed by an unrelated party — but spoofing an obscure, unproven contract carries far less deceptive value than spoofing an already-recognized one, so this residual is a much narrower exposure than the one closed here.
Add error AccountBoundToDifferentAgreement(address account, address boundAgreement); to IConfidencePool.sol.
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.