• No results found

Two are the main additions made by this extension: annotating addresses with contract names and subtyping. The latter is a little more limited than Solidity’s one. It is not the aim of this extension to investigate and formalize the way Solidity handles subtyping. In fact, we did not include either multiple inheritance, abstract contracts, or interfaces. In FS+, inheritance is only used for the sake of retro-compatibility and to make it pos- sible a direct mapping from existing contracts to the new type system we are proposing. The rules STRUCTURAL FALLBACKactually introduces a very limited form of mul- tiple inheritance. In fact, a contract implementing a fallback function extends, at the same time, both the contract indicated in its declaration (i.e. contract C is D), by nom- inal subtyping, and Topfb by structural subtyping. Nonetheless, this form of multiple inheritance is a very controlled one, and no ambiguities can arise (since Topfbis used only to check a given property of C).

Consider now the new address type: addresshCi. It tells the compiler that a value of that type points to an instance of contract C, thus allowing it to inspect its interface. This is, of course, not compatible with Solidity, where address does not keep any infor- mation about the contract it refers to. Nonetheless, a direct default mapping is easily definable: remember the contract Top, defined as the base contract for any hierarchy. It is hence natural to map the type address to addresshTopi. This provides no guar- antees, since we actually still do not know which contract that address refers to. On the other hand, the new code written using this extension allows the compiler to make fine-grained checks. Thus, no changes are required to the already deployed contracts: only the compiler has to be made more complex (as described above) to deal with the additional binding. We will not be able to apply our fine-grained check to the existing contracts, that cannot be modified, but we will be able to improve the reliability of the new ones. We shall also provide a flag (--notopcast) in the new compiler to disable rule CONTRRETRin Section 7.4 when C = Top and allow programmers to compile using rule CONTRRETRin Section 6.3. We do this for the sake of retro-compatibility: CONTRRETR in Section 7.4 would rule out any cast having addresshTopi as actual type of e, since ∀D type . Top 6<: D. All the other casts will be correctly verified.

The major effort required to programmers is to annotate each function with the re- quired type of its sender. This somehow reduces the flexibility provided by default by Solidity, since programmers have to specify in advance, and without the possibility to change it, a maximum supertype for the caller of their functions. This might seem an- noying, but actually allows the definition of sounder contracts (as we saw above). One of the most known benefits of types is that they get programmers to know which oper- ations can be invoked on each value. The compiler then checks that every value is used according to its type, i.e. that only allowed operations are invoked on it. Types might sometimes be too rigid, but they are a fundamental tool to make the code clearer, more readable and more correct. On the other hand, the flexibility of dynamically-typed languages (where types are not checked at compile-time) is well-known and largely appreciated by many programmers. Nonetheless, such languages provide weaker guar- antees and allow for run-time errors, avoidable thanks to a compiler check. Even worse, such errors might appear in certain executions and might not in others, making it ex- tremely difficult to catch the bug. Catching (and correcting) bugs is almost impossible in Solidity, where contracts may not be redeployed and the only (extreme) solution to dangerous bugs is the suicide. The main problem in Solidity’s type safety is the type address, that can be compared to a pointer to void (void *) in C. Such pointers allow for an extreme flexibility, but are really difficult to deal with since the compiler

does not give any hint on how to use them: programmers have to know what they are doing and how to do so, in order to avoid subtle bugs. They are widely used to implement generic functions, when the precise target type is not known in advance. Nonetheless, the real question is: “Would you allow a void* to be cast to a com- pletely arbitrary type, without knowing if that cast will cause run-time errors”? The answer in C is yes, and that can lead to many bugs if not done carefully. Void pointers are a dangerous backdoor in C’s type safety.

address is similar: it can refer to an instance of any accounts, and neither Solidity nor the EVM provides additional information on that. As we saw above, this is quite dangerous and can lead to Ether indefinitely locked into a contract or to unexpected run- time reverts. Hence, the main benefit of specifying the most general required sender’s supertype in a function’s signature is that the compiler has a precise knowledge about the operations programmers wish to invoke on the contract the sender refers to. In the context of function safeCast above, the compiler blocks any attempts to call it from a contract not extending A, thus making sure that any successful invocation will not raise any reverts due to an unsuccessful cast. In other words, we ask programmers to specify the minimum required interface to accomplish the function’s postcondition. Such specification makes the address type safe again, and simplify the task of proving properties and invariants on Solidity’s smart contracts and their functions.

Functions in Solidity can, in general, be annotated in many ways: the visibility (external, internal, public, and private), payable, and an indication of what the body does not do (view if it does not modify the state or pure if it does not read the state). Using such markers on function signatures is a common practice in Solidity, and makes the code more readable. We provide the following two new annotations as a syntactic sugar:

• payback to indicate that the function possibly sends Ether to the sender. This annotation is translated requiring Topfbas a most general type;

• any to indicate that the function does not impose any restrictions to the sender. This annotation is translated requiring Top as a most general type, and corre- sponds to the default mapping of legacy code.

It will be then a compiler’s task to translate such annotation ensuring the constraints they correspond to. In a similar way, address is translated into address<Top> and payableaddressinto address<Topfb>.

Example 7.4 compares the current Solidity with our enhanced version.

Example 7.4 (Comparison on transfer safety). Let us consider the contract U nsaf e in Example 7.3. The same contract (omitting, for the sake of clarity safeCast) would be written in Solidity as in Listing 7.5. Listing 7.6, on the other hand, shows the very same contract without our modifications.

1 contract SafeWithdrawal { 2 mapping (payableaddress => uint) private balances; 3 4 function deposit() public payable payback { 5 balances[msg.sender ] += msg.value; 6 } 7 8 function withdraw() public payback { 9 uint b = balances[ msg.sender]; 10 balances[msg.sender ] = 0; 11 msg.sender.transfer (b); 12 } 13 }

Listing 7.5: Safe withdrawal in Solidity 1 contract UnsafeWithdrawal { 2 mapping (address => uint) private balances; 3 4 function deposit() public payable { 5 balances[msg.sender ] += msg.value; 6 } 7 8 function withdraw() public { 9 uint b = balances[ msg.sender]; 10 balances[msg.sender ] = 0; 11 msg.sender.transfer (b); 12 } 13 }

Listing 7.6: Unsafe withdrawal in Solidity

The code looks very similar, with a few differences. First, the key type of balances is payableaddress and not address anymore. This means that only addresses referring to a contract with a fallback function may deposit money in (and successively possibly withdraw from) the contract. Note that the key set space will remain the same: we cannot know in advance which address will correspond to a suitable contract, and thus the set of keys of the two mappings (the one in Listing 7.5 and the one in List- ing 7.6) will be the same. Some keys will never be used in the former, but this is not a problem since unused keys are not stored anywhere. Secondly, to enforce the constraint on the mapping’s key type, payback is used on the two functions managing money. With these additional annotations the code remains readable, and the effort required to programmers is really limited.

Lastly, Example7.5 shows a use-case taking advantage of the full power of the new type system.

Example 7.5 (Requiring a callback interface). This example shows the same appli- cation as Example 2.9. Remember that we said there may be some troubles with the interaction of Oracle and Room: the latter may not define callback or may use a wrong address to initialize its state variable oracle. Listing 7.7 shows how our extension solves the problem.

1 interface Oracle {

2 function execute<Callbackable>(string) external; 3 }

4

5 interface Callbackable {

6 function callback(uint) public; 7 }

8

9 contract ConcreteOracle is Oracle {

10 event Execute(address<Callbackable>, string); 11

13 emit Execute(msg.sender, url);

14 }

15 } 16

17 contract Room is Callbackable { 18 uint public temperature; 19 Oracle oracle;

20

21 constructor(uint _temperature, address<Oracle> _oracle) public { 22 temperature = _temperature; 23 oracle = Oracle(_oracle); 24 } 25 26 function getTemperature() { 27 oracle.execute("..."); 28 } 29

30 function callback(uint _temperature) public { 31 temperature = _temperature;

32 }

33 }

Listing 7.7: Asynchronous interaction of contracts in Solidity

There are two interfaces defining a set of minimum operations: Oracle exposes a function execute() taking a string representing a URL and, ideally, getting the result of the operation identified by such URL; Callbackable defines the function that will be eventually invoked. ConcreteOracle then implements Oracle as we de- scribed above, but note how simple is to specify that the caller of execute() must be compliant with the Callbackable interface. The compiler will check for us that any attempts to call execute() comes from a contract implementing Callbackable, thus ensuring that the result can be eventually delivered to it.

An example of this is given by the contract Room. It imposes a constraint on a parameter of its constructor, forcing the oracle to adhere to the Oracle interface: consequently, the cast at line 23 will always be safe. It then implements the function callback()and invokes the oracle via getTemperature(). Note that the func- tion execute() is external: when getTemperature() calls it, the implicit variable msg will change and its sender field will contain the address of the instance of Room, instead of the one of the caller of getTemperature().

Again, ensuring properties on addresses using this extension is really simple, with small and limited modifications in the source code (see Listing 2.13), but with large benefits in terms of type safety and provable properties.

As a last note, EOAs in FS+ are modeled by means of contracts (EOC in Def- inition 1), but in Ethereum are not. Hence, the compiler must block any attempt of cast from an address pointing to an EOA. Internally, the former could still represent these accounts with the type EOC and rule out erroneous casts (D(a)) by simply adding the hypothesis C 6= EOC, where Γ ` a : addresshCi. Using EOC to inter- nally model EOAs also allows the latter to receive Ether via transfer: in fact, by rule STRUCTURALFALLBACK-1, EOC <: Topfb.