The first contract is a simple wrapper or "proxy" which users interact with directly and is in charge of forwarding transactions to and from the second contract, which contains the logic.
The proxy contract has a fallback function. This function gets triggered if a tx calls a function that does not exist on the contract.
The fallback function delegate calls the tx to the implmentation contract. (1) the calldata is copied to memory, (2) the call is forwarded to the logic contract, (3) the return data from the call to the logic contract is retrieved, and (4) the returned data is forwarded back to the caller.
Storage in Proxies
If the proxy stores the implementation address is the 0th slot and the implementation has some variable stored in it’s 0th slot, there will be storage collision.
imagine that the first implementation of the logic contract stores address public _owner at the first storage slot and an upgraded logic contract stores address public _lastContributorat the same first slot. When the updated logic contract attempts to write to the _lastContributorvariable, it will be using the same storage position where the previous value for _owner was being stored, and overwrite it!
New variables in different implementation should always be stored after the old variables.
Constructor
In Solidity, code that is inside a constructor or part of a global variable declaration is not part of a deployed contract’s runtime bytecode. This code is executed only once, when the contract instance is deployed.
proxies are completely oblivious to the storage trie changes that are performed by the constructor.
This is solved using the initialize function
To ensure that the initialize function can only be called once, a simple modifier is used. OpenZeppelin Upgrades provides this functionality via a contract that can be extended:
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract MyContract is Initializable {
function initialize(
address arg1,
uint256 arg2,
bytes memory arg3
) public payable initializer {
// "constructor" code...
}
}
Function clashes
proxies need some functions of their own, such as upgradeTo(address) to upgrade to a new implementation. This begs the question of how to proceed if the logic contract also has a function named upgradeTo(address): upon a call to that function, did the caller intend to call the proxy or the logic contract?
A transparent proxy will decide which calls are delegated to the underlying logic contract based on the caller address (i.e., the msg.sender):
If the caller is the admin of the proxy (the address with rights to upgrade the proxy), then the proxy will not delegate any calls, and only answer any messages it understands.
If the caller is any other address, the proxy will always delegate a call, no matter if it matches one of the proxy’s functions.
msg.sender
owner()
upgradeto()
transfer()
Owner
returns proxy.owner()
returns proxy.upgradeTo()
fails
Other
returns erc20.owner()
fails
returns erc20.transfer()
OpenZeppelin Upgrades uses an intermediary ProxyAdmin contract for each transparent proxy. Even if you call the deploy command from your node’s default account, the ProxyAdmin contracts will be the actual admins of your transparent proxies. This means that you will be able to interact with the proxies from any of your node’s accounts, without having to worry about the nuances of the transparent proxy pattern.