ConfidencePoolFactory.createPool creates a ConfidencePool that is supposed to cover a specific set of BattleChain accounts belonging to the protocol behind the given Safe Harbor agreement. Third parties trust getScopeAccounts() and back the pool's stakers via contributeBonus(). To validate this, the factory/pool check isAgreementValid, agreement.owner() == msg.sender, and per-account IAgreement(agreement).isContractInScope(account).
None of these prove agreement is actually bound to the accounts it lists — isContractInScope is a plain cache the agreement's own owner fills via addAccounts/addOrSetChains, with no ownership proof behind it. The real binding lives only in AttackRegistry.s_contractToAgreement, populated exclusively by requestUnderAttack/requestUnderAttackForUnverifiedContracts — neither the factory nor the pool ever reads it. So anyone can permissionlessly deploy their own empty Agreement, list a real, unrelated protocol's contract as "in scope", and spin up a pool through the legitimate factory flow that looks exactly like a genuine confidence pool for that protocol.
Likelihood:
Deploying an Agreement via AgreementFactory.create is fully permissionless and free of any ownership check on the scope addresses it lists.
The attacker's agreement never needs to interact with AttackRegistry at all — leaving it unregistered keeps its state at NOT_DEPLOYED for the entire life of the pool, which is exactly the state the pool treats as "pre-attack, nothing at risk yet."
Every step of the attack is an ordinary, unprivileged function call with ordinary parameters (createPool, stake, contributeBonus, claimExpired, sweepUnclaimedBonus), so no special token behavior, no timing race, no cooperation from the DAO, the registry, or the impersonated protocol is required.
Impact:
All bonus contributions (contributeBonus) sent to the pool are permanently and irrecoverably swept to recoveryAddress — an address the attacker sets to themselves at pool creation — via the permissionless sweepUnclaimedBonus().
Stakers recover their own principal in full, so the entire loss falls on whoever donated to the bonus pool believing it was backing a real, DAO-tracked protocol.
Copy to a test file and run with forge. Walkthrough:
Attacker deploys their own MockAgreement and self-declares an unrelated, real protocol's address (REAL_PROTOCOL_CONTRACT) as in-scope — no ownership of that address required.
Attacker calls the real factory.createPool() with that address as scope. Both gates (isAgreementValid, owner() == msg.sender) pass because the attacker owns their own fake agreement, not because they own REAL_PROTOCOL_CONTRACT.
A staker and a bonus donor interact with the pool normally — it publicly lists REAL_PROTOCOL_CONTRACT as covered scope, so it looks legitimate.
The attacker never calls requestUnderAttack, so AttackRegistry never links REAL_PROTOCOL_CONTRACT to the fake agreement; the registry state stays NOT_DEPLOYED for the pool's entire life, and riskWindowStart never opens.
At expiry, claimExpired() resolves the pool as EXPIRED. The staker gets their principal back in full.
The attacker calls sweepUnclaimedBonus() and receives 100% of the donor's bonus, since no bonus was ever earned as a risk premium.
Cross-check the real contract-to-agreement binding when scope is set, instead of trusting the agreement's self-declared cache alone:
Note: getAgreementForContract only populates after requestUnderAttack/requestUnderAttackForUnverifiedContracts, so this check also rejects a brand-new, not-yet-registered agreement — if that needs to stay supported, defer the binding check to the first _observePoolState() call instead of initialize/setPoolScope.
Defense-in-depth even without closing the binding gap: don't hand unclaimed bonus to a sponsor-controlled recoveryAddress when no risk was ever observed:
This removes the profit motive for the impersonation attack even if the scope-binding gap is not fully closed.
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.