Root + Impact
Description
In withdraw(), claimSurvived(), and claimExpired(), eligibleStake[msg.sender] is read into a local variable, then _clampUserSums(msg.sender) is called on the next line. Inside _clampUserSums() (line 679), eligibleStake[u] is read from storage a second time for the same address and same slot — a redundant warm SLOAD (~100 gas each). This happens at 3 call sites.
@> uint256 userEligible = eligibleStake[msg.sender];
@> _clampUserSums(msg.sender);
@> uint256 stake_ = eligibleStake[u];
Risk
Likelihood: High — every call to withdraw(), claimSurvived(), and claimExpired() incurs the redundant read.
Impact: Low — approximately 100 gas wasted per call (warm SLOAD). No correctness impact.
Proof of Concept
File: G1-DoubleSload-ClampUserSums.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 G1_DoubleSload_ClampUserSums_POC is BaseConfidencePoolTest {
function testPOC_G1_doubleSload_inClaimSurvived() external {
_stake(alice, 100 * ONE);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.PRODUCTION);
vm.prank(moderator);
pool.flagOutcome(PoolStates.Outcome.SURVIVED, false, address(0));
uint256 gasBefore = gasleft();
vm.prank(alice);
pool.claimSurvived();
uint256 gasUsed = gasBefore - gasleft();
assertGt(gasUsed, 0, "gas measurement sanity");
}
function testPOC_G1_doubleSload_inClaimExpired() 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");
}
function testPOC_G1_doubleSload_inWithdraw() external {
_stake(alice, 100 * ONE);
attackRegistry.setAgreementState(IAttackRegistry.ContractState.NOT_DEPLOYED);
uint256 gasBefore = gasleft();
vm.prank(alice);
pool.withdraw();
uint256 gasUsed = gasBefore - gasleft();
assertGt(gasUsed, 0, "gas measurement sanity");
}
}
Run: forge test --match-path 'G1-DoubleSload-ClampUserSums.poc.t.sol' -vv
Recommended Mitigation
Pass the already-read eligibleStake value to _clampUserSums as a second parameter:
- function _clampUserSums(address u) internal {
+ function _clampUserSums(address u, uint256 stake_) internal {
- uint256 stake_ = eligibleStake[u];
- _clampUserSums(msg.sender);
+ _clampUserSums(msg.sender, userEligible);