Skip to content

Ambiguous Evaluation Order

Statements that leave the sequence of variable evaluation open produce inconsistent outcomes. The Solidity documentation states:

The evaluation order of expressions is not specified (more formally, the order in which the children of one node in the expression tree are evaluated is not specified, but they are of course evaluated before the node itself). It is only guaranteed that statements are executed in order and short-circuiting for boolean expressions is done.

Expressions in Solidity are therefore not evaluated in a fully deterministic way. The output is deterministic for a particular compiler version, but nothing guarantees it across versions. The ambiguity becomes a problem when multiple functions independently affect shared stateful objects and are invoked within the same statement, because the final outcome of the statement varies with the sequence in which those functions are evaluated. Mikhail Vladimirov gives an example on the Ethereum StackExchange:

1
2
3
4
5
function foo () public pure returns (uint) {
  uint x = 5;
  // @audit x is read and incremented within the same expression
  return x * x++;
}

Instructions like addmod and mulmod, as well as events, diverge from the standard pattern of evaluation order, so code incorporating them yields outcomes that depend on the compiler.

The EthTrust Security Levels Specification underlines the risk with a second case. Where functions g and h manipulate any variable that the outcome of function f relies on, an invocation like f(g(x), h(y)) returns no consistent result.

The remedy is to store intermediate results in temporary variables, which fixes the evaluation order regardless of compiler variations or complex functional interactions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// SPDX-License-Identifier: MIT
// Source: EEA EthTrust Security Levels Specification

pragma solidity 0.8.18;

contract EvaluationOrder {
    uint256 public myNumber;
    uint256 public yourNumber;

    function firstTransform(uint256 someNumber) public returns (uint256) {
        myNumber += 1; // Side effect
        return someNumber * myNumber;
    }

    function secondTransform(uint256 someNumber) public returns (uint256) {
        yourNumber += 3; // Side effect
        return someNumber / yourNumber;
    }

    function deterministicResult(uint256 someNumber) public returns (uint256) {
        // Using a temporary variable to ensure consistent evaluation order
        uint256 firstResult = firstTransform(someNumber);
        return secondTransform(firstResult);
    }
}

The deterministicResult function sets the evaluation order explicitly by storing the result of firstTransform in the temporary variable firstResult and passing that into secondTransform. The sequence of execution is then predictable regardless of the side effects inside each transformation.