Remediating Atomic State Update Violations
How to ensure multi-step state updates maintain invariant consistency using intermediate checks, reentrancy guards, and explicit state machine patterns.
Remediating Atomic State Update Violations
Overview
Related Detector: Atomic State Update
Atomic state update violations occur when a function writes to multiple storage slots without verifying that the combined state remains consistent. The fix is to add require() checks between writes to enforce invariants, and to read the current state before overwriting it.
Recommended Fix
Add Invariant Checks Between Writes
// BEFORE: Two writes without intermediate validation
function swap(uint256 amountIn) external {
reserveA += amountIn;
reserveB -= calculateOutput(amountIn);
}
// AFTER: Validate invariant between writes
function swap(uint256 amountIn) external {
uint256 amountOut = calculateOutput(amountIn);
require(reserveB >= amountOut, "Insufficient reserve");
reserveA += amountIn;
reserveB -= amountOut;
// Post-condition: constant product invariant
require(reserveA * reserveB >= k, "Invariant violated");
}
Alternative Mitigations
Use an Explicit State Machine Pattern
For contracts with lifecycle phases, enforce valid transitions:
enum State { Created, Active, Paused, Finalized }
State public currentState;
modifier onlyInState(State required) {
require(currentState == required, "Invalid state");
_;
}
function activate() external onlyOwner onlyInState(State.Created) {
currentState = State.Active;
}
function finalize() external onlyOwner onlyInState(State.Active) {
// Read current values before writing
uint256 currentBalance = totalDeposited;
require(currentBalance > 0, "Nothing to finalize");
currentState = State.Finalized;
_distributeRewards(currentBalance);
}
Batch Updates with Snapshot Validation
For batch operations, snapshot the state before the loop and validate after:
function batchTransfer(address[] calldata to, uint256[] calldata amounts) external {
uint256 totalBefore = _totalBalance();
for (uint256 i = 0; i < to.length; i++) {
balances[msg.sender] -= amounts[i];
balances[to[i]] += amounts[i];
}
// Post-loop invariant: total balance unchanged
require(_totalBalance() == totalBefore, "Balance invariant");
}
Common Mistakes
Mistake: Checking Invariant Only at Function End
function complexOperation() external {
balances[a] -= x;
// 50 lines of code with external calls...
balances[b] += x;
require(invariantHolds(), "Failed"); // Callback already saw broken state
}
Check invariants immediately after each correlated write, not only at the function end. External calls between writes can observe the intermediate (broken) state.
Mistake: Writing Without Reading Current State
function setConfig(uint256 newFee, uint256 newCap) external onlyOwner {
fee = newFee; // Blind write -- no validation against current state
cap = newCap; // No check that fee < cap
}
Read and validate the relationship between correlated parameters:
function setConfig(uint256 newFee, uint256 newCap) external onlyOwner {
require(newFee <= newCap, "Fee exceeds cap");
fee = newFee;
cap = newCap;
}