This page details some learnings about upgradeable contract patterns.

<aside> ⚠️ Adding storage variables in the wrong place in Upgradeable contracts can be really, really bad, Audius found this out recently, you can read about it here:

Audius Exploit in Depth

</aside>

Basics

Whenever a contract A delegates a call to another contract B, it executes the code of contract B in the context of contract A. This means that msg.value and msg.sender values will be kept and every storage modification will impact the storage of contract A.

OZ DelegateCall

OZ Proxies make heavy use of delegatecall but not the native solidity version, as it does not return anything other than a boolean.

Specifically, OZ uses a simple assembly function to copy the return data from delegate call, into memory, and return it. Full details are in the Proxy Patterns blog below (not too hard to understand), but in short, the following code is added to the fallback function:

assembly {
	// initialise pointer at FMP address
  let ptr := mload(0x40)

  // (1) copy incoming call data
  calldatacopy(ptr, 0, calldatasize)

  // (2) forward call to logic contract
  // default delegatecall return is a boolean
  let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
  
  // (3) retrieve return data and size
  let size := returndatasize  
  returndatacopy(ptr, 0, size)

  // (4) forward return data back to caller
  switch result
  case 0 { revert(ptr, size) }
  default { return(ptr, size) }
}

Unstructured Storage and Collisions

Because we are working across 2 contracts, we might have the following situation:

  1. Proxy Stores in slot 1 the address of implementation contract
|Proxy                     |
|--------------------------|
|address _implementation   |
|...                       |
|                          |
|                          |
  1. Implementation stores all the storage variables
|Implementation           |
|-------------------------|
|address _owner           |
|mapping _balances        |
|uint256 _supply          |
|...                      |

When the logic contract writes to _owner, it does so in the scope of the proxy’s state, and in reality writes to _implementation