Skip to main content
Sigvex

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.

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;
}

References