Formal Specification and Verification of Solidity Contracts with Events

05/20/2020 ∙ by Ákos Hajdu, et al. ∙ Budapest University of Technology and Economics SRI International 0

Events in the Solidity language provide a means of communication between the on-chain services of decentralized applications and the users of those services. Events are commonly used as an abstraction of contract execution that is relevant from the users' perspective. Users must, therefore, be able to understand the meaning and trust the validity of the emitted events. This paper presents a source-level approach for the formal specification and verification of Solidity contracts with the primary focus on events. Our approach allows specification of events in terms of the on-chain data that they track, and predicates that define the correspondence between the blockchain state and the abstract view provided by the events. The approach is implemented in solc-verify, a modular verifier for Solidity, and we demonstrate its applicability with various examples.

READ FULL TEXT VIEW PDF
POST COMMENT

Comments

There are no comments yet.

Authors

page 1

page 2

page 3

page 4

This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

0.1 Introduction

Ethereum is a public, blockchain-based computing platform supporting the development of decentralized applications [11]. The core of such applications are programs – termed smart contracts [10] – deployed on the blockchain. While Ethereum nodes run a low-level virtual machine (EVM [11]), smart contracts are usually written in a high-level, contract-oriented language, most notably Solidity [9]. The contract code can be executed by issuing transactions to the network, which are then processed by the participating nodes. Results of a completed transaction are provided to the issuing user, and other interested parties observing the contract, through transaction receipts. While the blockchain is publicly available for users to inspect and replay the transactions, the contracts can communicate important state changes, including intermediate changes, by emitting events [1]. Events usually represent a limited abstract view of the transaction execution that is relevant for the users, and they can be read off the transaction receipts. The common expectation is that by observing the events, the user can reconstruct the relevant parts of the current state of the contracts. Technically, events can be viewed as special triggers with arguments that are stored in the blockchain logs. While these logs are programmatically inaccessible from contracts, the users can easily subscribe to and observe the events with the accompanying data. For example, a token exchange application can monitor the current state of token balances by tracking transfer events in the individual token contracts.

Smart contracts, as any software, are also prone to bugs and errors. In the Ethereum context, any flaws in contracts come with potentially devastating financial consequences, as demonstrated by various infamous examples [2]. While there has been a great interest in applying formal methods to smart contracts [2, 4]

, events are usually considered merely a logging mechanism that is not relevant for functional correctness. However, since events are a central state-change notification mechanism for users of decentralized applications, it is crucial that the users are able to understand the meaning and trust the validity of the emitted events. In this paper, we propose a source-level approach for the formal specification and verification of Solidity contracts with the primary focus on events. Our approach provides in-code annotations to specify events in terms of the blockchain data they track, and to declare events possibly emitted by functions. We verify that (1) whenever tracked data changes, a corresponding event is emitted, and (2) an event can only be emitted if there was indeed a change. Furthermore, to establish the correspondence between the abstract view provided by events and the actual execution, we allow events to be annotated with predicates (conditions) that must hold before or after the data change. We implemented the proposed approach in the open-source

111https://github.com/SRI-CSL/solidity/tree/merge solc-verify [7, 6] tool and demonstrated its applicability via various examples. solc-verify is based on modular program verification, but we present our idea in a more general setting that can serve as a building block for alternative verification approaches.

0.2 Background

Solidity [9] is a high-level, contract-oriented programming language supporting the rapid development of smart contracts for the Ethereum platform. We briefly introduce Solidity by restricting our presentation to the aspects relevant for events. An example contract (Registry) is shown in Figure 1. Contracts are similar to classes in object-oriented programming. A contract can define additional types, such as the Entry struct in the example, consisting of a Boolean flag and an integer data. The persistent data stored on the blockchain can be defined with state variables. The example contract declares a single variable entries, which is a mapping from addresses to Entry structs. Contracts can also define events including possible arguments. The example declares two events, new_entry and updated_entry, to signal a new or an updated entry, respectively. Both events take the address and the new value for the data as their arguments. Finally, functions are defined that can be called as transactions to act on the contract state. The example defines two functions: add and update. The add function first checks with a require that the data corresponding to the caller address (msg.sender) is not yet set. If the condition of require does not hold, the transaction is reverted. Otherwise, the function sets the data and the flag, and emits the new_entry event. The update function is similar to add, with the exception that the data must already be set, and the new value should be larger than the old one (for illustrative purposes).

Note that Solidity puts no restrictions on the emitted events, and a faulty (or malicious) contract could both emit events that do not correspond to state changes or miss triggering an event on some change [5], potentially misleading users. In the case of the Registry contract, the events are emitted correctly, and the user can reproduce the changes in entries by relying solely on the emitted events and their arguments.

contract Registry {
  struct Entry { bool set; int data; } // User-defined type
  mapping(address=>Entry) entries; // State variable
  /// @notice tracks-changes-in entries
  /// @notice precondition !entries[at].set
  /// @notice postcondition entries[at].set && entries[at].data == value
  event new_entry(address at, int value);
  /// @notice tracks-changes-in entries
  /// @notice precondition entries[at].set && entries[at].data < value
  /// @notice postcondition entries[at].set && entries[at].data == value
  event updated_entry(address at, int value);
  /// @notice emits new_entry
  function add(int value) public {
    require(!entries[msg.sender].set);
    entries[msg.sender].set = true;
    entries[msg.sender].data = value;
    emit new_entry(msg.sender, value);
  }
  /// @notice emits updated_entry
  function update(int value) public {
    require(entries[msg.sender].set && entries[msg.sender].data < value);
    entries[msg.sender].data = value;
    emit updated_entry(msg.sender, value);
  }
}
Figure 1: An example contract illustrating Solidity events. Users of the contract can associate an integer value to their address and can later update it with a larger value.

solc-verify [7] is a source-level verification tool for checking functional correctness of smart contracts. solc-verify takes contracts written in Solidity and provides various in-code annotations to specify functional behavior (e.g., pre- and postconditions, invariants). solc-verify translates the annotated contracts to the Boogie Intermediate Verification Language (IVL) and uses the Boogie verifier [3] to perform modular verification by discharging verification conditions to SMT solvers. This paper presents extensions to the specification and translation capabilities of solc-verify that enable reasoning about Solidity events. We propose event-specific annotations (Section 0.3) and use them to instrument the code during translation with additional conditions to be verified (Section 0.4).

0.3 Specification of Events

Our approach provides in-code annotations to specify events in terms of the on-chain data that they track for changes. Furthermore, additional predicates can specify the correspondence between the abstract view provided by events and the actual data, before and after the change.

Data changes and checkpoints.

Each event can declare a set of contract state variables that it tracks for changes. In the Registry example (Figure 1), both events track the single state variable entries, as specified by the tracks-changes-in annotations. Intuitively, we use the tracking of changes to make sure that (1) if a tracked variable changes, a corresponding event must be emitted after; and (2) an event should be emitted only if some of its tracked variables have changed before. As data changes often occur in multiple steps (e.g., updating both members of a struct in the function add of Figure 1), or conditionally, events cannot always be emitted directly after a single modifying statement. Therefore, we define the precise semantics of “before” and “after” by introducing before- and after-checkpoints. Before-checkpoints of an event are determined dynamically by the first change in a variable they track. In contrast, after-checkpoints are defined by static barriers, marking the latest point in code where the emitting should be fulfilled. Currently, we define loop and transaction boundaries (external calls to public functions and function return) as after-checkpoints. The semantics of checkpoints is that an event corresponding to a state variable change must be emitted at some point between before- and after-checkpoints, which also clears the before-checkpoint. Conversely, an event can only be emitted if a tracked variable indeed changed (there was a before-checkpoint).

Event pre- and postconditions.

In addition to the set of tracked variables, events can also be annotated with predicates that define conditions over the state variables and the arguments of the event. There are two kinds of predicates: pre- and postconditions. Preconditions capture the values of state variables at the before-checkpoint, while postconditions correspond to the point when the event is emitted. In the Registry contract (Figure 1), both events (new_entry and updated_entry) have the same postcondition, namely that the data at the given address must be set and its value must match the value in the argument. The precondition of new_entry is that the data must not yet be set, while for updated_entry, it must be set and its value should be smaller than the event argument. Postcondition expressions often need to connect the state at the point of emit and before the change. As an example, consider the transfer function of the token contract in Figure 2 that deducts the sender’s balance and increases the receiver’s. To specify the postcondition of the Transfer event, we need to relate the new balances to the previous balances. We provide a special before function – to be used in postconditions – that refers to previous values of state variables.

Functions.

We require contract functions to be annotated with the events that they possibly emit using the emits keyword. For example, the add and update functions in Figure 1 can emit new_entry and updated_entry respectively. If a function calls other functions (including base constructors), the callee’s emitted events must also be included in the caller’s specifications.

contract Token {
  mapping(address=>uint) balances;
  /// @notice tracks-changes-in balances
  /// @notice precondition balances[from] >= amount
  /// @notice postcondition balances[from] == before(balances[from]) - amount
  /// @notice postcondition balances[to] == before(balances[to]) + amount
  event Transfer(address from, address to, uint amount);
  /// @notice emits Transfer
  function transfer(address to, uint amount) public {
    require(balances[msg.sender] >= amount && msg.sender != to);
    balances[msg.sender] -= amount;
    balances[to] += amount;
    emit Transfer(msg.sender, to, amount);
  }
}
Figure 2: A token contract illustrating event postconditions that refer to previous state.

0.4 Verification

A contract with events and specifications is checked in two steps. First, a syntactical check is performed to ensure that functions only emit events that they specified (via emits annotations). Then, we check the data tracking specifications and predicates by translating the contract to the input language of a verifier and instrumenting the code with the checks and the required bookkeeping. In our implementation, we use the Boogie IVL and verifier [3], but we present our solution in a general way that can be reused in other Solidity verifiers.

Function emits.

We first check whether functions only emit those events that are specified via emits annotations. This is a syntactic check on the Solidity AST: we find all emit statements in the function and check whether the corresponding events are specified to be emitted. When a function calls other functions internally (i.e., from the same contract), we apply a modular check based on the call graph: all events specified to be emitted by the callee must also be specified by the caller. On the other hand, we currently ignore external calls (such as .call() or .transfer()). Such external calls cannot modify state variables or trigger events from the current contract directly (as they are non-public). Indirect modifications and emits are possible by calling back public functions, but those are specified and checked independently (modularity of reasoning). Finally, we also verify at each assignment (to a tracked variable), whether the function specifies a corresponding event to be emitted.

Data tracking and predicates.

Verification of data tracking and predicates is performed by instrumenting the contract code with additional variables and statements to save state and to make extra checks at checkpoints. For clarity, we describe the instrumentation on the Solidity level. We illustrate the approach through the example contract in Figure 3, which has two state variables x and y, and whenever one of them changes, an event is emitted with their current difference. Furthermore, x <= y should hold both at the before- and the after-checkpoint. The extra instructions are displayed as labels where they are injected, while the corresponding code can be found in the snippets to the right.

contract C {
  uint x;
  uint y;
  /// @notice tracks-changes in x
  /// @notice tracks-changes in y
  /// @notice precondition x <= y
  /// @notice postcondition x <= y
  /// @notice postcondition x + diff == y
  event xy_changed(uint diff);
  /// @notice emits xy_changed
  function f(uint n) public {
    require(x <= y);
    y += n;
    emit xy_changed(y - x);
    for (uint i = 0; i < n; ++i) {
      x++;
      emit xy_changed(y - x);
    }
  }
}

uint x_old; // Previous state of x
uint y_old; // Previous state of y
bool x_mod; // Modified since last checkpoint
bool y_mod; // Modified since last checkpoint

new-vars

new-vars

require(!x_mod && !y_mod); // Modif. clear

assume-clear

assume-clear

assert(!x_mod && !y_mod); // Modif. clear

after-chpt

after-chpt

after-chpt

after-chpt

// Save y if not saved yet: before-checkpt
if (!y_mod) { y_old = y; y_mod = true; }

y-before

y-before

assert(x_mod || y_mod); // Emit without change
assert((x_mod?x_old:x) <= (y_mod?y_old:y)); // Pre
assert(x <= y); // Post
assert(x + (y - x) == y); // Post
x_mod = y_mod = false; // Emitted

emit-spec

emit-spec

emit-spec

// Save x if not saved yet: before-checkpt
if (!x_mod) { x_old = x; x_mod = true; }

x-before

x-before
Figure 3: Example contract with instrumentation snippets for checking event specifications.

For each state variable that is tracked by any event, we introduce two additional variables in the contract: one with the same type to save the before-state, and a Boolean flag to indicate whether the data has been modified (snippet new-vars in Figure 3). Functions are then instrumented with extra statements to save state, enforce after-checkpoints (barriers) and to perform specification checks when events are emitted. Functions ensure on entry that none of the variables tracked by their specified events have been modified since the checkpoint before the call (snippet assume-clear). In other words, all relevant events must have been emitted before making the call. In modular verification, this assumption becomes a precondition to the function. Before each modification (assignment statement), if the state variable is not modified yet, the current value is stored222Saving data (e.g., mappings) with assignments might not yield valid Solidity code. This code is for clarity of presentation and is handled by solc-verify internally. in the helper variable and the flag for modification is set, introducing a before-checkpoint (snippets y-before and x-before).

At each emit statement, several checks are added (snippet emit-spec). First, we check that the data has indeed been modified, otherwise the event should not be emitted. Then we check each pre- and postcondition. By default, preconditions refer to the before-state and postconditions to the current values, except if the variable is explicitly wrapped with before(). Note that we refer to the previous value of a variable v with v_mod ? v_old : v because in general there might be variables that were not modified (e.g., x at the first emit in Figure 3). After performing the checks, emitting the event clears the flags (before-checkpoints). Finally, before returning, functions enforce after-checkpoints by asserting that no state variable is in a modified state, i.e., the function cannot end in debt with events (snippet after-chpt). In modular verification, this check becomes a postcondition to the function. We also insert an after-checkpoint before the loop and at the end of every iteration (serving as loop invariant).

Discussion.

One potential limitation of our approach is that we consider loop boundaries after-checkpoints: some contracts change the data many times in the loop but only emit a single summarizing event after the loop. This limitation could be alleviated with annotations to “allow delaying” the emit after the loop, but we do not support this as it leads to more complex specification and verification.

Our approach is not tied to Boogie or modular verification. The instrumentation can be performed on the Solidity level, and the correctness of the specification is reduced to checking assertions at particular points in the code. This means that the instrumented code can be fed into any Solidity verifier that can check for assertion failures. The event specifications are deemed correct if and only if there are no related assertion failures.

A possible future use-case of our approach lies in the behavioral analysis of contracts based on logs. Such analyses could reveal relationships individually and across contracts that are not otherwise apparent (e.g., exposing entities that control the blockchain interactions) or attack evidence. Application-level log analysis has been used for a long time for monitoring and security purposes, and most existing techniques assume that application logs can be trusted or, if applications are subverted by attackers, the subversion can be captured [8]. Our approach guarantees the validity of the emitted events, making them even more suitable for such analysis.

0.5 Conclusion

We presented an approach for the formal specification and verification of Solidity smart contracts that rely on events to communicate with their users, providing an abstract view of their state. We proposed in-code annotations to specify events in terms of the state variables they track for changes. Furthermore, we introduced additional predicates (pre- and postconditions) for specifying conditions on the state before and after the change, establishing the correspondence between the blockchain state and the emitted events. The approach is implemented in solc-verify and we demonstrated its applicability with various examples.

References

  • [1] (2016) A guide to events and logs in Ethereum smart contracts. Note: https://consensys.net/blog/blockchain-development/guide-to-events-and-logs-in-ethereum-smart-contracts Cited by: §0.1.
  • [2] N. Atzei, M. Bartoletti, and T. Cimoli (2017) A survey of attacks on Ethereum smart contracts. In Principles of Security and Trust, Lecture Notes in Computer Science, Vol. 10204, pp. 164–186. External Links: Document Cited by: §0.1.
  • [3] M. Barnett, B. E. Chang, R. DeLine, B. Jacobs, and K. R. M. Leino (2006) Boogie: a modular reusable verifier for object-oriented programs. In Formal Methods for Components and Objects, LNCS, Vol. 4111, pp. 364–387. Cited by: §0.2, §0.4.
  • [4] H. Chen, M. Pendleton, L. Njilla, and S. Xu (2019) A survey on Ethereum systems security: vulnerabilities, attacks and defenses. External Links: Link Cited by: §0.1.
  • [5] J. Chen, X. Xia, D. Lo, J. Grundy, X. Luo, and T. Chen (2020) Defining smart contract defects on Ethereum. IEEE Transactions on Software Engineering. Note: Early access Cited by: §0.2.
  • [6] Á. Hajdu and D. Jovanović (2020) SMT-friendly formalization of the solidity memory model. In Programming Languages and System, Lecture Notes in Computer Science, Vol. 12075, pp. 224–250. External Links: Document Cited by: §0.1.
  • [7] Á. Hajdu and D. Jovanović (2020) Solc-verify: a modular verifier for Solidity smart contracts. In Verified Software. Theories, Tools, and Experiments, Lecture Notes in Computer Science, Vol. 12301, pp. 161–179. External Links: Document Cited by: §0.1, §0.2.
  • [8] S. Ma, J. Zhai, F. Wang, K. H. Lee, X. Zhang, and D. Xu (2017) MPI: multiple perspective attack investigation with semantic aware execution partitioning. In Proceedings of the 26th USENIX Security Symposium, pp. 1111–1128. External Links: Link Cited by: §0.4.
  • [9] (2020) Solidity documentation. Note: https://solidity.readthedocs.io Cited by: §0.1, §0.2.
  • [10] N. Szabo (1994)(Website) Cited by: §0.1.
  • [11] G. Wood (2019) Ethereum: a secure decentralised generalised transaction ledger. Note: https://ethereum.github.io/yellowpaper/paper.pdf Cited by: §0.1.