Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SRC-14 Simple Proxy Standard #94

Merged
merged 8 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions SRCs/src-14.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Simple Upgradable Proxies
bitzoic marked this conversation as resolved.
Show resolved Hide resolved

## Abstract

The following proposes a standard for simple upgradable proxies.

## Motivation

We seek to standardize proxy implementation to improve developer experience and enable tooling to automatically deploy or update proxies as needed.

## Prior Art

[This OpenZeppelin blog post](https://blog.openzeppelin.com/the-state-of-smart-contract-upgrades#proxies-and-implementations) is a good survey of the state of the art at this time.

Proxy designs fall into three essential categories:
1. Immutable proxies which are lightweight clones of other contracts but can't change targets
2. Upgradable proxies such as [UUPS](https://eips.ethereum.org/EIPS/eip-1822) which store a target in storage and delegate all calls to it
3. [Diamonds](https://eips.ethereum.org/EIPS/eip-2535) which are both upgradable and can point to multiple targets on a per method basis

This document falls in the second category. We want to standardize the implementation of simple upgradable passthrough 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.

## Specification

### Required Behavior

The proxy contract MUST maintain the address of its target in its storage at slot `0x7bb458adc1d118713319a5baa00a2d049dd64d2916477d2688d76970c898cd55` (equivalent to `sha256("storage_SRC14_0")`).
IGI-111 marked this conversation as resolved.
Show resolved Hide resolved
kayagokalp marked this conversation as resolved.
Show resolved Hide resolved
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.

### Required Public Functions

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).
SwayStar123 marked this conversation as resolved.
Show resolved Hide resolved

## Rationale

This standard is meant to provide simple upgradability, 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.
IGI-111 marked this conversation as resolved.
Show resolved Hide resolved
This prevents irrecoverable updates if a proxy is made to point to another proxy and no longer has access to upgrade logic.

## Backwards Compatibility

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.

## Security Considerations

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.
IGI-111 marked this conversation as resolved.
Show resolved Hide resolved

## Example ABI

```sway
abi SRC14 {
#[storage(write)]
fn set_proxy_target(new_target: ContractId);
}
```

## Example Implementation

### [Minimal Proxy](../examples/examples/src14-simple-proxy/owned/src/minimal.sw)

Example of a minimal SRC-14 implementation with no access control.

### [Owned Proxy](../examples/examples/src14-simple-proxy/owned/src/owned.sw)

Example of a SRC-14 implementation that also implements SRC-5.
IGI-111 marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions examples/Forc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ members = [
"src11-security-information/variable-information",
"src12-contract-factory/with_configurables",
"src12-contract-factory/without_configurables",
"src14-simple-proxy/owned",
"src14-simple-proxy/minimal",
"src20-native-asset/single_asset",
"src20-native-asset/multi_asset",
]
8 changes: 8 additions & 0 deletions examples/src14-simple-proxy/minimal/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[project]
authors = ["Fuel Labs <contact@fuel.sh>"]
entry = "minimal.sw"
license = "Apache-2.0"
name = "minimal_src14"

[dependencies]
standards = { path = "../../../standards" }
26 changes: 26 additions & 0 deletions examples/src14-simple-proxy/minimal/src/minimal.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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())
}
8 changes: 8 additions & 0 deletions examples/src14-simple-proxy/owned/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[project]
authors = ["Fuel Labs <contact@fuel.sh>"]
entry = "owned.sw"
license = "Apache-2.0"
name = "owned_src14"

[dependencies]
standards = { path = "../../../standards" }
49 changes: 49 additions & 0 deletions examples/src14-simple-proxy/owned/src/owned.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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())
IGI-111 marked this conversation as resolved.
Show resolved Hide resolved
}

#[storage(read)]
fn only_owner() {
require(
storage
.owner
.read() == State::Initialized(msg_sender().unwrap()),
AccessError::NotOwner,
);
}
28 changes: 28 additions & 0 deletions standards/src/src14.sw
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
library;

abi SRC14 {
/// Change the target address of a proxy contract.
///
IGI-111 marked this conversation as resolved.
Show resolved Hide resolved
/// # Arguments
///
/// * `new_target`: [ContractId] - The new proxy contract to which all fallback calls will be passed.
///
/// # Examples
///
/// ```sway
/// use src14::SRC14;
///
/// fn foo(contract_id: ContractId) {
/// let contract_abi = abi(SRC14, contract_id.bits());
/// let new_target = ContractId::from(0x4a778acfad1abc155a009dc976d2cf0db6197d3d360194d74b1fb92b96986b00);
/// contract_abi.set_proxy_target(new_target);
/// }
/// ```
#[storage(write)]
fn set_proxy_target(new_target: ContractId);
}

/// The standard storage slot to store proxy target address.
///
/// Value is `sha256("storage_SRC14_0")`.
pub const SRC14_TARGET_STORAGE: b256 = 0x7bb458adc1d118713319a5baa00a2d049dd64d2916477d2688d76970c898cd55;
1 change: 1 addition & 0 deletions standards/src/standards.sw
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ pub mod src7;
pub mod src10;
pub mod src11;
pub mod src12;
pub mod src14;
pub mod src20;
Loading