On Proxies — Ethernaut Delegate Call Challenge

Yong kang Chia
6 min readNov 9, 2022

--

In this article, I will be going through the solution for Ethernaut Challenge 6 as well as diving deeper into delegateCall and how we can implement it in production with different patterns.

Key idea:

  • delegate call
  • msg.data

Delegate call

  • 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.

Solution

  • For this problem, the address(delegate).delegatecall(msg.data) calls a delegate-type contract
  • As we know delegate call can mutate the state of the contract calling it
  • It is also convinient that we have a contract pwn() for controlling the owner of the contract
  • Therefore all we need to do is to call the fallback function to get the delegate call to call pwn() as the msg.data

So what is msg.data in this case?

  • msg.data is the bytecode-encoded version of the function signature

Approach

  1. Get the bytecode for pwn()
  2. Make a call with bytecode-encoded function signature to the contract which will trigger the fallback function

In Javascript:

const encodedSignature = web3.eth.abi.encodeFunctionSignature("pwn()")await web3.eth.sendTransaction({from: player, to: contract.address, data: encodedSignature})

In solidity

Hashing with keccak256 and then casting it to bytes4 is exatcly how Solidity generates function signatures, so this should work.

bytes4 encodedSignature = bytes4(keccak256("pwn()"));

Or

bytes4 encodedSignature = Delegate(0).pwn.selector

Followed by

(bool success, ) = address(contract.address).call(encodedSignature);

How To Use delegate call Safely

1. Control what is executed with delegatecall

  • Use permissions or authentication or some other form of control for specifying or changing what functions and contracts are 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.

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.
  • Check for address if unsure
code to check address

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:

Strategies used 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.

Limitations

  • 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 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.

Apps Storage

Similar to Inherited Storage, 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.

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

AppStorage 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.

Advantages

  • 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 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:
  • 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 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.

Readings

Diamond storage

  • This is a popular pattern as seen in eip2535
  • 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.
  • 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.

Advantages

  • One is that the ERC721 contract is reusable.
  • 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.

Recommended readings

--

--

Yong kang Chia

Blockchain Developer. Chainlink Ex Spartan Group, Ex Netherminds