Skip to content

Ambiguous Evaluation Order

Smart contract code must be unequivocally clear regarding the evaluation order of variables. Statements mustn't leave room for ambiguity in the sequence of variable evaluation, as inconsistent outcomes can arise from such uncertainty. The Solidity documentation states the following:

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.

The consequence of this quote is that functions evaluated in Solidity are not evaluated in a fully deterministic way. While the output is deterministic for a particular Solidity compiler version, it may not remain consistent across different versions. This inherent ambiguity can become problematic when multiple functions independently affect shared stateful objects and are invoked within the same statement. Depending on the sequence in which these functions are evaluated, the final outcome of the statement could vary. An example is given by Mikhail Vladimirov on the Ethereum StackExchange:

1
2
3
4
function foo () public pure returns (uint) {
  uint x = 5;
  return x * x++; // Could be 25 or 30
}

Adding to the complexity, instructions like addmod and mulmod, as well as events, generally diverge from the standard pattern of evaluation order. Consequently, any code that incorporates these instructions might yield unexpected outcomes.

The EthTrust Security Levels Specification provides an example underlining the risk posed by the ambiguous evaluation order: If functions g and h manipulate any variable that the outcome of function f relies on, an invocation like f(g(x), h(y)) cannot be assured to consistently return the same results.

To mitigate this vulnerability, a prevalent strategy involves storing intermediate results in temporary variables. This method ensures that the evaluation order remains unambiguous, 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);
    }
}

In this example, the deterministicResult function explicitly sets the evaluation order by storing the result of the firstTransform function in a temporary variable, firstResult. It then passes firstResult into secondTransform, thereby assuring a predictable sequence of execution, regardless of the side effects within each transformation function.