Skip to content

Commit

Permalink
Merge pull request #313 from oasisprotocol/xz/add-harrys-proxy-recomm…
Browse files Browse the repository at this point in the history
…endations

Add deployment pattern recommendations
  • Loading branch information
aefhm committed May 29, 2024
2 parents 4170cc6 + f7a4bf6 commit 8a9f6b6
Showing 1 changed file with 240 additions and 0 deletions.
240 changes: 240 additions & 0 deletions docs/deployment.md
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);
}
}
```

0 comments on commit 8a9f6b6

Please sign in to comment.