This repository has been archived by the owner on Jan 24, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 200
Adds a new section to the documentation on "proxy patterns" #288
Merged
Merged
Changes from 6 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
3fc5ba1
[CI skip] First draft of a new section describing the unstructured st…
eternauta1337 72778e2
[CI skip] Finished first draft of Proxy section.
eternauta1337 f80ade0
[CI skip] Merged some items in advanced.md to proxies.md
eternauta1337 d7c1bcb
[CI skip] Assimilated some of elopio's reviews
eternauta1337 253cdcb
[CI skip] Minor spelling fixes
eternauta1337 e2f6c63
[CI skip] Added a few diagrams to the proxies section.
eternauta1337 19468e8
random -> fixed
come-maiz df3fae0
Merge branch 'master' into docs-proxy-patterns
come-maiz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
--- | ||
id: proxies | ||
title: Proxy Pattern | ||
--- | ||
|
||
This article describes the "unstructured storage" proxy pattern, the fundamental building block of ZeppelinOS's upgrades. | ||
|
||
Note: For a more in depth read, please see [blog.zeppelinos.org/proxy-patterns](https://blog.zeppelinos.org/proxy-patterns/), which discusses the need for proxies, goes into more technical detail on the subject, elaborates on other possible proxy patterns that were considered for zOS, and more. | ||
|
||
## Why upgrade a contract? | ||
|
||
By design, smart contracts are immutable. On the other hand, software quality heavily depends on the ability to upgrade and patch source code in order to produce iterative releases. Even though blockchain based software profits significantly from the technology's immutability, still a certain degree of mutability is needed for bug fixing and potential product improvements. ZeppelinOS solves this apparent contradiction by providing an easy to use, simple, robust, and opt-in upgrade mechanism for smart contracts that can be controlled by any type of governance, be it a multi-sig wallet, a simple address or a complex DAO. | ||
|
||
## Upgrading via the proxy pattern | ||
|
||
The basic idea behind using a proxy for upgrades. 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 key concept to understand is that the logic contract can be replaced while the proxy, or the access point is never changed. Both contracts are still immutable in the sense that their code cannot be changed, but the logic contract can simply be swapped by another contract. The wrapper can thus point to a different logic implementation and in doing so, the software is "upgraded". | ||
|
||
User ---- tx ---> Proxy ----------> Implementation_v0 | ||
| | ||
------------> Implementation_v1 | ||
| | ||
------------> Implementation_v2 | ||
|
||
## Proxy forwarding | ||
|
||
The most immediate problem that proxies need to solve is how the proxy exposes the entire interface of the logic contract without requiring a one to one mapping of the entire logic contract's interface. That would be difficult to maintain, prone to errors, and would make the interface itself not upgradeable. Hence, a dynamic forwarding mechanism is required. The basics of such mechanism are presented in the code below: | ||
|
||
```solidity | ||
assembly { | ||
let ptr := mload(0x40) | ||
calldatacopy(ptr, 0, calldatasize) // (1) copy incoming call data | ||
let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0) // (2) forward call to logic contract | ||
let size := returndatasize | ||
returndatacopy(ptr, 0, size) // (3) retrieve return data | ||
|
||
switch result | ||
case 0 { revert(ptr, size) } | ||
default { return(ptr, size) } // (4) forward return data back to caller | ||
} | ||
``` | ||
|
||
This code can be put in the [fallback function](https://solidity.readthedocs.io/en/v0.4.21/contracts.html#fallback-function) of a proxy, and will forward any call to any function with any set of parameters to the logic contract without it needing to know anything in particular of the logic contract's interface. In essence, (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. The technique needs to be implemented using Yul because [Solidity's `delegatecall`](https://solidity.readthedocs.io/en/v0.4.21/introduction-to-smart-contracts.html#delegatecall-callcode-and-libraries) returns a boolean instead of the callee's return data. | ||
|
||
A very important thing to note is that the code makes use of the EVM's `delegatecall` opcode which executes the callee's code in the context of the caller's state. That is, 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. | ||
|
||
## Unstructured storage proxies | ||
|
||
A problem that quickly comes up when using proxies has to do with the way in which variables are stored in the proxy contract. Suppose that the proxy stores the logic contract's address in it's only variable `address public _implementation;`. Now, suppose that the logic contract is a basic token whose first variable is `address public _owner`. Both variables are 32 byte in size, and as far as the EVM knows, occupy the first slot of the resulting execution flow of a proxied call. When the logic contract writes to `_owner`, it does so in the scope of the proxy's state, and thus really writes to `_implementation`. This problem can be referred to as a "storage collision". | ||
|
||
|Proxy |Implementation | | ||
|--------------------------|-------------------------| | ||
|address _implementation |address _owner | <=== Storage collision! | ||
|... |mapping _balances | | ||
| |uint256 _supply | | ||
| |... | | ||
|
||
There are many ways to overcome this problem, and the "unstructured storage" approach which ZeppelinOS implements works as follows. Instead of storing the `_implementation` address at the proxy's first storage slot, it chooses a pseudo random slot instead. This slot is sufficiently random, that the probability of a logic contract declaring a variable at the same slot is negligible. The same principle of randomizing slot positions in the proxy's storage is used in any other variables the proxy may have, such as an admin address (that is allowed to update the value of `_implementation`), etc. | ||
|
||
|Proxy |Implementation | | ||
|--------------------------|-------------------------| | ||
|... |address _owner | | ||
|... |mapping _balances | | ||
|... |uint256 _supply | | ||
|... |... | | ||
|... | | | ||
|... | | | ||
|... | | | ||
|... | | | ||
|address _implementation | | <=== Randomized slot. | ||
|... | | | ||
|... | | | ||
|
||
An example of how the randomized storage is achieved: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wouldn't use the word random, it sounds not-robust, I'd rather use something like |
||
|
||
``` | ||
bytes32 private constant implementationPosition = keccak256("org.zeppelinos.proxy.implementation"); | ||
``` | ||
|
||
As a result, a logic contract doesn't need to care about overwriting any of the proxy's variables. Other proxy implementations that face this problem usually imply having the proxy know about the logic contract's storage structure and adapt to it, or instead having the logic contract know about the proxy's storage structure and adapt to it. This is why this approach is called "unstructured storage"; neither of the contracts needs to care about the structure of the other. | ||
|
||
## Storage collisions between implementation versions | ||
|
||
As discussed, the unstructured approach avoids storage collisions between the logic contract and the proxy. However, storage collisions between different versions of the logic contract can occur. In this case, 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 _lastContributor` at the same first slot. When the updated logic contract attempts to write to the `_lastContributor` variable, it will be using the same storage position where the previous value for `_owner` was being stored, and overwrite it! | ||
|
||
#### Incorrect storage preservation: | ||
|
||
|Implementation_v0 |Implementation_v1 | | ||
|--------------------|-------------------------| | ||
|address _owner |address _lastContributor | <=== Storage collision! | ||
|mapping _balances |address _owner | | ||
|uint256 _supply |mapping _balances | | ||
|... |uint256 _supply | | ||
| |... | | ||
|
||
#### Correct storage preservation: | ||
|
||
|Implementation_v0 |Implementation_v1 | | ||
|--------------------|-------------------------| | ||
|address _owner |address _owner | | ||
|mapping _balances |mapping _balances | | ||
|uint256 _supply |uint256 _supply | | ||
|... |address _lastContributor | <=== Storage extension. | ||
| |... | | ||
|
||
The unstructured storage proxy mechanism doesn't safeguard against this situation. It is up to the user to have new versions of a logic contract extend previous versions, or otherwise guarantee that the storage hierarchy is always appended to but not modified. ZeppelinOS' CLI does however| Tables | Are | Cool | | ||
| ------------- |:-------------:| -----:| | ||
| col 3 is | right-aligned | $1600 | | ||
| col 2 is | centered | $12 | detect such collisions, and warns the developer appropriate. | ||
|
||
## The constructor caveat | ||
|
||
In Solidity, code that is inside a constructure 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. 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 existance of constructors. It's simply as if they weren't there for the proxy. | ||
|
||
The problem is easily solved though. Logic contracts should move the code within the constructor to a regular 'initializer' function, and have this function be called whenever the proxy links to this logic contract. Special care needs to be taken with this initializer function so that it can only be called once, which is one of the properties of constructors in general programming. | ||
|
||
This is why when the zOS CLI creates a proxy, it allows you to indicate an initializer function: | ||
|
||
```bash | ||
npx zos create MyLogicContract --init initialize --args arg1,arg2,arg3 | ||
``` | ||
|
||
With this command, ZeppelinOS creates a proxy that wraps around `MyLogicContract`, uses `MyLogicContract` as the logic contract, and calls the logic contract's `initialize` function. | ||
|
||
To ensure that the `initialize` function can only be called once, a simple modifier is used. ZeppelinOS provides this functionality via a contract that can be extended: | ||
|
||
```solidity | ||
|
||
import "zos-lib/contracts/Initializable.sol"; | ||
|
||
contract MyContract is Initializable { | ||
|
||
function initialize(address arg1, uint256 arg2, bytes arg3) initializer public payable { | ||
// "constructor" code... | ||
} | ||
|
||
} | ||
|
||
``` | ||
|
||
Notice how the contract extends `Initializable` and implements the `initializer` provided by it. | ||
|
||
## Summary | ||
|
||
Any developer using zOS should be familiar with proxies in the ways that are described in this article. In the end, the concept is very simple, and zOS is designed to encapsulate all the proxy mechanics in a way that the amount of things you need to keep in mind when developing upgradeable applications are reduced to an absolute minimum. It all comes down to a 3 item list: | ||
|
||
* Have a basic understanding of what a proxy is | ||
* Always extend storage instead of modifying it | ||
* Make sure your contracts use initializer functions instead of constructors | ||
|
||
Furthermore, ZeppelinOS will let you know when something goes wrong with one of the items in this list. So, seat back, enjoy, code, and let zOS take care of the rest. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to brand this:
The ZeppelinOS Upgrade Pattern
The problem here is that the ideas for this come from many different places, so maybe at the end we can add some generic thanks to everybody researching about upgradeability in ethereum.
@martriay what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep in mind that the title has to fit in the sidebar =D