function withdraw() external nonReentrant {
if (outcome != PoolStates.Outcome.UNRESOLVED) revert OutcomeAlreadySet();
IAttackRegistry.ContractState state = _observePoolState();
if (riskWindowStart != 0 || ...) {
revert WithdrawsDisabled();
}
uint256 amount = eligibleStake[msg.sender];
if (amount == 0) revert InvalidAmount();
_clampUserSums(msg.sender);
sumStakeTime -= userSumStakeTime[msg.sender];
}
function claimSurvived() external nonReentrant {
if (outcome != PoolStates.Outcome.SURVIVED) revert OutcomeNotSet();
if (hasClaimed[msg.sender]) revert InvalidAmount();
uint256 userEligible = eligibleStake[msg.sender];
if (userEligible == 0) revert InvalidAmount();
_clampUserSums(msg.sender);
}
The _clampUserSums function performs a storage load (riskWindowStart, eligibleStake[u]) and a comparison on every call. When the user has no stake, these operations execute uselessly because the subsequent zero-balance check will revert the transaction anyway.
pragma solidity 0.8.26;
import "forge-std/Test.sol";
contract ZeroStakeGasWaste is Test {
function test_withdraw_wastes_gas_before_revert() public {
}
function test_claimSurvived_wastes_gas_before_revert() public {
}
}
To measure the difference, add a counter to _clampUserSums in a test fork and compare gas usage with the check moved earlier.
Move the zero-stake validation before _clampUserSums in both functions:
// withdraw()
function withdraw() external nonReentrant {
if (outcome != PoolStates.Outcome.UNRESOLVED) revert OutcomeAlreadySet();
IAttackRegistry.ContractState state = _observePoolState();
if (riskWindowStart != 0 || ...) {
revert WithdrawsDisabled();
}
+ uint256 amount = eligibleStake[msg.sender];
+ if (amount == 0) revert InvalidAmount();
+
- uint256 amount = eligibleStake[msg.sender];
- if (amount == 0) revert InvalidAmount();
-
- _clampUserSums(msg.sender);
+ _clampUserSums(msg.sender);
sumStakeTime -= userSumStakeTime[msg.sender];
// ...
}
// claimSurvived()
function claimSurvived() external nonReentrant {
if (outcome != PoolStates.Outcome.SURVIVED) revert OutcomeNotSet();
if (hasClaimed[msg.sender]) revert InvalidAmount();
- uint256 userEligible = eligibleStake[msg.sender];
- if (userEligible == 0) revert InvalidAmount();
-
- _clampUserSums(msg.sender);
+ uint256 userEligible = eligibleStake[msg.sender];
+ if (userEligible == 0) revert InvalidAmount();
+
+ _clampUserSums(msg.sender);
// ...
}
The fix reorders existing statements without changing semantics — _clampUserSums is only meaningful when eligibleStake > 0, so the early return is safe.