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:
</aside>
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 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) }
}
Because we are working across 2 contracts, we might have the following situation:
|Proxy |
|--------------------------|
|address _implementation |
|... |
| |
| |
|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