Skip to main content

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);
}
}