Solidity Learning: State Specifiers and STATICCALL

Upcoming Changes to Solidity and How They Function

Crosspost: This post was originally written by “Niran” of ConsenSys and can be found here. This was posted with his permission, enjoy!

One of the biggest barriers to writing and reviewing safe smart contracts is understanding when the state can be modified during a chain of function calls. Reentrancy vulnerabilities have been a huge cause for concern — attempting to simply read from another contract’s storage can lead to a loss of assets if done improperly. Both Solidity and the EVM are introducing features that make it safer to ask other contracts about their storage.

Solidity 0.4.16 added two new specifiers for contract functions: pure and view. Solidity 0.4.17 adds the ability to enforce those specifiers—if you attempt to read or write to storage or log events in the body of a function that claims not to do so, your contract will not compile. This is disabled by default for backwards compatibility.

  • pure was introduced to specify functions that do not read or modify the state of the blockchain, like math functions.
  • view was introduced to specify functions that can read, but can never modify the state of the blockchain. view is an alias for constant, an existing specifier that was not enforced by the compiler.

Use these specifiers to clarify the intended behavior of your functions. As you write your contracts, Solidity will suggest one of these state mutability specifiers when it detects functions that don’t read and/or write to the blockchain.

Beyond helping others understand your codebase, pure and view are important parts of the interface between Solidity contracts. If a function makes calls to other functions or other contracts, Solidity will check if the specifiers on those functions can violate the claims of the calling function.

In addition, pure and view affect the JSON ABI that the compiler produces. The stateMutability field for a function indicates whether the function is pure, view, nonpayable, or payable. Tools can access this data without parsing the contracts themselves to aid in generating reports or visualizations that make it easier to understand contracts.

Examples

simple-token-sale

The Disbursement contract in simple-token-sale allows tokens to vest gradually during a specified disbursement period. Disbursement.calcMaxWithdraw() calculates the number of tokens that can currently be withdrawn. In the current codebase the function is specified as constant. In terms of the new specifiers, it is a view function---it reads from a contract's state but it doesn't modify it.

If we remove the existing constant modifier in the codebase, Solidity will detect that the function is read-only:

Disbursement.sol:95:5: Warning: Function state mutability can be restricted to view
function calcMaxWithdraw()
^

Adding the view modifier as shown below makes it clear that the function only reads from the state, and allows the compiler to help us maintain that behavior through the lifetime of the codebase.

If we later modify the function and violate that specifier (intentionally or not), Solidity will warn us:

Disbursement.sol:102:9: Warning: Function declared as view, but this expression (potentially) modifies the state and thus requires non-payable (the default) or payable.
withdrawnTokens = 0;
^-------------^

To avoid overlooking this kind of behavior change, we can add pragma experimental "v0.5.0" to the file to make the compilation fail with an error instead of just logging a warning.

Note that calcMaxWithdraw calls token.balanceOf, a function that can contain arbitrary code that could write to the token contract’s storage and violate the view specifier on calcMaxWithdraw. Since the abstract Token class in this project specifies balanceOf as constant, Solidity knows it should be safe to call it within a view function. If we remove the constant specifier from balanceOf, Solidity will warn us (or throw a compilation error if enabled):

browser/Disbursement.sol:100:27: TypeError: Function declared as view, but this expression (potentially) modifies the state and thus requires non-payable (the default) or payable.
uint maxTokens = (token.balanceOf(this) + withdrawnTokens) * (now - startDate) / disbursementPeriod;
^-------------------^

zeppelin-solidity

The SafeMath contract in zeppelin-solidity provides math functions that throw exceptions to prevent integer overflows. It's a library contract without its own state. In the current codebase, these functions are specified as constant, which indicates that the function doesn't write to the state, but doesn't let you know if it reads from the state. In terms of the new specifiers, these are pure functions—they don't read or write any state.

Compiling SafeMath with the latest Solidity will throw several warnings like this one to let us know that the functions aren't just constant or view, they're pure:

SafeMath.sol:27:3: Warning: Function state mutability can be restricted to pure
function add(uint256 a, uint256 b) internal constant returns (uint256) {
^

Updating these modifiers to pure communicates the behavior more precisely:

function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}

STATICCALL

The Byzantium network upgrade scheduled for October 9 will add a STATICCALL opcode that enforces read-only calls at runtime. Solidity only implements the STATICCALL opcode in its assembly language. Adding pureand view specifiers does not change the opcode that will be used to call the function, so they only affect compiler errors, not any behavior on chain. In the future, calls to pure or view functions could be compiled as STATICCALL, ensuring that the developer’s expectations of immutability are never violated.

STATICCALL allows a subset of reentrancy vulnerabilities to be avoided: if a contract’s state change depends on reading data from another contract, it can safely retrieve it without ever triggering a conflicting state change. However, if your contract’s state change requires a successful state change in another contract, STATICCALL cannot be used, so you still need to take precautions against reentrancy.

STATICCALL introduces new scenarios that can cause transactions to fail, which is important to consider when calling arbitrary contracts. Solidity can detect state mutability violations within a single codebase, but third-party contracts that implement standard interfaces (like tokens or registries) live in separate codebases with their own definitions of those interfaces that might be more loosely defined than the standard. Mandating the use of ethpm for standard interfaces is one potential way to make sure we can rely on Solidity’s static analysis across codebases. Until then, keep your eyes open: with new behavior comes new vulnerabilities.


Do you love this stuff?

I’m part of the team at ConsenSys Diligence. If you have a knack for diving deeply into Solidity and the EVM, and an interest in smart contract security, we’re looking for people to join our smart contract audit practice (apply here).

If you made it this far in the post, but don’t meet the exact criteria in the job description, that’s OK. Just include a message mentioning this post, and calling out your experience with, and interest in Ethereum.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.