“With great power, comes great responsibility ”— Uncle Ben
We will try to culminate the series by uttilizing all the knowledge from the previous UIPs and a few other solidtiy features to create the ultimate protocol. In the last part, we tried to use logic and data separation to bring about upgradeability. None of the UIPs have been perfect until now as we had to trade off some or the other factors. We want that we be able to upgrade anything at will, not change address and not having to relist anything. Convenience is what we are looking for. Let’s go all out!
Additional learning before reading this tutorial
- Understand the solidity documentation on delegatecall, memory slots, to understand a few nuances in our implementation (https://solidity.readthedocs.io/en/v0.3.1/introduction-to-smart-contracts.html)(http://solidity.readthedocs.io/en/develop/miscellaneous.html).
UIP3. The Pseudo Token
Our approach throughout the UIPs was to remove code from the token contract. But we have already robbed the Token contract of all its might, what next? The crux of the UIP 2.2 limitation lies in the fact that we have pre-defined skeletal functions like
totalSupply which disallows us from adding any other logic (event, requires, etc.) to the function or adding complete function altogether. Let’s remove them(You should have seen this coming)!
delegatecall is an inbuilt solidity function and will be the vital component of this architecture. Link to the solidity documetation on delegate call was attached in the start of this blog. It looks something like this-
This line will call the function
totalSupply that exists in the contract ‘to’ in the context of the current contract(contract in which this line exists). All of this fits into the
PseudoToken contract. We would use the delegatecall with assembly, because it enables us to pass on the return value.
The inner workings
The approach is plain and simple, but is going to be completely different than UIP 2. We loose the DataCentre, the ControlCentre, the multiple external calls, the extra gas costs, the headache in understanding the call flow and pretty much all of the code in the Token Contract. We just have 2 components. A LogicBank and a PsuedoToken.
The LogicBank can be thought to be the ControlCentre extension. But here, we want to code the LogicBank as if the LogicBank was the Token contract. Just extending our previous examples, this is how the LogicBank would look, a simple ERC20 token with a few additional functions that facilitate the pseudo implementation-
The kill function has been a common feature of our previous UIPs. The need for the initialize function will be explained later.
The trick here lies in the fact that none of the functions expect
kill will ever be called in the context of this contract. All of that will happen in the PseudoToken, which contains some high level gibberish and again, is practically devoid of any ERC20 functions!
A quick question- What would happen when you call the transfer function of this Token contract, which doesnt even have a transfer function? Lets say transfer 10 tokens from
address A- 0xf8bd89bfca1c5db120971bc0f7423be720413d35 (msg.sender)
address B- 0xacaeb46e5ae394e3011bad7ee50a7e6eee63e3c0
All of the call data for transfer, i.e
‘falls’ to the fallback function in the msg.data.
Now, the pseudo just takes this
msg.data, and delegates the call to the LogicBank. From here on, the transfer function will be executed in the context of the PseudoToken, i.e as if it was a function IN the PseudoToken contract. The msg.sender will still be the address A.
The storage slots also get created in the PseudoToken as and when required. When you transfer from address A to B(assuming B had no balance before),
balances[B] = 10 is created in the PseudoToken.
Now tracking back to the need for the initialize function. We cannot have a constructor in the PseudoToken that would initialize the balances for the contract creator, simply because the PseudoToken doesn’t even know it is going to have the ERC20 variables at this point. All of that can only happen after the ownership of the PseudoToken is transferred to the LogicBank. Like always, implementation might tie up loose ends in your understanding.
To much written, lets hit the code!
The deployment sequence for this simple example is as follows-
- Deploy the PseudoToken.
- Deploy the LogicBank with the PseudoToken address in constructor params
transferOwnershipof the PseudoToken to the LogicBank
This time it is not a 2 way link, but just a way to keep the upgradeability format similar to the UIP 2’s
Steps to reproduce working on remix-
- After completing the deployment sequence, you would have 2 contracts on your remix run tab, ideally the left part of the above image.
- Copy the address of the PseudoToken contract and create an instance using the LogicBank ABI, like the right part of the above image.
- Click on the ‘At Address’ button to create the instance.
This instance will allow us to call all the ERC20 functions at the PseudoToken address.
initialize the PseudoToken contract and assign yourself some tokens.
Try out the
totalSupply, transfer functions of the PseudoToken and witness magic!
How does it upgrade?
It is all just the same now-
- Deploy the new LogicBank
killthe old LogicBank contract and enter the new LogicBank address into the params.
transferOwnershipof the PseudoToken to the new LogicBank contract.
- Before step 3, transferring ownership of the PseudoToken to the LogicBank, you have no superpowers! You cannot access any of the ERC20 functions.
- Always try to keep the storage layout of all the LogicBank contracts similar.
- The storage space separation between the LogicBank and the Token can be puzzling at times.
Advantages of the PseudoToken-
- Reduction in gas cost than UIP 2’s and a much simpler call flow.
- Upgradeability max: Add new storage variables, functions, events and what not!
Albeit Perfect, a few major limitations/warnings-
- This time around all the STORAGE variables are in the Token contract. This contract can never be modified in case of bugs.
- There were cases in previous solidity versions when the Pseudo storage slots are overwritten with blank slots. The slots had to aligned to take care of this factor because if the owner of the Pseudo is overwritten to address
0x00, access to all the data is lost forever.
Everything comes at a cost. Extreme caution must be exercised while using this architecture! Only after thorough testing should it be deployed to the mainnet.
And that’s about it! If you were able to survive through the complete series, you would have learnt a number of the upgradeability protocols to save your day. Based on your use case, requirement and personal preference you can choose the UIP of your choice to build smart contracts in the future. Never again should a contract owner feel helpless because of the inability to fix his mistakes!
We will be back with more blogs and series on blockchain in general. Keep close watch for future posts that might explore into blockchain scalability initiatives, other smart contract platforms like NEO, Lisk, Stellar, EOS and more in depth content on ethereum.
Thanks for reading!