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.
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.
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
.
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 deployed on
the Sapphire Mainnet and Testnet when deterministicDeployment
is true
.
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:
deterministicDeployment: {
"97": {
factory: '0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7',
deployer: '0xE1CB04A0fA36DdD16a06ea828007E35e1a3cBC37',
funding: '2000000',
signedTx: '',
},
"23295": {
factory: '0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7',
deployer: '0xE1CB04A0fA36DdD16a06ea828007E35e1a3cBC37',
funding: '2000000',
signedTx: '',
}
},
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.
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.
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
// 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);
}
}