ConfidencePool resolves its outcome by reading the on-chain state of the agreement address
stored at initialization via _getAgreementState(). The factory validates that the agreement is
registered in the SafeHarborRegistry and that the caller owns it, and validates scope accounts
via IAgreement.isContractInScope. However, it never verifies that the stored agreement is the
Binding Agreement for the scope accounts as determined by
AttackRegistry.getAgreementForContract(account).
A protocol that owns multiple valid agreements can create a pool
using a non-Binding Agreement (Agreement A) while the scope accounts' actual Binding Agreement
registered in the AttackRegistry is Agreement B. The factory comment explicitly acknowledges
this gap: "duplicates are allowed and curated off-chain" — meaning no on-chain enforcement
exists. When Agreement B goes CORRUPTED in the AttackRegistry, flagOutcome(CORRUPTED)
reverts unconditionally because _getAgreementState() reads Agreement A (which remains
PRODUCTION). The moderator is permanently blocked from flagging CORRUPTED. The pool can only
resolve SURVIVED, zeroing the whitehat's bounty and incorrectly paying stakers. There is no
setAgreement function and the pool is a non-upgradeable clone — the misconfiguration is
permanent.
Impact: High
The moderator is permanently and unconditionally blocked from flagging CORRUPTED on the pool.
Even a good-faith moderator cannot override this — flagOutcome(CORRUPTED, ...) reverts at the
registry state check. The only callable path is flagOutcome(SURVIVED, ...), which accepts
PRODUCTION state.
The whitehat's bountyEntitlement is never set (remains 0) because CORRUPTED is never
flagged. The whitehat receives nothing despite a legitimate corruption event under the Binding
Agreement. The pool permanently misreports the protocol as SURVIVED.
Place this file at test/unit/ConfidencePool.bindingAgreementMismatch.t.sol.
Run with:
Add a Binding Agreement cross-check in _replaceScope for each scope account using
attackRegistry.getAgreementForContract. This ensures the pool's stored agreement is the
agreement that actually governs the scope accounts in the AttackRegistry.
The binding != address(0) guard handles child contracts (where getAgreementForContract
returns zero and the Binding Agreement is resolved via the deployer chain). For top-level
contracts, the check enforces that the pool's stored agreement matches the AttackRegistry's
authoritative Binding Agreement for each scope account.
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.