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 25 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 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
3f4b90e
Merge branch 'master' of https://github.com/FuelLabs/fuels-ts into db…
danielbate Nov 19, 2024
6abfd3b
chore: fix toml
danielbate Nov 19, 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
102 changes: 102 additions & 0 deletions apps/docs-snippets2/src/contracts/proxy-contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// #region proxy-2
import { Provider, Wallet } from 'fuels';

import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../env';
import {
Counter,
CounterFactory,
CounterV2,
CounterV2Factory,
Proxy,
ProxyFactory,
} from '../typegend';

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

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

// #region proxy-3
/**
* It is important to pass all storage slots to the proxy in order to
* initialize the storage slots.
*/
const storageSlots = Counter.storageSlots.concat(Proxy.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 ProxyFactory(wallet);
const proxyDeploy = await proxyContractFactory.deploy({
storageSlots,
configurableConstants,
});

const { contract: proxyContract } = await proxyDeploy.waitForResult();
const { waitForResult } = await proxyContract.functions
.initialize_proxy()
.call();

await waitForResult();
// #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 proxiedContract = new Counter(proxyContract.id, wallet);

const incrementCall = await proxiedContract.functions.increment_count(1).call();
await incrementCall.waitForResult();

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

console.log('count:', count.toNumber() === 1);

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

const updateTargetCall = await proxyContract.functions
.set_proxy_target({ bits: contractV2.id.toB256() })
.call();

await updateTargetCall.waitForResult();
// #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 incrementCall2 = await upgradedContract.functions
.increment_count(1)
.call();

await incrementCall2.waitForResult();

const { value: increments } = await upgradedContract.functions
.get_increments()
.get();

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

console.log('secondCount', count2.toNumber() === 2);
console.log('increments', increments);
2 changes: 1 addition & 1 deletion apps/docs-snippets2/sway/Forc.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[workspace]
members = ["counter", "script-sum"]
members = ["counter", "counter-v2", "proxy", "script-sum"]
7 changes: 7 additions & 0 deletions apps/docs-snippets2/sway/counter-v2/Forc.toml
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]
52 changes: 52 additions & 0 deletions apps/docs-snippets2/sway/counter-v2/src/main.sw
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_count(amount: u64) -> u64;

#[storage(write, read)]
fn decrement_count(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_count(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_count(amount: u64) -> u64 {
let current = storage.counter.try_read().unwrap_or(0);
storage.counter.write(current - amount);
storage.counter.read()
}
}
// #endregion proxy-5
16 changes: 14 additions & 2 deletions apps/docs-snippets2/sway/counter/src/main.sw
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// #region proxy-1
contract;

abi Counter {
Expand All @@ -6,6 +7,9 @@ abi Counter {

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

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

storage {
Expand All @@ -15,13 +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_count(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_count(amount: u64) -> u64 {
let current = storage.counter.try_read().unwrap_or(0);
storage.counter.write(current - amount);
storage.counter.read()
}
}
// #endregion proxy-1
9 changes: 9 additions & 0 deletions apps/docs-snippets2/sway/proxy/Forc.toml
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"

[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" }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We must be sure this is the correct version of this contract.

Can we please have a README explaining all these details?

This must always be paired with the one used by the fuels CLI, and forc:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll notice we use a pre-built version in the' fuels' CLI, which is the same approach followed by forc:

Perhaps we could even reuse the same built version inside the fuels/deploy command. We'd only need the source to use the contract's contents in code snippets, which doesn't seem to be the case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arboleya Should we export the generated types from the Src14Proxy contract in the umbrella package?

This way, we can advise users who prefer manual deployment ( not using the fuels CLI ) to use our exported version, ensuring it stays in sync with the audited version.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Torres-ssf @arboleya I get a cyclic dep if I export this directly from fuels, do we want, dare I say, another package?

Copy link
Contributor Author

@danielbate danielbate Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could export it from fuel-ts/abi-typegen as example standards?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exporting this has been a bit painful.

If the generated factory exists inside fuels and we want to export it from there, we get cyclic deps for all the things that the factory itself is importing. So we'd need to swap out the imports for the module imports.

It probably needs to exist in it's own package, that imports all the required module imports that the factory needs. With a script that swaps the fuels imports for the module imports so we can still export it from fuels.

Will feedback soon.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danielbate Yes. I've struggled to find a solution for this as well.

11 changes: 11 additions & 0 deletions apps/docs-snippets2/sway/proxy/src/interface.sw
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);
}
160 changes: 160 additions & 0 deletions apps/docs-snippets2/sway/proxy/src/main.sw
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
Loading