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

Scope-blind auto-CORRUPTED fallback can finalize a pool through the corrupted path after moderator grace

Author Revealed upon completion

Root + Impact

Description

After expiry + MODERATOR_CORRUPTED_GRACE, claimExpired() can mechanically finalize an unresolved pool through the CORRUPTED path.

The issue is that this fallback uses the broader registry CORRUPTED state, but does not perform a pool-local scope decision before moving the pool into corrupted settlement.

This creates a scope-blind fallback:

registry = CORRUPTED
risk window observed
moderator grace elapsed
claimExpired()
→ pool outcome becomes CORRUPTED
→ corrupted/recovery settlement becomes available

A confidence pool is created with its own scoped accounts. If the broader agreement/registry is corrupted because of an account outside this pool scope, the fallback can still move this pool into the corrupted settlement path.

Affected route:

  • claimExpired()

  • claimCorrupted()

  • sweepUnclaimedCorrupted()

Risk

Likelihood

Medium.

This can occur when the registry reaches CORRUPTED, the pool remains unresolved, and the moderator grace period expires before a scope-aware moderator decision is made.

Impact

Medium.

The pool can be finalized into CORRUPTED settlement without proving that the corrupted condition belongs to the pool-local scope.

This can redirect the pool into recovery/corrupted accounting instead of preserving the expected scope-specific settlement boundary.

Proof of Concept

The PoC shows that once the grace period passes, the mechanical expiration route can finalize the pool as CORRUPTED based on the registry state.

Relevant code path

// @> Pool is expired and still unresolved.
block.timestamp >= expiry;
outcome == UNRESOLVED;
// @> Registry reports CORRUPTED.
registryState == CORRUPTED;
// @> Moderator grace period has elapsed.
block.timestamp >= expiry + MODERATOR_CORRUPTED_GRACE;
// @> claimExpired() mechanically finalizes the pool through CORRUPTED.
claimExpired();
// @> Corrupted settlement routes become available.
claimCorrupted();
sweepUnclaimedCorrupted();

Minimal test logic

function testAutoCorruptedAfterModeratorGraceIsScopeBlind() public {
// pool has a local scope
assertTrue(pool.isAccountInScope(accountA));
assertFalse(pool.isAccountInScope(accountB));
// registry reaches CORRUPTED due to a broader agreement state
registry.setState(CORRUPTED);
// risk was observed and pool expires
vm.warp(pool.expiry() + pool.MODERATOR_CORRUPTED_GRACE() + 1);
// @> anyone can call the mechanical resolution route
pool.claimExpired();
// @> pool enters CORRUPTED settlement
assertEq(uint256(pool.outcome()), uint256(Outcome.CORRUPTED));
}

Expected behavior

The corrupted fallback should preserve the pool-local scope boundary before finalizing the pool as CORRUPTED.

Observed behavior

After moderator grace, the pool can be finalized through the corrupted path based on the broader registry state, without a local scope-specific corrupted decision.

Recommended Mitigation

The corrupted fallback should be bound to the pool-local scope before entering corrupted settlement.

- Finalize CORRUPTED fallback from broad registry state alone.
+ Require scope-specific corrupted confirmation before corrupted fallback settlement.
+ Preserve the pool-local scope boundary before enabling recovery/corrupted accounting.

A regression test should cover a pool with one local scoped account and a broader corrupted registry state, then verify that the pool cannot enter CORRUPTED settlement unless the corrupted condition is tied to the pool scope.

Support

FAQs

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

Give us feedback!