State Specifiers and STATICCALL
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 forconstant
, 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.
/// @dev Calculates the maximum amount of vested tokens
/// @return Number of vested tokens to withdraw
function calcMaxWithdraw()
public
view
returns (uint)
{
uint maxTokens = (token.balanceOf(this) + withdrawnTokens) * (now - startDate) / disbursementPeriod;
if (withdrawnTokens >= maxTokens || startDate > now)
return 0;
return maxTokens - withdrawnTokens;
}
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 17 will add a STATICCALL opcode that enforces read-only calls at runtime. Solidity only implements the STATICCALL opcode in its assembly language. Adding pure
and 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.
Like this piece? Sign up here for the ConsenSys weekly newsletter.
Disclaimer: The views expressed by the author above do not necessarily represent the views of Consensys AG. ConsenSys is a decentralized community with ConsenSys Media being a platform for members to freely express their diverse ideas and perspectives. To learn more about ConsenSys and Ethereum, please visit our website.