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

Fake Pool Impersonation — Missing On-Chain Verification of Pool Legitimacy Allows Theft of All Staker Deposits

Author Revealed upon completion

Description

  • When a pool is created, createPool() is expected to guarantee that the pool's scoped accounts are genuinely insured by the agreement — so that a staker reading the pool on-chain can trust that the pool represents the real protocol. The validation checks that the agreement was factory-deployed, that the caller owns the agreement, and that each scoped account is listed in the agreement's scope.

  • However, none of these checks verify that the agreement is the canonical/binding owner of the scoped contracts. isAgreementValid() returns true for any factory-deployed agreement, and the agreement's scope is fully attacker-controlled. An attacker can therefore create their own agreement that lists real protocol contracts, pass every check, and stand up a pool that is on-chain indistinguishable from the legitimate one — while the attacker controls the moderator role and recovery address.

// BattleChainSafeHarborRegistry.isAgreementValid() — the only gating check
function isAgreementValid(address agreementAddress) external view returns (bool) {
@> return IAgreementFactory(s_agreementFactory).isAgreementContract(agreementAddress);
// Only checks "was this created by AgreementFactory?" —
// NOT "does this agreement canonically own these contracts?"
}
// ConfidencePool._replaceScope() — accepts any agreement claiming a contract
if (!IAgreement(agreement).isContractInScope(account)) {
revert AccountNotInAgreementScope(account);
}
@> // No check that `agreement` is the binding agreement for `account`

Risk

Likelihood:

  • Any address can call AgreementFactory.create() with a scope listing real protocol contracts and then call createPool() — no special permissions, no cost beyond gas.

  • Real and fake pools expose identical scope arrays and emit identical ScopeUpdated events, so stakers relying on on-chain data cannot tell them apart and will deposit into the spoofed pool.

Impact:

  • The attacker, being pool owner, moderator, and recovery address, can flag CORRUPTED and sweep the entire pool balance to themselves — total loss of all staker deposits.

  • The attack is repeatable against every protocol, permanently undermining trust that any pool on-chain represents its claimed protocol.

Recommended Mitigation

Verify that the agreement is the canonical (binding) owner of each scoped contract, using IAttackRegistry.getAgreementForContract().

In src/interfaces/IConfidencePool.sol:

error AccountNotInAgreementScope(address account);
+ error AccountNotBoundToAgreement(address account);

In _replaceScope() in src/ConfidencePool.sol:

for (uint256 i; i < accounts.length; ++i) {
address account = accounts[i];
if (isAccountInScope[account]) revert DuplicateAccount(account);
if (!IAgreement(agreement).isContractInScope(account)) {
revert AccountNotInAgreementScope(account);
}
+ // Verify this agreement is the canonical owner of this contract
+ address attackRegistry = safeHarborRegistry.getAttackRegistry();
+ if (IAttackRegistry(attackRegistry).getAgreementForContract(account) != agreement) {
+ revert AccountNotBoundToAgreement(account);
+ }
_scopeAccounts.push(account);
isAccountInScope[account] = true;
}

Support

FAQs

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

Give us feedback!