DeepAI
Log In Sign Up

Solidity 0.5: when typed does not mean type safe

The recent release of Solidity 0.5 introduced a new type to prevent Ether transfers to smart contracts that are not supposed to receive money. Unfortunately, the compiler fails in enforcing the guarantees this type intended to convey, hence the type soundness of Solidity 0.5 is no better than that of Solidity 0.4. In this paper we discuss a paradigmatic example showing that vulnerable Solidity patterns based on potentially unsafe callback expressions are still unchecked. We also point out a solution that strongly relies on formal methods to support a type-safer smart contracts programming discipline, while being retro-compatible with legacy Solidity code.

READ FULL TEXT VIEW PDF

page 1

page 2

page 3

page 4

09/25/2020

A formal model of Algorand smart contracts

We develop a formal model of Algorand stateless smart contracts (statele...
04/13/2019

Flint for Safer Smart Contracts

The Ethereum blockchain platform supports the execution of decentralised...
03/12/2020

ÆGIS: Shielding Vulnerable Smart Contracts Against Attacks

In recent years, smart contracts have suffered major exploits, costing m...
08/04/2022

Deductive Verification of Smart Contracts with Dafny

We present a methodology to develop verified smart contracts. We write s...
09/08/2019

Obsidian: Typestate and Assets for Safer Blockchain Programming

Blockchain platforms are coming into broad use for processing critical t...
09/02/2020

zkay v0.2: Practical Data Privacy for Smart Contracts

Recent work introduces zkay, a system for specifying and enforcing data ...
03/15/2021

Compositional Security for Reentrant Applications

The disastrous vulnerabilities in smart contracts sharply remind us of o...

1 Introduction

Over the last few years the execution of smart contracts on the blockchain has emerged as a form of distributed programming of a global computer. Anyone can deploy a global service, encoded as a smart contract, that can be used by mutually untrusted parties to “safely” interact with no need of a central authority. Therefore it is of paramount importance that the intended interaction provided by the service is “correctly” implemented by the code of the corresponding contract. Indeed, while the term contract is generally used to refer to an interaction that is intended to be enforced by law, a smart contract on the blockchain is intended to be automatically enforced: the law is embodied by the code to be executed (see the TheDAO affair [1]).

Formal methods have a long tradition of successes in dealing with the subtle mismatches between program specification and code implementation, and they can be helpful also in the new context of smart contracts. Here we focus on Solidity, the most widely used programming language in Ethereum’s ecosystem, and on formal methods that provide support for a safer programming discipline by acting directly at the programming language level. In particular, since Solidity is a statically typed language, we foster the use of types as a tool to shape and substantiate the programmer’s reasoning. However, static typing conveys an effective programming discipline only if type constraints are actually enforced by the compiler. In other terms, there is a gap between the definition of types in a language and their type-safe usage. We show below that this is precisely the case of the last release of Solidity 0.5. Indeed, the newly-introduced type address payable is intended to prevent Ether transfers to smart contracts that are not supposed to receive money, but the compiler fails to enforce such semantics. In other words, the type soundness of Solidity 0.5 is no better than that of the previous release.

Formal methods and the theory of typed languages show the way to bridge that gap and develop a statically typed language that is also type-safe. In particular, since Solidity contracts are reminiscent of class-based objects in distributed Object-Oriented Languages, it is worth to study how the rich and well-known theory of OOLs can be reused and adapted to smart contracts programming.

In a previous work we defined the Featherweight Solidity typed calculus ([3]), which formalizes the core of the Solidity language and the basic type soundness provided by its compiler (both versions 0.4 and 0.5). In that work we also proposed a refined typing that enjoys a stronger soundness property, but remains retro-compatible with legacy Solidity code. That typing ensures safer accesses to contracts through their address; hence it statically prevents a general class of runtime errors. We show here that the unsafe usage of the address payable type can be statically captured by the refined type system put forward in [3]. Therefore, it represents a solution to the soundness issue of Solidity 0.5 and supports an effective smart contract programming discipline using the compiler as a convenient building tool.

2 The problem

As in class-based Object-Oriented Languages, the declaration of a Solidity contract defines a contract type . However, instances of such a contract are often referred to by the Solidity code through expressions of type address, that essentially represent an untyped way to access them. Such expressions must then be cast to the type in order to call the functions provided by the contract . Casting an untyped pointer is notoriously a very flexible but subtle feature requiring programmers to precisely know what pointers refer to. Solidity’s compiler provides no help here: neither static or dynamic checks are performed on cast expressions, and a dynamic error is raised only when calling a function (or accessing a state variable) that is not provided by the underlying contract.

Two features of Solidity make this problem pervasive in the code of smart contracts. First of all, in Ethereum the instances of smart contracts deployed on the blockchain can only be accessed through their public address. Secondly, contract functions make extensive use of their implicit variables this and msg.sender, that are dynamically bound to the contract instance being executed and the address of the caller contract, respectively. Therefore, while the callee is referred to through a typed pointer (as in OOLs), the caller is referred to through an untyped one. Hence, even though usual method recursion is type-safe, all the callback expressions undergo potentially unsafe usages. Indeed, besides the dangerous casts described above, a typical Solidity pattern consists in calling to send Ether from the balance of the callee to that of the caller. However, such a transfer implicitly calls the fallback function of the contract referred to by msg.sender, thus raising a dynamic error if such function has not been defined by that contract.

To mitigate this problem, the last release of Solidity (i.e. version 0.5 [2]) distinguishes two types, address and address payable, where the second one denotes addresses pointing to contracts that declare the fallback function. Ideally, by using the new type address payable, Solidity 0.5 intends to statically prevent at least the unsafe money transfers, that are actually the most common form of the dynamic errors described above. It is worth to observe that these errors, that in OOLs are known as message-not-understood, are particularly harmful in the context of the blockchain. Indeed, in Ethereum the occurrence of a dynamic error causes the initial transaction to be interrupted and rolled-back (the so-called revert). This makes the account that issued that transaction lose the money it paid to the miner node and possibly leads to Ether indefinitely locked into a contract’s balance. Hence, there is a pressing requirement to issue a transaction only if it can be statically guaranteed that it will not evolve to a revert.

Unfortunately, Solidity 0.5 fails to prevent unsafe money transfers at compile- time. As a matter of fact, no type check is enforced by the compiler to ensure that a variable of type address payable is substituted with the address of a contract that actually provides a fallback function. The problem can be detected with a careful read of the documentation111 https://solidity.readthedocs.io/en/v0.5.9/050-breaking-changes.html , which states:

It might very well be that you do not need to care about the distinction between address and address payable and just use address everywhere. For example, if you use the withdraw pattern you most likely do not have to change your code because transfer is only used on msg.sender instead of stored addresses and msg.sender is an address payable.
[…] Address literals can be implicitly converted to address payable.
[…] In external function signatures address is used for both the address and the address payable type.

1pragma solidity >=0.5.0 <0.7.0;
2
3contract WithoutFallback {
4  Test _test;
5
6  constructor (address _unsafeAddress) payable public {
7      _test = Test(_unsafeAddress);
8  }
9
10  function callUnsafeContract() external {
11      _test.foo();
12  }
13
14  function testUnsafeCast() external {
15     address _addr = address(_test);
16     //_addr.transfer(10); // DOES NOT COMPILE
17     address payable _payAddr = address(uint160(_addr));
18     _payAddr.transfer(10);
19    }
20}
21
22contract Test {
23  constructor () payable public {}
24
25  function foo() external {
26      msg.sender.transfer(10);
27  }
28}
Figure 1: Counterexample to the type safety of Solidity 0.5

Concretely, the counterexample in Figure 1 shows that the implicit variable msg.sender is assumed to be of type address payable, but no check is performed on the type of the actual caller’s address. More precisely, the expression in the body of the function of the contract Test (line 26) correctly compiles, and so does the call of this function from the contract WithoutFallback (line 11). However, issuing a transaction that invokes the function callUnsafeContract of WithoutFallback results in a revert as that contract cannot receive money back from the contract Test. The same problem occurs if the functions are marked public or private instead of external. Furthermore, in order for the contract WithoutFallback to refer to a deployed instance of the contract Test, its constructor can only accept a parameter of address type and then cast it to the expected contract type (line 7). Even if nothing ensures that the actual parameter refers to an instance of Test

, the cast expression correctly compiles and correctly executes, postponing the dynamic check to the moment where the

_test reference is actually used (line 11). The constructor’s parameter _unsafeAddress could also be of type address payable. In this case, one might expect the compiler to check that, when casting a payable address to a contract type, the target type of the cast (i.e. Test) at least defines a fallback function. Again, this is not true. No check is performed, either at compile-time or at run-time, to ensure that Test respects the constraints that address payable is supposed to impose.

The example also shows (in function testUnsafeCast) that the transfer primitive can be correctly used only on addresses of static type address payable, but the type constraint can be circumvented by resorting to an intermediate cast to the type uint160, as explicitly stated by the official documentation. Clearly, the expression _payAddr.transfer(10) at line 18 dynamically raises an error since there is no fallback function in the Test contract.

We tested the code in Figure 1 with Remix, the online Ethereum IDE, using the version 0.5.9+commit.e560f70d of the Solidity compiler.

Money transfers that dynamically lead to errors were possible since the first release of Solidity, so the new version has not introduced a new problem. On the other hand, the addition of the new type address payable to capture the (addresses of) contracts that can “safely” receive Ether, generates into programmers the expectation that “safely” means type-safely, that is the compiler will check it. In fact nothing has actually changed w.r.t. version 0.4: the new type essentially provides only a refined documentation about addresses, but programmers have certainly more confounded expectations.

3 The solution

The typed theory of programming languages allows to identify a type preservation issue in Solidity 0.5’s type system, confirmed by the code in Figure 1, and also offers a solution. In a previous work ([3]) we developed a precise formalization of the core of the Solidity language and its type system. We resorted to a formalization style that is reminiscent of the well known Featherweight Java language [4], highlighting the similarities between the notions of object and smart contract. Along with a precise definition of the basic type-soundness provided by the Solidity compiler, we proposed a refined type system that enjoys a stronger soundness property. In particular, that typing solves the type preservation problem pointed out here. Furthermore, the solution put forward in [3] is general enough to statically prevent not just unsafe calls to a non existent fallback function, but all the message-not-understood errors arising from unsafe casts from addresses to contract types.

The key idea is twofold. First, the type address is refined with type information about the contract it refers to. That is, is the type of the addresses of instances of the contract , or of a contract that inherits form . In particular, assuming a dummy contract that only contains a fallback function with an empty body, the type has the same meaning of Solidity 0.5’s address payable. Indeed, it is the (super-)type of the addresses of every contract that can safely accept money transfers.222In [3] we proposed the keyword payableaddress as a syntactic sugar for the type , since at the time of writing we were not aware of Solidity 0.5.

The second idea is to enrich functions’ signatures with the maximum type allowed for the caller, so that functions can only be invoked by contracts with an expected (super-)type. Adding a type constraint for the caller in function signatures is essential to safely type the implicit msg.sender parameter, thus to guarantee type preservation. The compiler can then statically check potentially unsafe callback expressions, such as or
C(msg.sender).foo(), that reduce to a revert if msg.sender is bound to the address of a contract that has no fallback function or does not have type , respectively.

The counterexample in Figure 1 can be fixed by choosing a suitable refined signature for the foo function of the contract Test. As the only requirement for the caller is to provide a fallback function, it is sufficient to amend the function’s code as follows:

function foo() <> external {
   msg.sender.transfer(10);
}

In the body of the function, the variable msg.sender is then assumed to have type , hence the call to transfer is now well typed. On the other hand, the compiler prevents the unsafe money transfer by identifying a type error in the function call at line 11, since the caller’s type, WithoutFallback, is not a subtype of . As a further example, the following function, whose refined signature specifies the expected (super-)type of the caller, could be safely added to the Test contract:

function boo() <WithoutFallback> external {
   WithoutFallback(msg.sender).testUnsafeCast();
}

To simplify the notation, and in line with the Solidity programming style, in [3] we proposed a syntactic sugar based on a new function marker, payback, for functions whose caller must simply provide a fallback function (which is the most common case). In this way the foo function inside the Test contract would simply become as follows:

function foo() payback external {
   msg.sender.transfer(10);
}

Similarly, the standard function signature with no annotation could correspond to assuming the (super-)type , that is no constraint for the caller.

Further details about the formalization of this idea, its type-soundness, and its retro-compatibility with Solidity contracts already deployed on the blockchain can be found in [3]. We just observe here that, despite the usage of the convenient payback marker, to take advantage of the full power of the refined typing the major effort required to Solidity programmers is to annotate their functions with the required (super-)type of the caller. Such a requirement might be verbose, but it actually supports a safer programming discipline, where types mirror the programmer’s reasoning and the compiler can be effectively used as a convenient building tool.

References