-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #313 from oasisprotocol/xz/add-harrys-proxy-recomm…
…endations Add deployment pattern recommendations
- Loading branch information
Showing
1 changed file
with
240 additions
and
0 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
--- | ||
description: Deploying upgradable and deterministic contracts | ||
--- | ||
|
||
# Deployment Patterns | ||
|
||
## Implementing Proxy contracts on Oasis Sapphire | ||
|
||
As a confidential Ethereum Virtual Machine (EVM), Oasis prevents external | ||
access to contract storage or runtime states in order to keep your secrets | ||
private. This unique feature affects how developers interact with and manage | ||
smart contracts, particularly when using common Ethereum development tools. | ||
|
||
### What are Upgradable Contracts? | ||
|
||
Upgradable contracts are smart contracts designed to allow developers to update | ||
functionality even after being deployed to a blockchain. This is particularly | ||
useful for fixing bugs or adding new features without losing the existing state | ||
or having to deploy a new contract. Upgradability is achieved through proxy | ||
patterns, where a proxy contract directs calls to an underlying logic contract | ||
which developers can swap out without affecting the state stored in the proxy. | ||
|
||
#### [EIP-1822]: Universal Upgradeable Proxy Standard (UUPS) | ||
|
||
EIP-1822 introduces a method for creating upgradable contracts using a proxy | ||
pattern and specifies a mechanism where the proxy contract itself contains the | ||
upgrade logic. This design reduces the complexity and potential for errors | ||
compared to other proxy patterns because it consolidates upgrade functionality | ||
within the proxy and eliminates the need for additional external management. | ||
|
||
#### [EIP-1967]: Standard Proxy Storage Slots | ||
|
||
EIP-1967 defines standard storage slots to be used by all proxy contracts for | ||
consistent and predictable storage access. This standard helps prevent storage | ||
collisions and enhances security by outlining specific locations in a proxy | ||
contract for storing the address of the logic contract and other administrative | ||
information. Using these predetermined slots makes managing and auditing proxy | ||
contracts easier. | ||
|
||
[EIP-1822]: https://eips.ethereum.org/EIPS/eip-1822 | ||
[EIP-1967]: https://eips.ethereum.org/EIPS/eip-1967 | ||
|
||
### The Impact of Confidential EVM on Tooling Compatibility | ||
|
||
While the underlying proxy implementations in EIP-1822 work perfectly in | ||
facilitating smart contract upgrades, the tools typically used to manage these | ||
proxies may not function as expected on Oasis Sapphire. | ||
For example, the [openzeppelin-upgrades] library, which relies on the EIP-1967 | ||
standard, uses [eth_getStorageAt] to access contract storage. This function | ||
does not work in a confidential environment which forbids direct storage | ||
access. | ||
|
||
Additionally, Sapphire natively protects against replay and currently does not | ||
allow an empty chain ID à la pre [EIP-155] transactions. | ||
|
||
[eth_getStorageAt]: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getstorageat | ||
[openzeppelin-upgrades]: https://github.com/OpenZeppelin/openzeppelin-upgrades | ||
[EIP-155]: https://eips.ethereum.org/EIPS/eip-155 | ||
|
||
### Solutions for Using UUPS Proxies on Oasis Sapphire | ||
|
||
Developers looking to use UUPS proxies on Oasis Sapphire have two primary | ||
options: | ||
|
||
#### 1. Directly Implement EIP-1822 | ||
|
||
Avoid using [openzeppelin-upgrades] and manually handle the proxy setup and | ||
upgrades with your own scripts, such as by calling the `updateCodeAddress` | ||
method directly. | ||
|
||
#### 2. Modify Deployment Scripts | ||
|
||
Change deployment scripts to avoid `eth_getStorageAt`. Alternative methods | ||
like calling `owner()` which do not require direct storage access. | ||
[hardhat-deploy] as of `0.12.4` supports this approach with a default proxy | ||
that includes an `owner()` function when deploying with a configuration that | ||
specifies `proxy: true`. | ||
|
||
```typescript | ||
module.exports = async ({getNamedAccounts, deployments, getChainId}) => { | ||
const {deploy} = deployments; | ||
const {deployer} = await getNamedAccounts(); | ||
await deploy('Greeter', { | ||
from: deployer, | ||
proxy: true, | ||
}); | ||
}; | ||
``` | ||
|
||
### Solution for Using Deterministic Proxies on Oasis Sapphire | ||
|
||
We suggest that developers interested in deterministic proxies on Oasis | ||
Sapphire use a contract that supports replay protection. | ||
|
||
`hardhat-deploy` supports using the [Safe Singleton factory][safe-singleton-factory] deployed on | ||
the Sapphire [Mainnet] and [Testnet] when `deterministicDeployment` is `true`. | ||
|
||
```typescript | ||
module.exports = async ({getNamedAccounts, deployments, getChainId}) => { | ||
const {deploy} = deployments; | ||
const {deployer} = await getNamedAccounts(); | ||
await deploy('Greeter', { | ||
from: deployer, | ||
deterministicDeployment: true, | ||
}); | ||
}; | ||
``` | ||
|
||
Next, in your `hardhat.config.ts` file, specify the address of the Safe | ||
Singleton factory: | ||
|
||
```typescript | ||
deterministicDeployment: { | ||
"97": { | ||
factory: '0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7', | ||
deployer: '0xE1CB04A0fA36DdD16a06ea828007E35e1a3cBC37', | ||
funding: '2000000', | ||
signedTx: '', | ||
}, | ||
"23295": { | ||
factory: '0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7', | ||
deployer: '0xE1CB04A0fA36DdD16a06ea828007E35e1a3cBC37', | ||
funding: '2000000', | ||
signedTx: '', | ||
} | ||
}, | ||
``` | ||
|
||
[hardhat-deploy]: https://github.com/wighawag/hardhat-deploy | ||
[Mainnet]: https://explorer.oasis.io/mainnet/sapphire/address/0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7 | ||
[Testnet]: https://explorer.oasis.io/testnet/sapphire/address/0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7 | ||
[safe-singleton-factory]: https://github.com/safe-global/safe-singleton-factory | ||
|
||
### Caution Against Using `eth_getStorageAt` | ||
|
||
Direct storage access, such as with `eth_getStorageAt`, is generally | ||
discouraged. It reduces contract flexibility and deviates from common practice | ||
which advocates for a standardized Solidity compatible API to both facilitate | ||
interactions between contracts and allow popular libraries such as [ABIType] | ||
and [TypeChain] to automatically generate client bindings. Direct storage | ||
access makes contracts less adaptable and complicates on-chain automation; it | ||
can even complicate the use of multisig wallets. | ||
For contracts aiming to maintain a standard interface and ensure future | ||
upgradeability, we advise sticking to ERC-defined Solidity compatible APIs and | ||
avoiding directly interacting with contract storage. | ||
|
||
[ABIType]: https://abitype.dev/ | ||
[TypeChain]: https://www.npmjs.com/package/typechain | ||
|
||
### [EIP-7201]: Namespaced Storage for Delegatecall Contracts | ||
|
||
ERC-7201 proposes a structured approach to storage in smart contracts that | ||
utilize `delegatecall` which is often employed in proxy contracts for | ||
upgradability. This standard recommends namespacing storage to mitigate the | ||
risk of storage collisions — a common issue when multiple contracts share the | ||
same storage space in a `delegatecall` context. | ||
|
||
[EIP-7201]: https://eips.ethereum.org/EIPS/eip-7201 | ||
|
||
### Benefits of Namespacing over Direct Storage Access | ||
|
||
Contracts using `delegatecall`, such as upgradable proxies, can benefit from | ||
namespacing their storage through more efficient data organization which | ||
enhances security. This approach isolates different variables and sections of | ||
a contract’s storage under distinct namespaces, ensuring that each segment is | ||
distinct and does not interfere with others. Namespacing is generally more | ||
robust and preferable to using `eth_getStorageAt`. | ||
|
||
See example ERC-7201 implementation and usage: | ||
https://gist.github.com/CedarMist/4cfb8f967714aa6862dd062742acbc7b | ||
|
||
```solidity | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity ^0.8.0; | ||
contract Example7201 { | ||
/// @custom:storage-location erc7201:Example7201.state | ||
struct State { | ||
uint256 counter; | ||
} | ||
function _stateStorageSlot() | ||
private pure | ||
returns (bytes32) | ||
{ | ||
return keccak256(abi.encode(uint256(keccak256("Example7201.state")) - 1)) & ~bytes32(uint256(0xff)); | ||
} | ||
function _getState() | ||
private pure | ||
returns (State storage state) | ||
{ | ||
bytes32 slot = _stateStorageSlot(); | ||
assembly { | ||
state.slot := slot | ||
} | ||
} | ||
function increment() | ||
public | ||
{ | ||
State storage state = _getState(); | ||
state.counter += 1; | ||
} | ||
function get() | ||
public view | ||
returns (uint256) | ||
{ | ||
State storage state = _getState(); | ||
return state.counter; | ||
} | ||
} | ||
contract ExampleCaller { | ||
Example7201 private example; | ||
constructor () { | ||
example = new Example7201(); | ||
} | ||
function get() | ||
external | ||
returns (uint256 counter) | ||
{ | ||
(bool success, bytes memory result ) = address(example).delegatecall(abi.encodeCall(example.get, ())); | ||
require(success); | ||
counter = abi.decode(result, (uint256)); | ||
} | ||
function increment() | ||
external | ||
{ | ||
(bool success, ) = address(example).delegatecall(abi.encodeCall(example.increment, ())); | ||
require(success); | ||
} | ||
} | ||
``` |