Understanding delegatecall And How to Use It Safely

Some developers are afraid of ‘delegatecall’ because they have been told it is “dangerous”. Fear and danger come from not understanding how something works and how to use it safely. For example most of us are not afraid to drive a car because we understand enough about how it works and we know how to do it safely.

When a contract makes a function call using delegatecall it loads the function code from another contract and executes it as if it were its own code.

When a function is executed with delegatecall these values do not change:

  • address(this)

  • msg.sender

  • msg.value

Reads and writes to state variables happen to the contract that loads and executes functions with delegatecall. Reads and writes never happen to the contract that holds functions that are retrieved.

So if ContractA uses delegatecall to execute a function from ContractB then the following two points are true:

  1. The state variables in ContractA can be read and written.

  2. The state variables in ContractB are never read or written.

Both ContractA and ContractB can declare the same state variables, and ContractB’s functions can read and write values to these state variables. But only ContractA’s state variables are ever read or written.

delegatecall affects the state variables of the contract that calls a function with delegatecall. The state variables of the contract that holds the functions that are borrowed are not read or written.

Example

Let’s look at a simple example.

Contract1 has the following:

  • address(this) == 0x2791bca1f2de4661ed88a30c99a7a9449aa84174

  • A state variable ‘string tokenName’ has the value “FunToken”

  • An external function called `initialize()` that calls the `setTokenName(string calldata _newName)` function in ContractB with delegatecall.

Contract2 has the following:

  • address(this) == 0x6b175474e89094c44da98b954eedeac495271d0f

  • A state variable ‘string tokenName’ has the value “BoringToken”

  • An external function called `setTokenName(string calldata _newName)` that sets the `tokenName` state variable to the ‘_newName’ value

When the `initialize()` function in ContractA is called with 2 ETH this is what happens:

  1. These values are set:

    • address(this) == 0x2791bca1f2de4661ed88a30c99a7a9449aa84174

    • msg.sender == 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B

    • msg.value == 2 ETH

  2. The `initialize()` function calls the `setTokenName` function in ContractB using delegatecall. Here are the values within `setTokenName` when it is executed. Notice they didn’t change.

    • address(this) == 0x2791bca1f2de4661ed88a30c99a7a9449aa84174

    • msg.sender == 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B

    • msg.value == 2 ETH

  3. The `string tokenName` state variable value is changed in ContractA. It is not changed in ContractB, even though the code came from ContractB.

Here is how the code looks:

Addresses in Solidity have a `delegatecall` method that enables you to execute delegatecall. This delegatecall method returns a boolean status variable that tells you if the function call reverted or not. The delegatecall function returns a second value which is any return value from the function call. See the code example above.

How To Use delegatecall Safely

Now that you understand how delegatecall works let’s look at how to use it safely.

1. Control what is executed with delegatecall

Do not execute untrusted code with delegatecall because it could maliciously modify state variables or call `selfdestruct` to destroy the calling contract. Use permissions or authentication or some other form of control for specifying or changing what functions and contracts are executed with delegatecall.

2. Only call delegatecall on addresses that have code

Delegatecall will return ‘True’ for the status value if it is called on an address that is not a contract and so has no code. This can cause bugs if code expects delegatecall functions to return `False` when they can’t execute.

If you are unsure if an address variable will always hold an address that has code and delegatecall is used on it then check that any address from the variable has code before using delegatecall on it and revert if it doesn’t have code. Here is an example of code that checks if an address has code:

3. Manage State Variable Layout

Solidity stores data in contracts using a numeric address space. The first state variable is stored at position 0, the next state variable is stored at position 1, the next state variable is stored at position 2, etc.

A contract and function that is executed with delegatecall shares the same state variable address space as the calling contract, because functions called with delegatecall read and write the calling contract’s state variables.

Therefore a contract and function that is called with delegatecall must have the same state variable layout for state variable locations that are read and written to. Having the same state variable layout means that the same state variables are declared in the same order in both contracts.

If a contract that calls delegatecall and the contract with the borrowed functions do not have the same state variable layout and they read or write to the same locations in contract storage then they will overwrite or incorrectly interpret each other’s state variables.

For example let’s say that a ContractA declares state variables ‘uint first;’ and ‘bytes32 second;’ and ContractB declares state variables ‘uint first;’ and ‘string name;’. They have different state variables at postion 1 (‘bytes32 second’ and ‘string name’) in contract storage and so they will write and read wrong data between them at position 1 if delegatecall is used between them.

Managing the state variable layout of contracts that call functions with delegatecall and the contracts that are executed with delegatecall is not hard to do in practice when a strategy is used to do it. Here are some known strategies that have been used successfully in production:

Inherited Storage

One strategy is to create a contract that declares all state variables used by all contracts that share a contract storage addess space because they use delegatecall between them. It could be called ‘Storage’ or something. It could then be inherited by every contract that shares the same storage address space. This strategy works but it has limitations and in my opinion I’ve found a similar but better strategy.

A limitation Inherited Storage has is that it prevents contracts from being reusable. If you deploy a contract that uses Inherited Storage then you likely won’t be able to reuse that deployed contract with different contracts that have different state variables when using delegatecall.

Another limitation, in my opinion, is that it is too easy to accidentally name something like an internal function or local variable the same name as a state variable and have a name clash. But this could be overcome by using code naming conventions that prevent such name clashes.

Diamond Storage

Contracts that use delegatecall between them do not actually have to declare the same state variables in the same order if they store data at different locations.

As mentioned earlier, Solidity automatically stores state variables at storage locations starting from 0 and incrementing by one. But we don’t have to use Solidity’s default storage layout mechanism. We don’t have to store data starting at location 0. We can specify where to start storing data in the address space. For different contracts we can specify different locations to start storing data, therefore preventing different contracts with different state variables from clashing storage locations. This is what Diamond Storage does.

We can hash a unique string to get a random storage position and store a struct there. The struct can contain all the state variables that we want. The unique string can act like a namespace for particular functionality.

For example we could implement an ERC721 contract. This contract could store a struct called ‘ERC721Storage’ at position ‘keccak256("com.myproject.erc721");’. The struct could contain all the state variables related to ERC721 functionality that the ERC721 contract reads and writes. There are a couple nice advantages to this. One is that the ERC721 contract is reusable. The ERC721 contract can be deployed only once, and the deployed ERC721 contract can be used with multiple different contracts that use delegatecall with it and that are using different state variables. Another nice thing is that the ERC721 contract is not cluttered with state variable declarations of variables it doesn’t use.

Another nice advantage to Diamond Storage is that it is possible for the internal functions of Solidity libraries to access Diamond Storage just like any regular contract function. I wrote a blog post about using Solidity libraries with Diamond Storage here: Solidity Libraries Can't Have State Variables -- Oh Yes They Can!

For more information about Diamond Storage and a code example, see this blog post: How Diamond Storage Works. I also recommend reading Understanding Diamonds on Ethereum.

AppStorage

AppStorage is similar to Inherited Storage but it solves the name clash problem where it is too easy to accidentally name something like an internal function or local variable the same name as a state variable. This might seem a trivial matter but I found in practice it is very nice because AppStorage also distinguishes code in a way that makes it easier to scan and read. If you care about code readability then you will like AppStorage.

AppStorage enforces a naming or access convention that makes it impossible to clash names of state variables with something else.

A struct called AppStorage is written in a Solidity file. The AppStorage struct contains state variables that will be shared between contracts. To use it a contract imports the AppStorage struct and declares `AppStorage internal s;` as the first and only state variable in the contract. The contract then accesses all state variables in functions via the struct like this: `s.myFirstVariable`, `s.mySecondVariable`, etc. Here is an example:

It is important that ‘AppStorage internal s;’ is declared as the first and only state variable in all contracts that use it. That puts it at position 0 in the storage address space. So if all contracts declare it as the first and only state variable then the storage data between contracts that use delegatecall will line up correctly. Don’t add state variables directly to a contract because that will clash with the state variables declared in the AppStorage struct. To add more state variables add them to the end of the AppStorage struct or use Diamond Storage.

AppStorage is more convenient to use than Diamond Storage because in every function Diamond Storage requires getting a pointer to a struct whereas with AppStorage the `s` struct pointer is automatically available throughout a contract.

Another advantage that AppStorage has over Inherited Storage is that AppStorage can be accessed by Solidity libraries in the same way that Diamond Storage can. An AppStorage struct is always stored at location 0 so internal functions in Solidity libraries can use this to initialize the ‘s’ storage pointer to point to the AppStorage struct. Here is an example of that:

A storage pointer to an AppStorage struct can also be passed into library functions as an argument, as can be seen in the `myLibraryFunction2` function above.

AppStorage can be used with contract inheritance. This is done by declaring ‘AppStorage internal s;’ in a contract. Then all contracts that use AppStorage inherit that contract.

AppStorage is particularly useful for application or project specific contracts that won’t be resused with other projects or contracts that also use AppStorage or Inherited Storage. AppStorage can be used with Diamond Storage in the same contract.

AppStorage is also useful in smart contracts that don’t use delegatecall because it makes code more readable and prevents name clashes.

Checkout this blog post about AppStorage for more information: AppStorage Pattern for State Variables in Solidity

Systems that Use Delegatecall

Here are smart contract architectures that use delegatecall:

Proxy Contracts

A proxy contract uses delegatecall to delegate external function calls to an implementation contract. Proxy contracts are used to implement upgradeable contracts. To upgrade a proxy contract it is pointed to a different implementation contract.

Different versions of implementation contracts used by the same proxy contract must use a strategy to manage state variable layout. Otherwise different implementations can read and write data at the wrong storage locations. Implementation contracts can use Inherited Storage or Diamond Storage or AppStorage.

OpenZeppelin provides support for proxy contracts.

The SolidState smart contract library supports proxy contracts and implementation contracts that use Diamond Storage.

EIP-2535 Diamonds

EIP-2535 Diamonds is a standard that supports building modular smart contract systems that can be extended in production.

A diamond is a proxy contract that has multiple implementation contracts. Learn more about diamonds from the standard and this introduction: Introduction to the Diamond Standard, EIP-2535 Diamonds

The implementation contracts of diamonds, called facets, can use Inherited Storage, Diamond Storage and AppStorage.

Several reference implementations, which have been audited, exist for getting started with diamonds. See here: diamond reference implementations.

The solidstate-solidity smart contract library supports diamonds.

The hardhat-deploy plugin supports deploying and upgrading diamonds.

Solidity Libraries

Solidity libraries are not a smart contract architecture, like proxies and diamonds. They are a tool and part of the Solidity language.

From Solidity documentation:

Libraries are similar to contracts, but their purpose is that they are deployed only once at a specific address and their code is reused using the DELEGATECALL feature of the EVM.

The external functions of Solidity libraries are executed using delegatecall.

Solidity libraries can access state variables by using storage pointers as parameters to functions. Solidity libraries can also access and use Diamond Storage and AppStorage. Here’s an article that shows how Solidity libraries can use Diamond Storage: Solidity Libraries Can't Have State Variables -- Oh Yes They Can!

Find a job developing smart contracts using Solidity at delegatecall.careers