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

Triple Storage Read — `outcome` Uncached in `claimExpired()`

Author Revealed upon completion

Root + Impact

Description

claimExpired() reads the outcome state variable from storage three separate times (lines 514, 518, 602) without caching it. Since outcome is immutable during the function's execution (no writes to it within the function), the value could be read once into a memory local and reused, saving two warm SLOADs (~200 gas).

// claimExpired() lines 514, 518, 602
@> if (outcome != PoolStates.Outcome.UNRESOLVED && outcome != PoolStates.Outcome.EXPIRED) { // SLOAD #1
// ...
@> if (outcome == PoolStates.Outcome.UNRESOLVED) { // SLOAD #2
// ...
@> if (outcome == PoolStates.Outcome.SURVIVED) { // SLOAD #3

Risk

Likelihood: High — every claimExpired() call incurs two redundant reads.

Impact: Low — approximately 200 gas wasted. No correctness impact; outcome is confirmed invariant during the function body.

Proof of Concept

File: G3-TripleSload-Outcome-ClaimExpired.poc.t.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {IAttackRegistry} from "@battlechain/interface/IAttackRegistry.sol";
import {PoolStates} from "src/libraries/PoolStates.sol";
import {BaseConfidencePoolTest} from "test/helpers/BaseConfidencePoolTest.sol";
/// @notice PoC for G-3: Triple SLOAD of `outcome` in claimExpired()
contract G3_TripleSload_Outcome_ClaimExpired_POC is BaseConfidencePoolTest {
function testPOC_G3_tripleSload_outcomeInClaimExpired() external {
_stake(alice, 100 * ONE);
vm.warp(pool.expiry());
// claimExpired reads `outcome` from storage 3 times:
// Line 514: outcome != UNRESOLVED && outcome != EXPIRED
// Line 518: outcome == UNRESOLVED
// Line 602: outcome == SURVIVED
// Each is a separate SLOAD. Caching once saves ~200 gas.
uint256 gasBefore = gasleft();
vm.prank(alice);
pool.claimExpired();
uint256 gasUsed = gasBefore - gasleft();
assertGt(gasUsed, 0, "gas measurement sanity");
assertEq(uint256(pool.outcome()), uint256(PoolStates.Outcome.EXPIRED));
}
}

Run: forge test --match-path 'G3-TripleSload-Outcome-ClaimExpired.poc.t.sol' -vv

Recommended Mitigation

Cache outcome to a local variable at the function entry:

+ PoolStates.Outcome cachedOutcome = outcome;
- if (outcome != PoolStates.Outcome.UNRESOLVED && outcome != PoolStates.Outcome.EXPIRED) {
+ if (cachedOutcome != PoolStates.Outcome.UNRESOLVED && cachedOutcome != PoolStates.Outcome.EXPIRED) {
- if (outcome == PoolStates.Outcome.UNRESOLVED) {
+ if (cachedOutcome == PoolStates.Outcome.UNRESOLVED) {
- if (outcome == PoolStates.Outcome.SURVIVED) {
+ if (cachedOutcome == PoolStates.Outcome.SURVIVED) {

Support

FAQs

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

Give us feedback!