Why proxies can't read from the constructor?
2 Answers
This was a question which was bothering me for a long time too and I also couldn't find a proper explanation. Let me summarise how I understood it:
2.Taking a closer look, it is good to see that each smart contract has code and storage.
- Storage is the place where we store the changes from saved to the smart contract.
- Code is the place where the smart contract code is stored as well as all the information included in the constructor
- Now see that those are 2 different places.
- This is why we can't use constructor. Because when we use the constructor we will save everything passed inside, to the code location and not to the storage.
- Remember Proxies read from Storage location not from code location.
- That is why we need to use initialisation, because whatever we save into the initialisation will be stored to the storage. The name initialisation is not fixed like storage, you can name it whatever you want. Important is that it is called only once and called from the person who deploys the contract, because if it is called from someone else and it changes some roles like admin, it will make the first caller the admin.
- Remember proxies read from Storage not from code.
I hope you that it is more clear now, but in order to understand it it important to understand the difference between those 2 storage and code. I think this is missing in most of the explanations and lead to so many confusions.
Proxies need to run instructions that are stored in a different address (the implementation) but using their own storage. That's why they run delegatecall
instead of call
.
The constructor is a special function that is run only once when the contract is deployed. It can never be run again and it is run by the contract being deployed on its own storage.
Let's think of an example. Say your contract needs to have roles. You want to give a role "API" to some hard-coded address. If you were to implement it by adding this constructor to your implementation contract:
mapping(address => bytes32) _roles;
constructor() {
_roles[0xwhatever] = keccak256("API");
}
function isAPI(address _addr) public view returns(bool) {
return _roles[_addr] == keccak256("Super Admin");
}
Then these roles would be stored in the implementation contract's storage. So if you were to ask your proxy if 0xwhatever
has the role, your proxy would run the isAPI
piece of code present in the implementation contract but on the proxy's storage (because it uses delegatecall
). According to the proxy's storage, that address has no role.
On the other hand, with an initialize()
method:
function initialize() external onlyProxy {
_roles[0xwhatever] = keccak256("API");
}
Then after you have deployed the implementation contract, your proxy would be able to call that method using delegatecall
and the role would be saved to the proxy's storage