Chapter 14. Security considerations

This chapter covers

  • Understanding Solidity weak spots and risks associated with external calls
  • Performing safe external calls
  • Avoiding known security attacks
  • General security guidelines

In the previous chapter, I gave you some advice on areas you should look at before deploying your Dapp on the production network. I believe security is such an important topic that it should be presented separately, so I’ve decided to dedicate this entire chapter to it.

I’ll start by reminding you of some limitations in the Solidity language that, if you overlook them, can become security vulnerabilities. Among these limitations, I’ll particularly focus on external calls and explain various risks you might face when executing them, but I’ll also try to give you some tips for avoiding or minimizing such risks. Finally, I’ll present classic attacks that might be launched against Ethereum Dapps so that you can avoid costly mistakes, especially when Ether is at stake.

14.1. Understanding general security weak spots

You should pay attention to certain limitations in the Solidity language because they’re generally exploited as the first line of attack by malicious participants against unaware developers:

  • Data privacy—If privacy is a requirement, you should store data in encrypted form rather than clear form.
  • Randomness—Some Dapps, for example, betting games, occasionally need to randomize. You must ensure equal processing on all nodes without exposing the application to manipulation that takes advantage of the predictability of pseudo-random generators, and that can be tricky.
  • View functions—You should be aware that functions defined as view might modify state variables, as mentioned in chapter 5, section 5.3.5.
  • Gas limits—You should be careful when setting the gas limits of your transactions, whether they’re low or high, because attackers might try to exploit them to their advantage.

Some of these vulnerabilities, such as those around randomness, might have more severe consequences, such as losing Ether. Other vulnerabilities, such as those around gas limits, have less severe consequences; for example, they can be exploited for denial of service attacks that can only cause temporary malfunctions. Whether they seem severe or not, you shouldn’t underestimate any of these vulnerabilities.

14.1.1. Private information

As you already know, data stored on the blockchain is always public, regardless of the level of access of the contract state variables it has been stored against. For example, everybody can still see the value of a contract state variable declared as private.

If you need privacy, you need to implement a hash commit–reveal scheme like the one that the MAINNET ENS domain registration uses, as described in chapter 9, section 9.3.2. For example, to conceal a bid in an auction, the original value must not be submitted. Instead, the bid should be structured in two phases, as shown in figure 14.1:

  1. Hash commit—You should initially commit the hash of the original value, and possibly of some unique data identifying the sender.
  2. Reveal—You’ll reveal the original value in a second stage, when the auction closes and the winner must be determined.

Figure 14.1. A hash commit–reveal scheme used for auction bid privacy. 1. During the commit phase, you commit a hash of a document containing the sender address, the bid value, and a secret instead of the bid value in clear. 2. During the reveal phase you reveal the bid values together with their secrets. 3. The winner is determined by finding the highest revealed bid. You then verify the results by calculating the hash from its sender address, bid value, and password and comparing it against the previously submitted hash.

14.1.2. Randomness

If you want the state of your decentralized application to depend on randomness, you’ll face the same challenges associated with concealing private information that you saw in the previous section. The main concern is preventing miners from manipulating randomness to their advantage while also making sure the logic of your contract is executed exactly in the same way on all nodes.

Consequently, the way you should handle randomness in a Dapp should be similar to the commit–reveal scheme for private data. For example, as you can see in figure 14.2, in a decentralized roulette game the following should happen:

  1. All players submit their bets (specific number, color, odd, or pair).
  2. A random value provider is requested to supply a random number and, so as to keep the number secret until the last moment, the provider initially returns (commits) the number encoded using a one-way hash algorithm.
  3. The completed transaction, including bets and the generated random number, is processed identically by all nodes, which will query the randomness provider for the original number associated with the hash. The provider reveals the original random number, and the winners are determined. If the randomness provider supplies a random number incompatible with the hash, the roulette spinning transaction fails.

Figure 14.2. A hash commit–reveal scheme used for providing reproducible randomness. 1. All players submit their bets. 2. A randomness provider commits the hash of a generated random number, emulating a roulette spin. 3. A transaction, including bets and a random number hash, is processed. All nodes query the provider, which reveals the random number so that winners and losers can be determined.

14.1.3. Calling view functions

Defining a function as a view doesn’t guarantee that nothing can modify the contract state while you’re running it. The compiler doesn’t perform such a check (but version 0.5.0 or higher of the Solidity compiler will perform this check), nor does the EVM. The compiler will only return a warning. For example, if you define the authorize() function of SimpleCoin as

    function authorize(address _authorizedAccount, uint256 _allowance)
        public view returns (bool success) {
        allowance[msg.sender][_authorizedAccount] = _allowance;
        return true;
    }

you’ll get the following warning, because the state of the allowance mapping is being modified:

Warning: Function declared as view, but this expression (potentially)
     modifies the state...

If the contract state gets modified, a transaction is executed (rather than a simple call), and this consumes gas. An attacker might take advantage of the fact that the contract owner didn’t foresee gas expenditure for this function and might cause consequences, ranging from a few transaction failures to sustained DoS attacks. To avoid

this mistake, you should pay attention to compiler warnings and make sure you rectify the code accordingly.

14.1.4. Gas limits

As you know, to be processed successfully, a transaction must not exceed the block gas limit that the sender set. If a transaction fails because it hits the gas limit, the state of the contract is reverted, but the sender must pay transaction costs, and they don’t get refunded.

The gas limit that the transaction sender sets can be favorable or detrimental to security, depending on how it’s used. Here are the two extreme cases, as illustrated in figure 14.3:

  • High gas limit—This makes it more likely that a transaction completes without running out of gas, and it’s safer against attacks trying to fail the transaction by blowing the gas limit. On the other hand, a high gas limit allows a malicious attacker to manipulate the transaction more easily, because they can use more costly resources and computation to alter the expected course of the transaction.
  • Low gas limit—This makes it more likely that a transaction can fail to complete because it runs out of gas, especially if something unexpected happens. Consequently, a transaction would be more exposed to attacks trying to fail it by blowing the gas limit. On the other hand, a low gas limit restricts how a malicious attacker can manipulate a transaction.
Figure 14.3. Pros and cons of high and low gas limits

In general, the best advice is to set the lowest possible gas limit that allows all genuine transactions to be completed against the expected logic. But it’s hard to nail a reasonable gas estimate that’s safe for both completion and security.

For example, if the logic executing a transaction includes loops, the transaction sender might decide to set a relatively high gas limit to cover the eventuality of a high number of loops. But it would be difficult to figure out in advance whether the gas limit would be hit, especially if the number of loops was determined dynamically and depended on state variables. If any of the state variables were subject to user input, an attacker could manipulate them so that the number of loops became very big, and the transaction would be more likely to run out of gas. Trying to bypass this problem by setting a very high gas limit defeats the purpose of the limit itself and isn’t the right solution. In the next few sections, we’ll explore correct solutions.

14.2. Understanding risks associated with external calls

Calls to external contracts introduce several potential threats to your application. This section will help you avoid or minimize them.

The first word of advice is to avoid calling external contracts if you can use alternative solutions. External calls transfer the flow of your logic to untrusted parties that might be malicious, even if only indirectly. For example, even if the external contract you’re calling isn’t malicious, its functions might in turn call a malicious contract, as shown in figure 14.4.

Figure 14.4. Your contract might get manipulated by an external malicious contract, even when you’re confident the contract it directly interacts with is legit.

Because you lose direct control of the execution of your logic, you’re exposed to attacks based on race conditions and reentrancy, which we’ll examine later. Also, after the external call is complete, you must be careful about how to handle return values, especially in case of exceptions. But often, even if it might feel risky, you have no choice but to interact with external contracts, for example, at the beginning of a new project, when you want to make quick progress by taking advantage of tried and tested components. In that case, the safest approach is to learn about the related potential pitfalls and write your code to prevent them—read on!

14.2.1. Understanding external call execution types

You can perform external calls to invoke a function on an external contract or to send Ether to an external contract. It’s also possible to invoke the execution of code while simultaneously transferring Ether. Table 14.1 summarizes the characteristics of each way of performing an external call.

Warning

Both send() and call() are becoming obsolete starting with version 0.5 of the Solidity compiler.

Table 14.1. Characteristics of each external call execution type

Call execution type

Purpose

External function called

Throws exception

Execution context

Message object

Gas limit

externalContractAddress .send(etherAmount) Raw Ether transfer Fallback No N/A N/A 2,300
externalContractAddress .transfer(etherAmount) Safe Ether transfer Fallback Yes N/A N/A 2,300
externalContractAddress .call.(bytes4(sha3( "externalFunction()"))) Raw function call in context of external contract Specified function No External contract Original msg. Gas limit of orig. call
externalContractAddress .callcode.(bytes4(sha3( "externalFunction()"))) Raw function call in context of caller Specified function No Calling contract New msg. created by caller Gas limit of orig. call
externalContractAddress .delegatecall.(bytes4(sha3("externalFunction()"))) Raw function call in context of caller Specified function No Calling contract Original msg. Gas limit of orig. call
ExternalContract(external ContractAddress).externalFunction() Safe function call Specified function Yes External contract Original msg. Gas limit of orig. call

When using call(), callcode(), and delegatecall(), you can transfer Ether simultaneously with the call invocation by specifying the Ether amount with the value keyword, as follows:

externalContractAddress.call.value(etherAmount)(
    bytes4(sha3("externalFunction()")

It’s also possible to transfer Ether without calling any function, in this way:

externalContractAddress.call.value(etherAmount)()

This way of sending Ether has advantages and disadvantages: it allows the recipient to have a more complex fallback function, but for the same reason, it exposes the sender to malicious manipulation. As you can understand from table 14.1, various aspects of external calls might affect security when performing an external call:

  • What external function can you call?
  • Is an exception thrown if the external call fails?
  • In which context is the external call executed?
  • What’s the message object received?
  • What’s the gas limit?

Let’s examine them one by one.

14.2.2. What external function can you call?

It’s possible to group call execution types into two sets: those only allowing you to transfer Ether and those allowing you to call any function.

Pure Ether transfer

The send() and transfer() calls can only call (implicitly) the fallback() function of the external contract, as shown here:

contract ExternalContract {
   ...
   function payable() { }          1
}

contract A {
   ...
   function doSomething()
   {
      ...
      externalContractAddress.send(etherAmount); 
   }
}

  • 1 This is the fallback function, which send() calls implicitly, as explained in section 5.3.2.

In general, a fallback function can contain logic of any complexity. But send() and transfer()impose a gas limit of 2,300 on the execution of the fallback function by transferring only a budget of 2,300 to the external function. This is such a low gas limit that, aside from transferring Ether, the fallback function can only perform a logging operation.

The low limit reassures the sender against potential reentrancy attacks (which I’ll describe shortly). It does so because when the control flow is transferred to the fallback function, the external contract isn’t able to perform any operations other than accepting the Ether transfer. On the other hand, this means you can’t use send() and transfer()if you need to execute any substantial logic around the Ether payment. If you haven’t fully understood this point yet, don’t worry: it’ll become clear when you learn about reentrancy attacks in the pages that follow.

Invocation of custom functions

All other execution types can invoke custom external functions while transferring Ether to the external contract. The downside of such flexibility is that although you can associate logic of any complexity with an Ether transfer (thanks to a gas limit that can be as high as the sender wishes), the risk of a malicious manipulation of the external call, and consequently of diversion of Ether, is also higher.

As I explained earlier, it’s possible to also purely transfer Ether without calling any function, as follows:

externalContractAddress.call.value(etherAmount)()

This way of sending Ether has the advantages and disadvantages associated with external calls through call(). The unrestricted gas limit on call() allows the recipient to have a more complex fallback function that can also access contract state, but for the same reason, it exposes the sender to potential malicious external manipulation.

Tip

The safest way to make an Ether transfer is to execute it through send() or transfer() and consequently to have it completely decoupled from any business logic.

14.2.3. Is an exception thrown if the external call fails?

From the point of view of the behavior when errors occur in external calls, you can divide call execution types into raw and safe. I’ll discuss those types here and then move on to discuss the different contexts that you can execute calls in.

Raw calls

Most call execution types are considered raw because if the external function throws an exception, they return a Boolean false result but don’t revert the contract state automatically. Consequently, if no action is taken after the unsuccessful call, the state of the calling contract may be incorrect, and any Ether you sent will be lost without having produced the expected economic benefit. Here are some examples of unhandled external calls:

externalContractAddress.send(etherAmount);          1

externalContractAddress.call.value(etherAmount)();  2

externalContractAddress.call.value(etherAmount)(
    bytes4(sha3("externalFunction()");              3

  • 1 If send() fails and isn’t handled, etherAmount is lost; the 2,300 gas budget transferred to the external fallback function is lost.
  • 2 If call() fails and isn’t handled, etherAmount is lost; all the gas is transferred to the external fallback function and is lost.
  • 3 If externalFunction fails, the etherAmount sent isn’t returned and is lost; all the gas transferred to externalFunction is also lost.

Following the external call, you have two ways to revert the contract state if the call fails. You can use require or revert().

The first way to manually revert the state if errors occur is to introduce require() conditions on some of the state variables. You must design the require() condition to fail if the external call fails, so the contract state gets reverted automatically, as shown in this snippet:

contract ExternalContract {
   ... 
   function externalFunction payable (uint _input) {
   ...                                                           1
   } 
}

contract A {
   ...
   function doSomething ()
   {
      uint stateVariable;
            
      uint initialBalance = this.Balance;
      uint commission = 60;
      externalContractAddress.call.value(commission)(
           bytes4(sha3("externalFunction()")), 10);            2

      require(this.Balance == initialBalance - commission);    3
      require(stateVariable == expectedValue);                 3
   }
}

  • 1 Something wrong happens here, and externalFunction throws an exception.
  • 2 An external function is called and following its failure, a Boolean false value is returned.
  • 3 Two require () conditions are set against the Ether balance and the state of the contract. If any condition isn’t met, both the Ether balance and the contract state are reverted.

The second way is to perform explicit checks followed by a call to revert() if the checks are unsuccessful, as shown in this code:

contract ExternalContract {
   ...

   function externalFunction payable (uint _input) {
   ...                                                1
   } 
}

contract A {
   uint stateVariable;
    
   ...
   function doSomething ()
   { 
      uint initialBalance = this.Balance;
      uint commission = 60;

      if (!externalContractAddress.call.value(commission)(
         bytes4(sha3("externalFunction()")), 10))
         revert();
   }
   ...

  • 1 Something wrong happens here, and externalFunction throws an exception.
Safe calls

Two types of external calls are considered safe in that the failure of the external call propagates the exception to the calling code, and this reverts the contract state and Ether balance. The first type of safe call is

externalContractAddress.transfer(etherAmount);        1

  • 1 If transfer() fails, the external failure triggers a local exception, which reverts the state and balance of the calling contract.

The second type of safe call is a high-level call to the external contract:

ExternalContract(externalContractAddress)
    .externalFunction();                      1

  • 1 If externalFunction fails, this triggers a local exception, which reverts the state and balance of the calling contract.
Tip

Favor safe calls through transfer(), for transferring Ether, or through direct high-level custom contract functions, for executing logic. Avoid unsafe calls such as send() for sending Ether and call() for executing logic. If a safe call fails, the contract state will be reverted cleanly, whereas if an unsafe call fails, you’re responsible for handling the error and reverting the state.

14.2.4. Which context is the external call executed in?

You can execute a call in the context of the calling contract, which means it affects (and uses) the state of the calling contract. You can also execute it in the context of the external contract, which means it affects (and uses) the state of the external contract.

Execution in the context of the external contract

If you scan through the values in the execution context column of table 14.1, you’ll realize that most call execution types involve execution in the context of the external contract. The code in the following listing shows an external call taking place in the context of the external contract.

Listing 14.1. Example of execution in the context of an external contract
contract A {
   uint value;                                     1
   address  msgSender;                             1
   address externalContractAddress = 0x5;

   function setValue(uint _value)
   {
      externalContractAddress.call.(
          bytes4(sha3("setValue()")), _value);     2
   }
}

contract ExternalContract {


   uint value;                                     3
   address msgSender;                              3
     
   function setValue(uint _value) {
       value = _value;                             4
       msgSender = msg.sender;                     5
   } 
} 

  • 1 State variables
  • 2 External call performed on ExternalContract.setValue ()
  • 3 State variables defined on contract A
  • 4 Modifies ExternalContract.value and sets it to _value
  • 5 Modifies ExternalContract.msgSender and sets it to the original msg.sender sent to contract A

Through the example illustrated in figure 14.5, I’ll show you how the state of ContractA and ExternalContract change following the external call implemented in listing 14.1.

Figure 14.5. Example illustrating execution in the context of an external contract

The addresses of the user and contract accounts used in the example are summarized in table 14.2.

Table 14.2. User and contract account addresses

Account

Address

user1 0x1
user2 0x2
ContractA 0x3
ContractB 0x4
ExternalContract 0x5

The initial state of the contracts before the external call takes place is summarized in table 14.3. The state of the contracts will change to that shown in table 14.4.

Table 14.3. Initial state of the contracts

ContractA

ExternalContract

Value 16 24

Now imagine user1 performs the following call on ContractA; for example, from a web UI, through Web3.js, as you saw in chapter 12:

ContractA.setValue(33)
Table 14.4. State of the contracts and msg object following an external call

ContractA

ExternalContract

Value 16 33
msg sender 0x1 0x1

In summary, the state of ContractA hasn’t changed, whereas the state of ExternalContract has been modified, as shown in table 14.4. The msg object that ExternalContract handles is the original msg object that user1 generated while calling ContractA.

Execution in the context of the calling contract with delegatecall

Execution through delegatecall takes place in context of the calling contract. The code in the following listing shows an external call taking place in the context of the external contract.

Listing 14.2. Example of delegatecall execution in the context of a calling contract
contract A {
   uint value;                                   1
   address msgSender;                            1
   address externalContractAddress = 0x5;

   function setValue(uint _value)
   {
      externalContractAddress.delegatecall.(
        bytes4(sha3("setValue()")), _value);     2

   }
}

contract ExternalContract {

   uint value;                                   3
   address msgSender;                            3
     
   function setValue(uint _value) {
      value = _value;                            4
      msgSender = msg.sender;                    5
   } 
}

  • 1 External call performed on ExternalContract.setValue ()
  • 2 External call performed on ExternalContract.setValue ()
  • 3 State variables defined as in ContractA
  • 4 Modifies ContractA.value and sets it to _value
  • 5 Modifies ContractA.msgSender and sets it to the original msg.Sender sent to ContractA

As I did earlier, through the example illustrated in figure 14.6, I’ll show you how the state of ContractA and ExternalContract change following the external call implemented in listing 14.2.

Figure 14.6. Example illustrating delegatecall execution in the context of the calling contract

The initial state of the contracts before the external call takes place is summarized in table 14.5.

Table 14.5. Initial state of the contracts

ContractA

ExternalContract

Value 16 24

Now imagine user1 performs the following call on ContractA, for example, from a web UI:

ContractA.setValue(33)

In summary, the state of ContractA has been modified, whereas the state of External-Contract hasn’t changed, as shown in table 14.6. The msg object that External-Contract handles is still the original msg object that user1 generated while calling ContractA.

Table 14.6. State of the contracts and msg object following an external call through delegatecall

ContractA

ExternalContract

Value 33 24
msg sender 0x1 0x1
Execution in the context of the calling contract with callcode

The last case to examine is when the implementation of ContractA.setValue() uses callcode rather than delegatecall, as shown here:

function setValue(uint _value)
{
   externalContractAddress.callcode.(bytes4(sha3("setValue()")), _value);
}

Assuming the same initial state as before, after user1’s call, illustrated in figure 14.7, the state of the contracts will be that shown in table 14.7.

Figure 14.7. Example illustrating callcode execution in the context of the calling contract

Table 14.7. State of the contracts following an external call through callcode

ContractA

ExternalContract

Value 33 24
msg sender 0x1 0x3

As you can see, an external function that callcode calls is still executed in the context of the caller, as when the call was performed through delegatecall. But ContractA generates a new msg object when the external call takes place, and the message sender is ContractA.

From a security point of view, execution in the context of the external contract is clearly safer than in the context of the calling contract. When execution takes place in the context of the calling contract, such as when calling external functions through callcode or delegatecall, the caller is allowing the external contract read/write access to its state variables. As you can imagine, it’s safe to do so only in limited circumstances-, mainly when the external contract is under your direct control (for example, you’re the contract owner). You can find a summary of the context and the msg object used for each call type in table 14.8.

Table 14.8. Summary of execution context and msg object in each call type

Call type

Execution context

msg object

call External contract Original msg object
delegatecall Caller contract Original msg object
callcode Caller contract Caller contract-generated msg object
Tip

If you need to use call(), favor calls through call(), in the context of the external contract rather than in the context of the calling contract. Bear in mind, though, that call() will become obsolete starting with version 0.5 of Solidity.

14.2.5. What’s the msg object received?

In general, a message object is supposed to flow from its point of creation up to the last contract of an external-call chain, which might span several contracts. This is true when invoking external calls through all external call types, apart from callcode, which generates a new message instead, as you saw when comparing the external call execution under callcode and delegatecall. The delegatecall opcode was introduced as a bug fix for the unwanted message-creating behavior of callcode. Consequently, you should avoid using callcode if possible.

Tip

Avoid callcode if possible and choose delegatecall instead.

14.2.6. What’s the gas limit?

Apart from send() and transfer(), which impose a gas limit of 2,300 gas on the external call that’s only sufficient to perform an Ether transfer and a log operation, all the other external call types transfer to the external call the full gas limit present in the original call. As I explained previously, both low and high gas limits have security implications, but when it comes to transferring Ether, a lower limit is preferable because it prevents external manipulation when Ether is at stake.

Tip

Favor a lower gas limit over a higher gas limit.

14.3. How to perform external calls more safely

You should now have a better idea of the characteristics and tradeoffs associated with each external call type, and you might be able to choose the most appropriate one for your requirements. But even if you pick the correct call type, you might end up in trouble if you don’t use it correctly. In this section, I’ll show you some techniques for performing external calls safely. You’ll see how even performing an Ether transfer through the apparently safe and inoffensive transfer() can end up in a costly mistake if you don’t think through all the scenarios that could lead your call to fail.

14.3.1. Implementing pull payments

Imagine you’ve developed an auction Dapp and you’ve implemented an Auction contract like the one shown in the open source Ethereum Smart Contract Best Practices guide coordinated by ConsenSys,[1] which I’ve provided in the following listing. Have a good look at this listing, because I’ll reference it a few times in this chapter.

1

See “Recommendations for Smart Contract Security in Solidity,” http://mng.bz/MxXD, licensed under Apache License, Version 2.0.

Listing 14.3. Incorrect implementation of an Auction contract
contract Auction {//INCORRECT CODE //DO NOT USE!//UNDER APACHE LICENSE 2.0
    // Copyright 2016 Smart Contract Best Practices Authors
    address highestBidder;
    uint highestBid;

    function bid() payable {
        require(msg.value >= highestBid);           1

        if (highestBidder != 0) { 
           highestBidder.transfer(highestBid);      2
        }

       highestBidder = msg.sender;                  3
       highestBid = msg.value;                      3
    }
}

  • 1 Reverts the transaction if the current bid isn’t the highest one
  • 2 If the current bid is the highest, refunds the previous highest bidder
  • 3 Updates the details of the highest bid and bidder

What happens if one of the bidders has implemented a fallback, as shown in the following listing, and then they submit a bid higher than the highest one?

Listing 14.4. A malicious contract calling the Auction contract
contract MaliciousBidder {
   address auctionContractAddress = 0x123;
   function submitBid() public {
      auctionContractAddress.call.value(
          100000000000)(bytes4(sha3("bid()")));
   }

   function payable() { 
      revert ();            1
   } 
...
}

  • 1 This contract will revert its state and throw an exception every time it receives an Ether payment.

As soon as the MaliciousBidder contract submits the highest bid through submitBid(), Auction.bid() refunds the previous highest bidder then sets the address and value of the highest bid to those of the MaliciousBidder. So far, so good. What happens next?

A new bidder now makes the highest bid. Auction.bid() will consequently try to refund MaliciousBidder, but the following line of code fails, even if the new bidder has done nothing wrong and the logic of the bid() function seems correct:

highestBidder.transfer(highestBid);

This line fails because the current highestBidder is still the address of MaliciousBidder, and its fallback, which highestBidder.transfer() calls, throws an exception.If you think about it, no new bidder will ever be able to pass this line, because a refund to MaliciousBidder will be attempted on every occasion. Also, the call to highestBidder.transfer() will keep failing before the address and value of a new highest bid can ever be updated, as illustrated in figure 14.8. That’s why MaliciousBidder is . . . malicious!

What about replacing transfer() with send()? An exception will be thrown in the bid() function following a failure in send(). As a result, using send() instead of transfer() in the recommended way, as shown in the following line of code, doesn’t solve the problem:

require(highestBidder.send(highestBid));

Figure 14.8. After the malicious contract has become the highest bidder, the Auction contract becomes unusable because it will unsuccessfully try to refund the malicious contract at every new higher bid and will never be able to set the new highest bidder.

With your current bid() implementation, you don’t even need a malicious external bidder contract to end up in trouble. Also, unintentional exceptions that are thrown by any external bidding contract that has a faulty fallback() can rock the boat. For example, a sloppy developer of a bidder contract, unaware of the gas limitations associated with transfer() (or send()) might have decided to implement a complex fallback function, such as the one shown in the code that follows, that accepts the refund and processes it by modifying its own contract state. That would consequently blow the transfer() 2,300 gas stipend and almost immediately throw a “ran out of gas” exception:

function () payable() {
      refunds +=  msg.value;       1
}

  • 1 As soon as the state variable refund is updated, the function execution runs out of the 2,300 gas stipend imposed by transfer() and fails.

As you can see, the current implementation of bid() relies heavily on the assumption that you’re dealing with honest and competent external contract developers. That might not always be the case.

A safer way to accept a bid is to separate the logic that updates the highest bidder from the execution of the refund to the previous highest bidder. The refund will no longer be pushed automatically to the previous highest bidder but should now be pulled with a separate request by them, as shown in the following listing. (This solution also comes from the ConsenSys guide I mentioned earlier.)

Listing 14.5. Correct implementation of an Auction contract
//UNDER APACHE LICENSE 2.0
//Copyright 2016 Smart Contract Best Practices Authors
//https://consensys.github.io/smart-contract-best-practices/
contract Auction {
    address highestBidder;
    uint highestBid;
    mapping(address => uint) refunds;

    function bid() payable external {
         require(msg.value >= highestBid);

        if (highestBidder != 0) {
            refunds[highestBidder] += highestBid;     1
        }

        highestBidder = msg.sender;                   2
        highestBid = msg.value;                       2
    }

    function withdrawRefund() external {
        uint refund = refunds[msg.sender];
        refunds[msg.sender] = 0;
        msg.sender.transfer(refund);                  3
        }
    }
}

  • 1 Now this function only stores the amount to refund because of a new higher bidder in the refund mapping. No Ether transfer takes place.
  • 2 The update of the new highest bid and bidder will now succeed because bid() no longer contains external operations that might get hijacked, such as the previous transfer() call.
  • 3 If this transfer fails—for example, when paying MaliciousBidder—the state of the Auction contract is now unaffected.

Pull payments also come in handy in case the function that makes a payment performs a number of payments in a loop. An example would be a function that refunds all the accounts of the investors in an unsuccessful crowdsale, as shown in the following listing.

Listing 14.6. Incorrect implementation of a function making several payments
contract Crowdsale {
   address[] investors;
   mapping(address => uint) investments;

   function refundAllInvestors() payable onlyOwner external {
      //INCORRECT CODE //DO NOT USE!
    
      for (int i =0; i< investors.length; ++i) {
         investors[i].send(investments[investors[i]]);     
    }
}

If an attacker makes very small investments from a very high number of accounts, the number of items in the investors array might become so big that the for loop will run out of gas before completing, because each step of the loop has a fixed gas cost. This is a form of DoS attack exploiting gas limits. A safer implementation is to keep only the refund assignment in refundAllInvestors() and to move the Ether transfer operation into a separate pull payment function called withdrawalRefund(). This is similar to the one you saw earlier in the Auction contract, as you can see in the following listing.

Listing 14.7. Improved refundAllInvestors() implementation
contract Crowdsale {
   address[] investors;
   mapping(address => uint) investments;
   mapping(address => uint) refunds;

   function refundAllInvestors() payable onlyOwner external {
    
      for (int i =0; i< investors.length; ++i) {
         refunds[investors [i]] = investments[i];
         investments[investors[i]] = 0;     
      }
   }

   function withdrawRefund() external {
      uint refund = refunds[msg.sender];
      refunds[msg.sender] = 0;
      msg.sender.transfer(refund); 
   }
}    

14.3.2. Implementing a minimal fallback function

Although pull payments are a good solution from the point of view of the contract that’s transferring Ether out, now put yourself in the shoes of the bidder. If you’re expecting Ether from an external contract, such as the Auction contract, don’t assume the external contract is implementing safe pull-payment functionality, as shown in listing 14.7. Assume instead that the external contract has been implemented in a suboptimal way, as in listing 14.6, the initial implementation you looked at. In this case, if you want to make sure the refund operation executed with transfer() (or send()) succeeds, you must provide a minimal fallback function: empty or at most with a single log operation, as shown here:

function() public payable {}

14.3.3. Beware of Ether coming to you through selfdestruct()

Unfortunately, you can’t make sure your contract doesn’t receive Ether from unknown sources. You might think that having a fallback that always throws an exception or reverts the state of your contract when called, as shown here, should be sufficient to stop this undesired inflow of Ether:

 function() public payable {revert ();}

But I’m afraid there’s a way to transfer Ether to any address that doesn’t require any payable function on the receiving side—not even a fallback function. This can be achieved by calling

selfdestruct(recipientAddress);

The selfdestruct() function was introduced to provide a way to destroy a contract in case of emergency, and with the same operation, to transfer all the Ether associated with the contract account to a specified address. Typically, this would be executed when a critical bug was discovered or when a contract was being hacked.

Unfortunately, selfdestruct() also lends itself to misuse. If an external contract contains at least 1 Wei and self-destructs, targeting the address of your contract, there isn’t much you can do. You might think receiving unwanted Ether wouldn’t be a serious issue, but if the logic of your contract depends on checks and reconciliations performed on the Ether balance, for example, through require(), you might be in trouble.

14.4. Avoiding known security attacks

Now that we’ve reviewed Solidity’s known security weak spots associated with external calls, it’s time to analyze known attacks that have taken place exploiting such weaknesses. You can group attacks on Solidity contracts into three broad categories, depending on the high-level objective of the attacker. The objective can be to

  • manipulate the outcome of an individual transaction
  • favor one transaction over other transactions
  • make a contract unusable

Table 14.9 summarizes manipulation techniques associated with each attack category. The next few sections will define and present in detail each attack technique included in the table.

Table 14.9. Security attacks, strategies, and techniques

Attack objective

Attack strategy

Attack technique

Individual transaction manipulation Race condition Reentrancy, cross-function race condition
Favoring one transaction over others Front-running Front-running
Making contract unusable Denial of service Fallback calling revert(), exploiting gas limits
Warning

This section only covers the most common attacks, mainly to give you an idea of how malicious participants can manipulate a contract. Also, new security attacks are continuously discovered, so you must learn about and constantly keep up to date with the latest security breaches by consulting the official Solidity documentation and the many other websites and blogs that cover the topic. I’ll point you to some resources in section 14.5.

14.4.1. Reentrancy

Reentrancy attacks target functions containing an external call and exploit a race condition between simultaneous calls to this function caused by the possible time lag that takes place during the external call. The objective of the attack is generally to manipulate the state of the contract, often having to do with an Ether or custom cryptocurrency balance, by calling back the targeted function many times simultaneously while the attacker hijacks the execution of the external call. If we go back to the example of the auction Dapp I showed you earlier, an attacker could launch a reentrancy attack on an incorrect implementation of withdrawRefund() by requesting a refund many times in parallel while hijacking each refund call, as illustrated in figure 14.9.

Figure 14.9. If you were to implement Auction.withdrawRefund() incorrectly, for example, by clearing the balance of the caller only after the Ether transfer has been completed, an attacker could attempt to call it many times in parallel while hijacking each call, by slowing down the execution of the receiving fallback() function. Various of these simultaneous Ether transfers are allowed and can complete successfully until one of them finally completes and the balance of the caller is cleared. Before this happens, many illegitimate refunds might take place.

The following code shows an incorrect implementation of withdrawRefund() (also from the ConsenSys guide) that will put your contract in danger:

function withdrawRefund() external { {//INCORRECT CODE //DO NOT USE!
    // UNDER APACHE LICENSE 2.0
    // Copyright 2016 Smart Contract Best Practices Authors
   uint refund = refunds[msg.sender];
   require (msg.sender.call.value(refund)());     1
   refunds[msg.sender] = 0;                       2
}

  • 1 Calls to an external contract fallback function, which might take a relatively long time
  • 2 Executes only after the previous external call is complete

As I mentioned, an attacker contract might call withdrawRefund() several times while hijacking each external call to the fallback function that enables the payment, as shown here:

contract ReentrancyAttacker {
   function() payable public () {
     uint maxUint = 2 ** 256 - 1;
     for (uint I = 0; i < maxUint; ++i)
     { 
        for (uint j =0; j  < maxUint; ++j)
        {
           for  (uint k =0; k < maxUint; ++k)       1
           {
                ...
     }
}

  • 1 The fallback contains only code to delay its completion. Before this completes, the attacker calls withdrawRefund() several times.

Such a slow execution of the Ether transfer would prevent withdrawRefund() from reaching the code line that clears the caller balance for a long time:

refunds[msg.sender] = 0;

Until this line is reached, various Ether transfers might take place, each equal to the amount owed to the caller. As a result, the caller could receive more Ether than they’re owed, as shown in the sequence diagram in figure 14.10.

Note

The reason why I wanted to include in this chapter the auction Dapp from the ConsenSys guide, and particularly its incorrect implementation of the withdrawRefund() function, is that this code shows one of the vulnerabilities that contributed to the initial success of the DAO attack.

Figure 14.10. Sequence diagram of parallel invocations of an incorrect implementation of withdrawRefund() by an attacker

You can prevent this attack using a couple of methods:

  • If you can, use transfer() or send() instead of call.value()(), so the attacker will have a gas limit of 2,300 and be prevented from implementing any transfer-delaying code in their fallback() function.
  • Place the external call performing the Ether transfer as the last operation, so the Ether balance can be cleared before the call takes place rather than after, as shown in listing 14.8. If the attacker tried to call withdrawRefund() again, the value of refunds[msg.sender] would be zero, and consequently the refund would be set to zero. Therefore, the new Ether payment wouldn’t have any effect.
Listing 14.8. Correct withdrawRefund implementation
function withdrawRefund() external {
   uint refund = refunds[msg.sender];
   refunds[msg.sender] = 0;
   require (msg.sender.call.value(refund)());         1
}

  • 1 Now the transfer takes place after the balance has been cleared, so subsequent calls to withdraw-Refund() have no effect.

14.4.2. Cross-function race conditions

You’ve learned that reentrancy attacks exploit a race condition between simultaneous calls to the same function. But an attacker can also exploit race conditions between simultaneous calls on separate functions that try to modify the same contract state—for example, the Ether balance of a specific account.

A cross-race condition also could happen on SimpleCoin. Recall SimpleCoin’s transfer() function:

function transfer(address _to, uint256 _amount) public {
    require(coinBalance[msg.sender] > _amount);
    require(coinBalance[_to] + _amount >= coinBalance[_to] );
    coinBalance[msg.sender] -= _amount;  
    coinBalance[_to] += _amount;   
    emit Transfer(msg.sender, _to, _amount);  
}

Now, imagine you decided to provide a withdrawFullBalance() function, which closed the SimpleCoin account of the caller and sent them back the equivalent amount in Ether. If you implemented this function as follows, you’d expose the contract to a potential cross-function race condition:

function withdrawFullBalance() public {//INCORRECT CODE //DO NOT USE!
    uint amountToWithdraw = coinBalance[msg.sender] * exchangeRate;
    require(msg.sender.call.value(
        amountToWithdraw)());               1
    coinBalance[msg.sender] = 0;
}

  • 1 The attacker contract calls transfer() at this point of the execution of withdrawFullBalance().

A cross-function race condition attack works in a similar way to the reentrancy attack shown earlier. An attacker would first call withdrawFullBalance() and, while they were hijacking the external call from their fallback function, as shown in the following code, they’d call transfer() to move the full SimpleCoin balance to another address they own before the execution of withdrawBalance()cleared this balance. In this way, they’d both keep the full SimpleCoin balance and get the equivalent Ether amount:

contract RaceConditionAttacker {
   function() payable public () {
      uint maxUint = 2 ** 256 - 1;
         for (uint I = 0; i < maxUint; ++i)
         { 
            for (uint j =0; j  < maxUint; ++j)
            {
               for   (uint k =0; k < maxUint; k++)
               {
                  ...
     }
}

The solution is, as was the case for the reentrancy attack, to replace call.value()() with send() or transfer(). You would also need to make sure the external call that performs the balance withdrawal takes place in the last line of the function, after the caller balance has already been set to 0:

function withdrawFullBalance() public {
    uint amountToWithdraw = coinBalance[msg.sender];
    coinBalance[msg.sender] = 0;
    require(msg.sender.call.value(
        amountToWithdraw)());              1
}

  • 1 The external call is now performed after the caller balance has been cleared.

More complex cases of reentrancy involve call chains spanning several contracts. The general recommendation is always to place external calls or calls to functions containing external calls at the end of the function body.

14.4.3. Front-running attacks

The attacks based on race conditions you’ve seen so far try to manipulate the outcome of a transaction by altering its expected execution flow, generally by hijacking the part of the execution that takes place externally.

Other attack strategies work at a higher level and target decentralized applications for which the ordering of the execution of the transactions is important. Attackers try to influence the timing or ordering of transaction executions by favoring and prioritizing certain transactions over others. For example, a malicious miner might manipulate a decentralized stock market-making application by creating new buy order transactions when detecting in the memory pool many buy orders for a certain stock. The miner would then include only their own buy order transactions on the new block, so their transactions would get executed before any other similar order present in the memory pool, as illustrated in figure 14.11. If the miner’s PoW was successful, their buy order would become official. Subsequently, the stock price would rise because of the many buy orders that have been submitted but not executed yet. This would generate an instant profit for the miner.

Figure 14.11. An example of a front-running attack. A malicious miner could detect big buy or sell orders being sent to a stock market-making Dapp and still in the memory pool. They could then decide to front run them by ignoring these big orders and including their own orders in the new block they would try to create, therefore making an unfair profit if the block got mined.

This manipulation is an example of front running, which is the practice of malicious stock brokers who place the execution of their own orders ahead of those of their clients. A way to avoid this attack is to design the order clearing logic on batch execution rather than individual execution, with an implementation similar to batch auctions. With this setup, the auction contract collects all bids and then determines the winner with a single operation. Another solution is to implement a commit–reveal scheme similar to that described earlier in this chapter to disguise order information.

14.4.4. Denial of service based on revert() in fallback

Some attacks aim to bring down a contract completely. These are known as denial of service (DoS) attacks.

As I’ve already shown you in the Auction contract, at the beginning of this chapter, an attacker could make a contract unusable by implementing the following fallback and then calling the targeted contract in such a way that it triggers an incoming payment:

function payable() { 
   revert ();              1
} 

  • 1 This contract will revert its state and throw an exception every time it receives an Ether payment.

If the targeted contract implements a function as shown here, it will become unusable as soon as it tries to send Ether to the attacker:

function bid() payable {//INCORRECT CODE//DO NOT USE!
   //UNDER APACHE LICENSE 2.0
   //Copyright 2016 Smart Contract Best Practices Authors

   require(msg.value >= highestBid);          1

   if (highestBidder != 0) { 
      highestBidder.transfer(highestBid));    2
   }

   highestBidder = msg.sender;                3
   highestBid = msg.value;                    3
}

  • 1 Checks if the current bid is higher than the current highest bid
  • 2 The previous highest bidder is refunded, but when the previous highest bidder is the malicious contract, the transfer fails, and a new higher bidder and bid value can no longer be set.
  • 3 Sets new higher bidder and bid values

As you already know, you can avoid this attack by implementing a pull payment facility rather than an automated push payment. (See section 14.3.1 for more details.)

14.4.5. Denial of service based on gas limits

In the section on pull payments, you saw the example of an incorrectly implemented function that refunds all the accounts of the investors in an unsuccessful crowdsale:

contract Crowdsale {
   address[] investors;
   mapping(address => uint) investments;

   function refundAllInvestors() payable onlyOwner external {
      //INCORRECT CODE //DO NOT USE!    
      for (int i =0; i< investors.length; ++i) {
         investors[i].send(investments[investors[i]]);     
    }
}

I already warned you that this implementation lends itself to manipulation by an attacker who makes very small investments from a very high number of accounts. The high number of for loops required by the large investments array will damage the contract permanently, because any invocation of the function will blow the gas limit. This is a form of DoS attack exploiting gas limits. Refunds based on pull payment functionality also prevent this attack.

You’ve learned about the pitfalls associated with external calls and how to avoid the most common forms of attack. Now I’ll close the chapter by sharing with you some security recommendations I’ve been collecting over time from various sources.

14.5. General security guidelines

The official Solidity documentation has an entire section dedicated to security considerations and recommendations,[2] which I invite you to consult before deploying your contract on public networks. Other excellent resources are available, such as the open source Ethereum Smart Contract Best Practices guide (http://mng.bz/dP4g), initiated and maintained by the Diligence (https://consensys.net/diligence/) division of ConsenSys, which focuses on security and aims at raising awareness around best practices in this field. This guide, which I’ve referenced in various places in this chapter, is widely considered to be the main reference on Ethereum security. I’ve decided to adopt its terminology to make sure you can look up concepts easily, if you decide you want to learn more about anything I’ve covered here.

2

See “Security Considerations” at http://mng.bz/a7o9.

In table 14.10, I’ve listed an additional set of useful free resources on Ethereum security that ConsenSys Diligence has created. Presentations and posts by Christian Reitwiessner,[3] the head of Solidity at Ethereum, are also a must-read.

3

See “Smart Contract Security,” June 10,2016, http://mng.bz/GWxA, and “How to Write Safe Smart Contracts,” November 10, 2015, http://mng.bz/zMx6.

Table 14.10. Ethereum security resources ConsenSys Diligence has distributed

Resource

Description

Secure smart contract philosophy1 Series of Medium articles written by ConsenSys Diligence on how to approach smart contract security
EIP 1470: SWC2 Standardized weakness classification for smart contracts, so tool vendors and security practitioners can classify weaknesses in a more consistent way
0x security audit report3 Full security audit of ConsenSys 0x smart contract system, carried out by ConsenSys Diligence. This gives a good idea of the weaknesses assessed during a thorough security audit.
Audit readiness guide4 Guidelines on how to prepare for a smart contract security audit
1. See “Building a Philosophy of Secure Smart Contracts,” http://mng.bz/ed5G.
2. See these GitHub pages: https://github.com/ethereum/EIPs/issues/1469andhttp://mng.bz/pgVR.
3. See http://mng.bz/O2Ej.
4. See Maurelian’s “Preparing for a Smart Contract Code Audit,” September 6, 2017, at http://mng.bz/YPqj.

I’ll summarize in a short list the most important points all the resources I’ve mentioned tend to agree on. I reiterate, though, that it’s important to constantly keep up to date with the latest security exploits and discovered vulnerabilities on sites such as http://hackingdistributed.com, https://cryptonews.com, or https://cryptoslate.com. Here’s the list:

  • Favor a simple contract design. The same design recommendations that generally apply to object-oriented classes are also valid for smart contracts. Aim for small contracts, focused on only one piece of functionality, with a small number of state variables and with short functions. This will help you avoid mistakes and will help fellow developers understand your code correctly.
  • Amend code that raises compiler warnings. Understand any compiler warnings and amend your code accordingly. Aim to remove all the warnings you get, if possible, especially those related to deprecated functionality. Solidity syntax has been amended often because of security concerns, so take the advice from warnings seriously.
  • Call external contracts as late as possible. As you’ve learned in the sections dedicated to reentrancy, you should avoid changing the state of your contract after returning from an external call. This call might get hijacked, and you might not be able to return from it safely. The recommended pattern to adopt is called check–effects–interaction, according to which you structure a function on the following ordered steps:

    • Check—Validate that the message sender is authorized, function input parameters are valid, Ether being transferred is available, and so on. You generally do this directly through require statements or indirectly through function modifiers.
    • Effects—Change the state of your contract based on the validated function input.
    • Interaction—Call external contracts or libraries using the new contract state.
  • Plan for disaster. As you learned in the previous chapter, once a contract has been deployed, you can’t modify it. If you discover a bug or, even worse, a security flaw, and Ether is at risk, you can’t apply a hot-fix as you’d do on a conventional centralized application. You should plan for this eventuality beforehand and provide the contract owner with an emergency function, such as freeze() or selfDestruct(), as I mentioned in chapters 6 and 7 when presenting Simple-Crowdsale. Such functions can disable the contract temporarily, until you understand the defect, or even permanently. Some developers have taken a more proactive approach and have implemented auto-freezing (or fail-safe) functionality based on contract state pre- or postcondition checks on each contract function. If the condition isn’t met, the contract moves into a fail-safe mode in which all or most of its functionality is disabled. Regardless of whether you decided to fit your contract with an interactive or automated emergency stop, ideally you should also plan for an upgrade strategy, as I discussed in chapter 13.
  • Use linters. A linter is a static code analysis tool that aims at catching breaches against recommended style, efficient implementation, or known security vulnerabilities. The two most well-known Solidity linters are Solium (now Ethlint) (https://github.com/duaraghav8/Solium) and Solint (https://github.com/SilentCicero/solint). They both provide integration plugins for most common general code editors, such as Atom, Sublime, VS Code, and JetBrains IDEA. Apart from highlighting security vulnerabilities, these tools give feedback on coding style and best practices in general, so they can help you learn Solidity quickly.
  • Use security analysis frameworks. If you want to go the extra mile, don’t stop at linters. Instead, aim at integrating security analysis into your development cycle with frameworks such as the Mythril Platform,[4] which combines a variety of static and dynamic analyzers.

    4

    See Bernhard Mueller, “MythX Is Upping the Smart Contract Security Game,” http://mng.bz/0WmE, for an introduction to Mythril.

  • Follow the wisdom of the crowds. If you’re not sure about the safety of smart contracts you’d like to connect to, you could look up their ratings in Panvala,[5] a system that attempts to gather the level of security of smart contracts from their users.

    5

    See “Introducing Panvala,” http://mng.bz/K1Mg, for an introduction to Panvala.

  • Commission a formal security audit. If your smart contract handles anything valuable, such as cryptocurrency or tokens, before going into production you should consider commissioning a formal security audit from one of the many consultancies that are starting to specialize in this area.

Summary

  • Attackers generally exploit limitations in the Solidity language, the EVM, and the blockchain as the first line of attack against unaware developers, especially around data privacy, random numbers, integer overflows, and gas limits.
  • If not well understood, external calls can expose a contract to manipulation by malicious participants. For example, some external calls throw exceptions, whereas others don’t, or some execute in the context of the caller contract, and others in the context of the called contract. You must understand the risks of each type and handle returned value and contract state accordingly.
  • Various techniques are available to perform safer external calls and reduce the chance of external manipulation. Examples include pull payment (rather than automated payment) functionality and Ether transfer based on transfer() and send(), which restrict the gas limit on an external fallback function.
  • The minimum line of defense is to prepare at least against well-known attacks, such as reentrancy, front-running attacks, and denial of service (DoS) attacks.
  • The official Solidity documentation and various online security guides, sites, and blogs provide up-to-date information on the latest attacks and guidelines for avoiding them.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset