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 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 .)
Example 6. FibonacciLib.sol
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
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 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 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 (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 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:
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 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 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 .
The library contract is as follows:
And here’s the wallet contract:
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?