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

docs: proxy contract cookbook #3253

Draft
wants to merge 23 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
73b1bb1
docs: cookbook for manually deploying and upgrading a proxy
danielbate Oct 5, 2024
191054d
Merge branch 'master' of https://github.com/FuelLabs/fuels-ts into db…
danielbate Oct 9, 2024
0e795f7
chore: add missing test groups
danielbate Oct 9, 2024
36c0e24
chore: changeset
danielbate Oct 9, 2024
238fd5b
chore: update changeset
danielbate Oct 9, 2024
5459232
chore: forc format
danielbate Oct 9, 2024
8a79ee2
Merge branch 'db/chore/manual-proxy-contracts' of https://github.com/…
danielbate Oct 9, 2024
360b704
chore: update doc
danielbate Oct 9, 2024
d7c0b0c
chore: update doc
danielbate Oct 9, 2024
445c93c
chore: update doc
danielbate Oct 9, 2024
914bc01
Merge branch 'master' into db/chore/manual-proxy-contracts
danielbate Oct 9, 2024
67d9433
Merge branch 'master' into db/chore/manual-proxy-contracts
Torres-ssf Oct 11, 2024
d0ff3df
Merge branch 'master' of https://github.com/FuelLabs/fuels-ts into db…
danielbate Oct 15, 2024
8f9c67c
docs: use src 14 commit hash for doc
danielbate Oct 15, 2024
38a125f
chore: migrate to v2 snippet
danielbate Oct 17, 2024
a17bb8e
chore: restore v1 snippets files
danielbate Oct 17, 2024
dea7aea
chore: fix snippet path
danielbate Oct 17, 2024
93b0472
chore: fix test region
danielbate Oct 17, 2024
2290e74
multilning doc comments
Torres-ssf Oct 17, 2024
b966a26
moving snippet to another place
Torres-ssf Oct 17, 2024
e1c676a
Merge branch 'master' into db/chore/manual-proxy-contracts
Torres-ssf Oct 21, 2024
4c5f549
Merge branch 'master' into db/chore/manual-proxy-contracts
Torres-ssf Oct 22, 2024
3eccadc
Merge branch 'master' into db/chore/manual-proxy-contracts
Torres-ssf Oct 29, 2024
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
4 changes: 4 additions & 0 deletions .changeset/friendly-cooks-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

docs: proxy contract cookbook
105 changes: 105 additions & 0 deletions apps/docs-snippets/src/guide/contracts/proxy-contracts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Provider, Wallet } from 'fuels';
import { launchTestNode } from 'fuels/test-utils';

import {
Counter,
CounterFactory,
CounterV2,
CounterV2Factory,
ProxyContract,
ProxyContractFactory,
} from '../../../test/typegen';

/**
* @group node
* @group browser
*/
describe('Proxy Contracts', () => {
it('deploys and upgrades a contracts using a proxy', async () => {
using launched = await launchTestNode();

const {
provider: testProvider,
wallets: [testWallet],
} = launched;
const providerUrl = testProvider.url;
const WALLET_PVT_KEY = testWallet.privateKey;

// #region proxy-2
// #import { Provider, Wallet };
// #context import { WALLET_PVT_KEY } from 'path/to/my/env/file';
// #context import { CounterFactory, Counter, ProxyFactory, CounterV2Factory } from 'path/to/typegen/outputs';

const provider = await Provider.create(providerUrl);
const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);

const counterContractFactory = new CounterFactory(wallet);
const { waitForResult: waitForCounterContract } = await counterContractFactory.deploy();
const { contract: counterContract } = await waitForCounterContract();
// #endregion proxy-2

// #region proxy-3
// It is important to pass the pass all storage slots to the proxy in order to initialize the storage slots.
danielbate marked this conversation as resolved.
Show resolved Hide resolved
const storageSlots = Counter.storageSlots.concat(ProxyContract.storageSlots);

// These configurables are specific to our recommended SRC14 compliant contract. They must be passed on deploy
// and then `initialize_proxy` must be called to setup the proxy contract.
const configurableConstants = {
INITIAL_TARGET: { bits: counterContract.id.toB256() },
INITIAL_OWNER: { Initialized: { Address: { bits: wallet.address.toB256() } } },
};

const proxyContractFactory = new ProxyContractFactory(wallet);
const { waitForResult: waitForProxyContract } = await proxyContractFactory.deploy({
storageSlots,
configurableConstants,
});
const { contract: proxyContract } = await waitForProxyContract();

const { waitForResult: waitForProxyInit } = await proxyContract.functions
.initialize_proxy()
.call();
await waitForProxyInit();
// #endregion proxy-3

// #region proxy-4
// Make sure to use only the contract ID of the proxy when instantiating the contract
// as this will remain static even with future upgrades.
const initialContract = new Counter(proxyContract.id, wallet);

const { waitForResult: waitForIncrement } = await initialContract.functions
.increment_counter(1)
.call();
await waitForIncrement();

const { value: count } = await initialContract.functions.get_count().get();
// #endregion proxy-4

// #region proxy-6
const { waitForResult: waitForCounterContractV2 } = await CounterV2Factory.deploy(wallet);
const { contract: counterContractV2 } = await waitForCounterContractV2();

const { waitForResult: waitForUpdateTarget } = await proxyContract.functions
.set_proxy_target({ bits: counterContractV2.id.toB256() })
.call();

await waitForUpdateTarget();
// #endregion proxy-6

// #region proxy-7
// Again, we are instantiating the contract with the same proxy ID but using a new contract instance.
const upgradedContract = new CounterV2(proxyContract.id, wallet);
const { waitForResult: waitForSecondIncrement } = await upgradedContract.functions
.increment_counter(1)
.call();
await waitForSecondIncrement();

const { value: increments } = await upgradedContract.functions.get_increments().get();
const { value: secondCount } = await upgradedContract.functions.get_count().get();
// #endregion proxy-7

expect(count.toNumber()).toBe(1);
expect(secondCount.toNumber()).toBe(2);
expect(increments.toNumber()).toBe(1);
});
});
2 changes: 2 additions & 0 deletions apps/docs-snippets/test/fixtures/forc-projects/Forc.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"counter",
"counter-v2",
"echo-enum",
"liquidity-pool",
"log-values",
Expand Down Expand Up @@ -34,4 +35,5 @@ members = [
"input-output-types",
"bytecode-input",
"configurable-pin",
"proxy-contract",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "main.sw"
license = "Apache-2.0"
name = "counter-v2"

[dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// #region proxy-5
contract;

abi Counter {
#[storage(read)]
fn get_count() -> u64;

#[storage(read)]
fn get_increments() -> u64;

#[storage(write, read)]
fn increment_counter(amount: u64) -> u64;

#[storage(write, read)]
fn decrement_counter(amount: u64) -> u64;
}

storage {
counter: u64 = 0,
increments: u64 = 0,
}

impl Counter for Contract {
#[storage(read)]
fn get_count() -> u64 {
storage.counter.try_read().unwrap_or(0)
}

#[storage(read)]
fn get_increments() -> u64 {
storage.increments.try_read().unwrap_or(0)
}

#[storage(write, read)]
fn increment_counter(amount: u64) -> u64 {
let current = storage.counter.try_read().unwrap_or(0);
storage.counter.write(current + amount);

let current_iteration: u64 = storage.increments.try_read().unwrap_or(0);
storage.increments.write(current_iteration + 1);

storage.counter.read()
}

#[storage(write, read)]
fn decrement_counter(amount: u64) -> u64 {
let current = storage.counter.read();
storage.counter.write(current - amount);
storage.counter.read()
}
}
// #endregion proxy-5
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// #region proxy-1
contract;

abi Counter {
Expand All @@ -18,20 +19,21 @@ storage {
impl Counter for Contract {
#[storage(read)]
fn get_count() -> u64 {
storage.counter.read()
storage.counter.try_read().unwrap_or(0)
}

#[storage(write, read)]
fn increment_counter(amount: u64) -> u64 {
let current = storage.counter.read();
let current = storage.counter.try_read().unwrap_or(0);
storage.counter.write(current + amount);
storage.counter.read()
}

#[storage(write, read)]
fn decrement_counter(amount: u64) -> u64 {
let current = storage.counter.read();
let current = storage.counter.try_read().unwrap_or(0);
storage.counter.write(current - amount);
storage.counter.read()
}
}
// #endregion proxy-1
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
entry = "main.sw"
license = "Apache-2.0"
name = "proxy-contract"

[dependencies]
standards = { git = "https://github.com/FuelLabs/sway-standards", tag = "v0.6.0" }
sway_libs = { git = "https://github.com/FuelLabs/sway-libs", tag = "v0.24.0" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
library;

use standards::src5::State;

abi OwnedProxy {
#[storage(write)]
fn initialize_proxy();

#[storage(write)]
fn set_proxy_owner(new_proxy_owner: State);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
contract;

mod interface;

use interface::OwnedProxy;
use ::sway_libs::{
ownership::errors::InitializationError,
upgradability::{
_proxy_owner,
_proxy_target,
_set_proxy_owner,
_set_proxy_target,
only_proxy_owner,
},
};
use standards::{src14::{SRC14, SRC14Extension}, src5::State};
use std::execution::run_external;

configurable {
/// The initial value of `storage::SRC14.target`.
INITIAL_TARGET: Option<ContractId> = None,
/// The initial value of `storage::SRC14.proxy_owner`.
INITIAL_OWNER: State = State::Uninitialized,
}

storage {
SRC14 {
/// The [ContractId] of the target contract.
///
/// # Additional Information
///
/// `target` is stored at sha256("storage_SRC14_0")
target in 0x7bb458adc1d118713319a5baa00a2d049dd64d2916477d2688d76970c898cd55: Option<ContractId> = None,
/// The [State] of the proxy owner.
///
/// # Additional Information
///
/// `proxy_owner` is stored at sha256("storage_SRC14_1")
proxy_owner in 0xbb79927b15d9259ea316f2ecb2297d6cc8851888a98278c0a2e03e1a091ea754: State = State::Uninitialized,
},
}

impl SRC14 for Contract {
/// Change the target contract of the proxy contract.
///
/// # Additional Information
///
/// This method can only be called by the `proxy_owner`.
///
/// # Arguments
///
/// * `new_target`: [ContractId] - The new proxy contract to which all fallback calls will be passed.
///
/// # Reverts
///
/// * When not called by `proxy_owner`.
///
/// # Number of Storage Accesses
///
/// * Reads: `1`
/// * Write: `1`
#[storage(read, write)]
fn set_proxy_target(new_target: ContractId) {
only_proxy_owner();
_set_proxy_target(new_target);
}

/// Returns the target contract of the proxy contract.
///
/// # Returns
///
/// * [Option<ContractId>] - The new proxy contract to which all fallback calls will be passed or `None`.
///
/// # Number of Storage Accesses
///
/// * Reads: `1`
#[storage(read)]
fn proxy_target() -> Option<ContractId> {
_proxy_target()
}
}

impl SRC14Extension for Contract {
/// Returns the owner of the proxy contract.
///
/// # Returns
///
/// * [State] - Represents the state of ownership for this contract.
///
/// # Number of Storage Accesses
///
/// * Reads: `1`
#[storage(read)]
fn proxy_owner() -> State {
_proxy_owner()
}
}

impl OwnedProxy for Contract {
/// Initializes the proxy contract.
///
/// # Additional Information
///
/// This method sets the storage values using the values of the configurable constants `INITIAL_TARGET` and `INITIAL_OWNER`.
/// This then allows methods that write to storage to be called.
/// This method can only be called once.
///
/// # Reverts
///
/// * When `storage::SRC14.proxy_owner` is not [State::Uninitialized].
///
/// # Number of Storage Accesses
///
/// * Writes: `2`
#[storage(write)]
fn initialize_proxy() {
require(
_proxy_owner() == State::Uninitialized,
InitializationError::CannotReinitialized,
);

storage::SRC14.target.write(INITIAL_TARGET);
storage::SRC14.proxy_owner.write(INITIAL_OWNER);
}

/// Changes proxy ownership to the passed State.
///
/// # Additional Information
///
/// This method can be used to transfer ownership between Identities or to revoke ownership.
///
/// # Arguments
///
/// * `new_proxy_owner`: [State] - The new state of the proxy ownership.
///
/// # Reverts
///
/// * When the sender is not the current proxy owner.
/// * When the new state of the proxy ownership is [State::Uninitialized].
///
/// # Number of Storage Accesses
///
/// * Reads: `1`
/// * Writes: `1`
#[storage(write)]
fn set_proxy_owner(new_proxy_owner: State) {
_set_proxy_owner(new_proxy_owner);
}
}

/// Loads and runs the target contract's code within the proxy contract's context.
///
/// # Additional Information
///
/// Used when a method that does not exist in the proxy contract is called.
#[fallback]
#[storage(read)]
fn fallback() {
run_external(_proxy_target().expect("FallbackError::TargetNotSet"))
}
Loading