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:
The state variables in ContractA can be read and written.
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.
ContractA 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.
ContractB 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:
These values are set:
address(this) == 0x2791bca1f2de4661ed88a30c99a7a9449aa84174
msg.sender == 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B
msg.value == 2 ETH
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
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!
This is amazing information... really answers many of my questions on delegatecall.
Best article since now about delegatecall. Good job