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
// library contract - calculates Fibonacci-like numbers
contract FibonacciLib {
// initializing the standard Fibonacci sequence
uint public start;
uint public calculatedFibNumber;
// modify the zeroth number in the sequence
function setStart(uint _start) public {
start = _start;
}
function setFibonacci(uint n) public {
calculatedFibNumber = fibonacci(n);
}
function fibonacci(uint n) internal returns (uint) {
if (n == 0) return start;
else if (n == 1) return start + 1;
else return fibonacci(n - 1) + fibonacci(n - 2);
}
}
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
contract FibonacciBalance {
address public fibonacciLibrary;
// the current Fibonacci number to withdraw
uint public calculatedFibNumber;
// the starting Fibonacci sequence number
uint public start = 3;
uint public withdrawalCounter;
// the Fibonancci function selector
bytes4 constant fibSig = bytes4(sha3("setFibonacci(uint256)"));
// constructor - loads the contract with ether
constructor(address _fibonacciLibrary) external payable {
fibonacciLibrary = _fibonacciLibrary;
}
function withdraw() {
withdrawalCounter += 1;
// calculate the Fibonacci number for the current withdrawal user-
// this sets calculatedFibNumber
require(fibonacciLibrary.delegatecall(fibSig, withdrawalCounter));
msg.sender.transfer(calculatedFibNumber * 1 ether);
}
// allow users to call Fibonacci library functions
function() public {
require(fibonacciLibrary.delegatecall(msg.data));
}
}
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:
contract Attack {
uint storageSlot0; // corresponds to fibonacciLibrary
uint storageSlot1; // corresponds to calculatedFibNumber
// fallback - this will run if a specified function is not found
function() public {
storageSlot1 = 0; // we set calculatedFibNumber to 0, so if withdraw
// is called we don't send out any ether
<attacker_address>.transfer(this.balance); // we take all the ether
}
}
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:
contract WalletLibrary is WalletEvents {
...
// throw unless the contract is not yet initialized.
modifier only_uninitialized { if (m_numOwners > 0) throw; _; }
// constructor - just pass on the owner array to multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit)
only_uninitialized {
initDaylimit(_daylimit);
initMultiowned(_owners, _required);
}
// kills the contract sending everything to `_to`.
function kill(address _to) onlymanyowners(sha3(msg.data)) external {
suicide(_to);
}
...
}
And here’s the wallet contract:
contract Wallet is WalletEvents {
...
// METHODS
// gets called when no other function matches
function() payable {
// just being sent some cash?
if (msg.value > 0)
Deposit(msg.sender, msg.value);
else if (msg.data.length > 0)
_walletLibrary.delegatecall(msg.data);
}
...
// FIELDS
address constant _walletLibrary =
0xcafecafecafecafecafecafecafecafecafecafe;
}
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.