DELEGATECALL

The CALL and DELEGATECALL opcodes are useful in allowing Ethereum developers to modularize their code. Standard external message calls to contracts are handled by the CALL opcode, whereby code is run in the context of the external contract/function. The DELEGATECALL opcode is almost identical, except that the code executed at the targeted address is run in the context of the calling contract, and msg.sender and msg.value remain unchanged. This feature enables the implementation of libraries, allowing developers to deploy reusable code once and call it from future contracts.

Although the differences between these two opcodes are simple and intuitive, the use of DELEGATECALL can lead to unexpected code execution.

For further reading, see Loi.Luu’s Ethereum Stack Exchange question on this topic and the Solidity docs.

The Vulnerability

As a result of the context-preserving nature of DELEGATECALL, building vulnerability-free custom libraries is not as easy as one might think. The code in libraries themselves can be secure and vulnerability-free; however, when run in the context of another application new vulnerabilities can arise. Let’s see a fairly complex example of this, using Fibonacci numbers.

Consider the library in FibonacciLib.sol, which can generate the Fibonacci sequence and sequences of similar form. (Note: this code was modified from https://bit.ly/2MReuii.)

Example 6. FibonacciLib.sol

  1. // library contract - calculates Fibonacci-like numbers
  2. contract FibonacciLib {
  3. // initializing the standard Fibonacci sequence
  4. uint public start;
  5. uint public calculatedFibNumber;
  6. // modify the zeroth number in the sequence
  7. function setStart(uint _start) public {
  8. start = _start;
  9. }
  10. function setFibonacci(uint n) public {
  11. calculatedFibNumber = fibonacci(n);
  12. }
  13. function fibonacci(uint n) internal returns (uint) {
  14. if (n == 0) return start;
  15. else if (n == 1) return start + 1;
  16. else return fibonacci(n - 1) + fibonacci(n - 2);
  17. }
  18. }

This library provides a function that can generate the n-th Fibonacci number in the sequence. It allows users to change the starting number of the sequence (start) and calculate the n-th Fibonacci-like numbers in this new sequence.

Let us now consider a contract that utilizes this library, shown in FibonacciBalance.sol.

Example 7. FibonacciBalance.sol

  1. contract FibonacciBalance {
  2. address public fibonacciLibrary;
  3. // the current Fibonacci number to withdraw
  4. uint public calculatedFibNumber;
  5. // the starting Fibonacci sequence number
  6. uint public start = 3;
  7. uint public withdrawalCounter;
  8. // the Fibonancci function selector
  9. bytes4 constant fibSig = bytes4(sha3("setFibonacci(uint256)"));
  10. // constructor - loads the contract with ether
  11. constructor(address _fibonacciLibrary) external payable {
  12. fibonacciLibrary = _fibonacciLibrary;
  13. }
  14. function withdraw() {
  15. withdrawalCounter += 1;
  16. // calculate the Fibonacci number for the current withdrawal user-
  17. // this sets calculatedFibNumber
  18. require(fibonacciLibrary.delegatecall(fibSig, withdrawalCounter));
  19. msg.sender.transfer(calculatedFibNumber * 1 ether);
  20. }
  21. // allow users to call Fibonacci library functions
  22. function() public {
  23. require(fibonacciLibrary.delegatecall(msg.data));
  24. }
  25. }

This contract allows a participant to withdraw ether from the contract, with the amount of ether being equal to the Fibonacci number corresponding to the participant’s withdrawal order; i.e., the first participant gets 1 ether, the second also gets 1, the third gets 2, the fourth gets 3, the fifth 5, and so on (until the balance of the contract is less than the Fibonacci number being withdrawn).

There are a number of elements in this contract that may require some explanation. Firstly, there is an interesting-looking variable, fibSig. This holds the first 4 bytes of the Keccak-256 (SHA-3) hash of the string 'setFibonacci(uint256)'. This is known as the function selector and is put into calldata to specify which function of a smart contract will be called. It is used in the delegatecall function on line 21 to specify that we wish to run the fibonacci(uint256) function. The second argument in delegatecall is the parameter we are passing to the function. Secondly, we assume that the address for the FibonacciLib library is correctly referenced in the constructor (External Contract Referencing discusses some potential vulnerabilities relating to this kind of contract reference initialization).

Can you spot any errors in this contract? If one were to deploy this contract, fill it with ether, and call withdraw, it would likely revert.

You may have noticed that the state variable start is used in both the library and the main calling contract. In the library contract, start is used to specify the beginning of the Fibonacci sequence and is set to 0, whereas it is set to 3 in the calling contract. You may also have noticed that the fallback function in the FibonacciBalance contract allows all calls to be passed to the library contract, which allows for the setStart function of the library contract to be called. Recalling that we preserve the state of the contract, it may seem that this function would allow you to change the state of the start variable in the local FibonnacciBalance contract. If so, this would allow one to withdraw more ether, as the resulting calculatedFibNumber is dependent on the start variable (as seen in the library contract). In actual fact, the setStart function does not (and cannot) modify the start variable in the FibonacciBalance contract. The underlying vulnerability in this contract is significantly worse than just modifying the start variable.

Before discussing the actual issue, let’s take a quick detour to understand how state variables actually get stored in contracts. State or storage variables (variables that persist over individual transactions) are placed into slots sequentially as they are introduced in the contract. (There are some complexities here; consult the Solidity docs for a more thorough understanding.)

As an example, let’s look at the library contract. It has two state variables, start and calculatedFibNumber. The first variable, start, is stored in the contract’s storage at slot[0] (i.e., the first slot). The second variable, calculatedFibNumber, is placed in the next available storage slot, slot[1]. The function setStart takes an input and sets start to whatever the input was. This function therefore sets slot[0] to whatever input we provide in the setStart function. Similarly, the setFibonacci function sets calculatedFibNumber to the result of fibonacci(n). Again, this is simply setting storage slot[1] to the value of fibonacci(n).

Now let’s look at the FibonacciBalance contract. Storage slot[0] now corresponds to the fibonacciLibrary address, and slot[1] corresponds to calculatedFibNumber. It is in this incorrect mapping that the vulnerability occurs. delegatecall preserves contract context. This means that code that is executed via delegatecall will act on the state (i.e., storage) of the calling contract.

Now notice that in withdraw on line 21 we execute fibonacciLibrary.delegatecall(fibSig,withdrawalCounter). This calls the setFibonacci function, which, as we discussed, modifies storage slot[1], which in our current context is calculatedFibNumber. This is as expected (i.e., after execution, calculatedFibNumber is modified). However, recall that the start variable in the FibonacciLib contract is located in storage slot[0], which is the fibonacciLibrary address in the current contract. This means that the function fibonacci will give an unexpected result. This is because it references start (slot[0]), which in the current calling context is the fibonacciLibrary address (which will often be quite large, when interpreted as a uint). Thus it is likely that the withdraw function will revert, as it will not contain uint(fibonacciLibrary) amount of ether, which is what calculatedFibNumber will return.

Even worse, the FibonacciBalance contract allows users to call all of the fibonacciLibrary functions via the fallback function at line 26. As we discussed earlier, this includes the setStart function. We discussed that this function allows anyone to modify or set storage slot[0]. In this case, storage slot[0] is the fibonacciLibrary address. Therefore, an attacker could create a malicious contract, convert the address to a uint (this can be done in Python easily using int('<address>',16)), and then call setStart(<attack_contract_address_as_uint>). This will change fibonacciLibrary to the address of the attack contract. Then, whenever a user calls withdraw or the fallback function, the malicious contract will run (which can steal the entire balance of the contract) because we’ve modified the actual address for fibonacciLibrary. An example of such an attack contract would be:

  1. contract Attack {
  2. uint storageSlot0; // corresponds to fibonacciLibrary
  3. uint storageSlot1; // corresponds to calculatedFibNumber
  4. // fallback - this will run if a specified function is not found
  5. function() public {
  6. storageSlot1 = 0; // we set calculatedFibNumber to 0, so if withdraw
  7. // is called we don't send out any ether
  8. <attacker_address>.transfer(this.balance); // we take all the ether
  9. }
  10. }

Notice that this attack contract modifies the calculatedFibNumber by changing storage slot[1]. In principle, an attacker could modify any other storage slots they choose, to perform all kinds of attacks on this contract. We encourage you to put these contracts into Remix and experiment with different attack contracts and state changes through these delegatecall functions.

It is also important to notice that when we say that delegatecall is state-preserving, we are not talking about the variable names of the contract, but rather the actual storage slots to which those names point. As you can see from this example, a simple mistake can lead to an attacker hijacking the entire contract and its ether.

Preventative Techniques

Solidity provides the library keyword for implementing library contracts (see the docs for further details). This ensures the library contract is stateless and non-self-destructable. Forcing libraries to be stateless mitigates the complexities of storage context demonstrated in this section. Stateless libraries also prevent attacks wherein attackers modify the state of the library directly in order to affect the contracts that depend on the library’s code. As a general rule of thumb, when using DELEGATECALL pay careful attention to the possible calling context of both the library contract and the calling contract, and whenever possible build stateless libraries.

Real-World Example: Parity Multisig Wallet (Second Hack)

The Second Parity Multisig Wallet hack is an example of how well-written library code can be exploited if run outside its intended context. There are a number of good explanations of this hack, such as “Parity Multisig Hacked. Again” and “An In-Depth Look at the Parity Multisig Bug”.

To add to these references, let’s explore the contracts that were exploited. The library and wallet contracts can be found on GitHub.

The library contract is as follows:

  1. contract WalletLibrary is WalletEvents {
  2. ...
  3. // throw unless the contract is not yet initialized.
  4. modifier only_uninitialized { if (m_numOwners > 0) throw; _; }
  5. // constructor - just pass on the owner array to multiowned and
  6. // the limit to daylimit
  7. function initWallet(address[] _owners, uint _required, uint _daylimit)
  8. only_uninitialized {
  9. initDaylimit(_daylimit);
  10. initMultiowned(_owners, _required);
  11. }
  12. // kills the contract sending everything to `_to`.
  13. function kill(address _to) onlymanyowners(sha3(msg.data)) external {
  14. suicide(_to);
  15. }
  16. ...
  17. }

And here’s the wallet contract:

  1. contract Wallet is WalletEvents {
  2. ...
  3. // METHODS
  4. // gets called when no other function matches
  5. function() payable {
  6. // just being sent some cash?
  7. if (msg.value > 0)
  8. Deposit(msg.sender, msg.value);
  9. else if (msg.data.length > 0)
  10. _walletLibrary.delegatecall(msg.data);
  11. }
  12. ...
  13. // FIELDS
  14. address constant _walletLibrary =
  15. 0xcafecafecafecafecafecafecafecafecafecafe;
  16. }

Notice that the Wallet contract essentially passes all calls to the WalletLibrary contract via a delegate call. The constant _walletLibrary address in this code snippet acts as a placeholder for the actually deployed WalletLibrary contract (which was at 0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4).

The intended operation of these contracts was to have a simple low-cost deployable Wallet contract whose codebase and main functionality were in the WalletLibrary contract. Unfortunately, the WalletLibrary contract is itself a contract and maintains its own state. Can you see why this might be an issue?

It is possible to send calls to the WalletLibrary contract itself. Specifically, the WalletLibrary contract could be initialized and become owned. In fact, a user did this, calling the initWallet function on the WalletLibrary contract and becoming an owner of the library contract. The same user subsequently called the kill function. Because the user was an owner of the library contract, the modifier passed and the library contract self-destructed. As all Wallet contracts in existence refer to this library contract and contain no method to change this reference, all of their functionality, including the ability to withdraw ether, was lost along with the WalletLibrary contract. As a result, all ether in all Parity multisig wallets of this type instantly became lost or permanently unrecoverable.