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).
@> if (outcome != PoolStates.Outcome.UNRESOLVED && outcome != PoolStates.Outcome.EXPIRED) {
@> if (outcome == PoolStates.Outcome.UNRESOLVED) {
@> if (outcome == PoolStates.Outcome.SURVIVED) {
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
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";
contract G3_TripleSload_Outcome_ClaimExpired_POC is BaseConfidencePoolTest {
function testPOC_G3_tripleSload_outcomeInClaimExpired() external {
_stake(alice, 100 * ONE);
vm.warp(pool.expiry());
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) {