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

README documents that the moderator resolves EXPIRED via flagOutcome, but flagOutcome reverts InvalidOutcome for EXPIRED — the two protocol docs contradict each other and the code

Author Revealed upon completion

Root + Impact

Description

  • README.md lists the moderator's abilities as flagging the pool outcome "SURVIVED / CORRUPTED / EXPIRED via flagOutcome", and protocol-readme.md separately (correctly) documents that EXPIRED is reached mechanically via claimExpired() after expiry. The two authoritative docs disagree about how EXPIRED is set.

  • In the code, flagOutcome only accepts SURVIVED and CORRUPTED; every other outcome (including EXPIRED) hits the trailing else and reverts InvalidOutcome. EXPIRED is only ever set mechanically inside claimExpired(). A moderator following README.md and calling flagOutcome(EXPIRED, ...) gets a hard revert; there is no on-chain path for the moderator to resolve EXPIRED.

// flagOutcome — accepts only SURVIVED / CORRUPTED; EXPIRED falls through to the revert
if (newOutcome == PoolStates.Outcome.SURVIVED) { ... }
else if (newOutcome == PoolStates.Outcome.CORRUPTED) { ... }
@> else {
@> revert InvalidOutcome(); // flagOutcome(EXPIRED, ...) always reverts
}
// EXPIRED is only reachable mechanically, via claimExpired():
@> outcome = PoolStates.Outcome.EXPIRED; // inside claimExpired(), never via flagOutcome

Risk

Likelihood:

  • A moderator relying on README.md (which lists EXPIRED among the outcomes flagged via flagOutcome) attempts to resolve a pool as EXPIRED through flagOutcome.

  • protocol-readme.md and the interface docs describe the actual (mechanical claimExpired) path, so the two documents are internally inconsistent for any reader.

Impact:

  • No funds are at risk. The impact is a documentation/spec defect: the README overstates the moderator's flagOutcome capability, so a moderator following it calls a function that always reverts, and an auditor/integrator reading the two docs gets contradictory models of the resolution state machine.

Proof of Concept

function test_R2_flagOutcomeExpired_reverts() external {
vm.warp(expiry + 1);
vm.prank(moderator);
vm.expectRevert(InvalidOutcome.selector); // README says this is allowed; code rejects it
pool.flagOutcome(PoolStates.Outcome.EXPIRED, false, address(0));
}

Recommended Mitigation

Align the docs with the code — remove EXPIRED from the flagOutcome capability in README.md and state that EXPIRED is resolved mechanically via claimExpired() after expiry, matching protocol-readme.md.

- - flags the pool outcome (SURVIVED / CORRUPTED / EXPIRED) via `flagOutcome`,
+ - flags the pool outcome (SURVIVED / CORRUPTED) via `flagOutcome`;
+ EXPIRED is resolved mechanically via `claimExpired()` after `expiry`, not by the moderator.

Support

FAQs

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

Give us feedback!