The following proposes a standard for simple upgradeable proxies.
We seek to standardize a proxy implementation to improve developer experience and enable tooling to automatically deploy or update proxies as needed.
This OpenZeppelin blog post is a good survey of the state of the art at this time.
Proxy designs fall into three essential categories:
This document falls in the second category. We want to standardize the implementation of simple upgradeable pass-through contracts.
The FuelVM provides an LDC
instruction that is used by Sway's std::execution::run_external
to provide a similar behavior to EVM's delegatecall
and execute instructions from another contract while retaining one's own storage context. This is the intended means of implementation of this standard.
The proxy contract MUST maintain the address of its target in its storage at slot 0x7bb458adc1d118713319a5baa00a2d049dd64d2916477d2688d76970c898cd55
(equivalent to sha256("storage_SRC14_0")
).
It SHOULD base other proxy specific storage fields at sha256("storage_SRC14")
to avoid collisions with target storage.
It MAY have its storage definition overlap with that of its target if necessary.
The proxy contract MUST delegate any method call not part of its interface to the target contract.
This delegation MUST retain the storage context of the proxy contract.
The following functions MUST be implemented by a proxy contract to follow the SRC-14 standard:
fn set_proxy_target(new_target: ContractId);
If a valid call is made to this function it MUST change the target address of the proxy to new_target
.
This method SHOULD implement access controls such that the target can only be changed by a user that possesses the right permissions (typically the proxy owner).
This standard is meant to provide simple upgradeability, it is deliberately minimalistic and does not provide the level of functionality of diamonds.
Unlike in UUPS , this standard requires that the upgrade function is part of the proxy and not its target. This prevents irrecoverable updates if a proxy is made to point to another proxy and no longer has access to upgrade logic.
SRC-14 is intended to be compatible with SRC-5 and other standards of contract functionality.
As it is the first attempt to standardize proxy implementation, we do not consider interoperability with other proxy standards.
Permissioning proxy target changes is the primary consideration here. This standard is not opinionated about means of achieving this, use of SRC-5 is recommended.
abi SRC14 {
#[storage(write)]
fn set_proxy_target(new_target: ContractId);
}
Example of a minimal SRC-14 implementation with no access control.
contract;
use std::execution::run_external;
use std::constants::ZERO_B256;
use standards::src14::SRC14;
// use sha256("storage_SRC14") as base to avoid collisions
#[namespace(SRC14)]
storage {
// target is at sha256("storage_SRC14_0")
target: ContractId = ContractId::from(ZERO_B256),
}
impl SRC14 for Contract {
#[storage(write)]
fn set_proxy_target(new_target: ContractId) {
storage.target.write(new_target);
}
}
#[fallback]
#[storage(read)]
fn fallback() {
// pass through any other method call to the target
run_external(storage.target.read())
}
Example of a SRC-14 implementation that also implements SRC-5 .
contract;
use std::execution::run_external;
use std::constants::ZERO_B256;
use standards::src5::{AccessError, SRC5, State};
use standards::src14::SRC14;
/// The owner of this contract at deployment.
const INITIAL_OWNER: Identity = Identity::Address(Address::from(ZERO_B256));
// use sha256("storage_SRC14") as base to avoid collisions
#[namespace(SRC14)]
storage {
// target is at sha256("storage_SRC14_0")
target: ContractId = ContractId::from(ZERO_B256),
owner: State = State::Initialized(INITIAL_OWNER),
}
impl SRC5 for Contract {
#[storage(read)]
fn owner() -> State {
storage.owner.read()
}
}
impl SRC14 for Contract {
#[storage(write)]
fn set_proxy_target(new_target: ContractId) {
only_owner();
storage.target.write(new_target);
}
}
#[fallback]
#[storage(read)]
fn fallback() {
// pass through any other method call to the target
run_external(storage.target.read())
}
#[storage(read)]
fn only_owner() {
require(
storage
.owner
.read() == State::Initialized(msg_sender().unwrap()),
AccessError::NotOwner,
);
}