There are quite a few caveats to Upgradable Smart contracts and various approaches.
In the case of the proxy method you are referring to the Proxy Smart contract interacts with a Logic Smart contract.
The contract state is stored in the Proxy Contract and Logic is executed via the Logic contract. When Upgrades are performed it is referring to creating a different logic contract to be associated with the Proxy Contract.
The relationship between the Proxy contract and the Logical one with respect to Upgradability is made possible via EVM’s opcode delegatecall
. This executes the callee’s code in the context of the caller’s state.
The logic contract controls the proxy’s state and the logic contract’s state is meaningless. Thus, the proxy doesn’t only forward transactions to and from the logic contract, but also represents the pair’s state. The state is in the proxy and the logic is in the particular implementation that the proxy points to.
[1] https://docs.soliditylang.org/en/v0.8.6/introduction-to-smart-contracts.html?highlight=Delegatecall#delegatecall-callcode-and-libraries
The proxy is a simple contract that just delegates all calls to an implementation contract. A delegate call is similar to a regular call, except that all code is executed in the context of the caller, not of the callee. Because of this, a transfer in the implementation contract’s code will actually transfer the proxy’s balance, and any reads or writes to the contract storage will read or write from the proxy’s own storage.
[2] https://docs.openzeppelin.com/learn/upgrading-smart-contracts#how-upgrades-work
Another important consideration is the possibility of Storage Collisions which has solutions in using an unstructured storage for the initial deployment and then appending the storage hierarchy in new versions.
[3] https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies#the-constructor-caveat
For Upgradable Smart Contracts you cannot use a constructor because 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. Constructor code is executed only once, when the contract instance is deployed. As a consequence of this, the code within a logic contract’s constructor will never be executed in the context of the proxy’s state. To rephrase, proxies are completely oblivious to the existence of constructors. It’s simply as if they weren’t there for the proxy.
[3] https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies#the-constructor-caveat
The solution to this is to to implement an initialization function instead of a constructor. Moreover if you are using OpenZeppelin it is imperative that you use the openzeppelin-contracts-upgradeable
as all subcontracts will have to have this structure as well.
[4] https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
This caveat further applies to the initial values on field declarations which are equivalent to constructors i.e.
contract MyContract {
uint256 public hasInitialValue = 42; // equivalent to setting in the constructor
}
This should be set as
contract MyContract is Initializable {
uint256 public hasInitialValue;
function initialize() public initializer {
hasInitialValue = 42; // set initial value in initializer
}
}
[5] https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#avoid-initial-values-in-field-declarations
Finally there is the also the consideration of function clash (same function name in Proxy and Logic). This is can be solved using the transparent proxy pattern
[6] https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies#transparent-proxies-and-function-clashes
Unavoidably however clashing can also happen among functions with different name. This is because every contracts ABI is identified - at a bytecode level - by a 4-byte identifier. With only 4-bytes - it's possible that two functions with different names may have the same identifier. Solidity compiler tracks this within the same contract - however in the case of proxy contracts -> logic contract the compiler cannot track it because its two separate smart contracts.
[7] https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357
So the overall summary here is that upgradeable smart contract instances (proxies) work by delegating all calls to a logic smart contract. Made possible by the low level call delegatecall
. Constructors cannot be used as they are not part of a deployed smart contract’s runtime bytecode and thus the code within a logic contract’s constructor will never be executed in the context of the proxy’s state. The latter as related to constructors is likewise true for initial values on field declarations.
pausable
and these kind of flag to shut down the execution