-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
danielbate
wants to merge
25
commits into
master
Choose a base branch
from
db/chore/manual-proxy-contracts
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+397
−6
Draft
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 191054d
Merge branch 'master' of https://github.com/FuelLabs/fuels-ts into db…
danielbate 0e795f7
chore: add missing test groups
danielbate 36c0e24
chore: changeset
danielbate 238fd5b
chore: update changeset
danielbate 5459232
chore: forc format
danielbate 8a79ee2
Merge branch 'db/chore/manual-proxy-contracts' of https://github.com/…
danielbate 360b704
chore: update doc
danielbate d7c0b0c
chore: update doc
danielbate 445c93c
chore: update doc
danielbate 914bc01
Merge branch 'master' into db/chore/manual-proxy-contracts
danielbate 67d9433
Merge branch 'master' into db/chore/manual-proxy-contracts
Torres-ssf d0ff3df
Merge branch 'master' of https://github.com/FuelLabs/fuels-ts into db…
danielbate 8f9c67c
docs: use src 14 commit hash for doc
danielbate 38a125f
chore: migrate to v2 snippet
danielbate a17bb8e
chore: restore v1 snippets files
danielbate dea7aea
chore: fix snippet path
danielbate 93b0472
chore: fix test region
danielbate 2290e74
multilning doc comments
Torres-ssf b966a26
moving snippet to another place
Torres-ssf e1c676a
Merge branch 'master' into db/chore/manual-proxy-contracts
Torres-ssf 4c5f549
Merge branch 'master' into db/chore/manual-proxy-contracts
Torres-ssf 3eccadc
Merge branch 'master' into db/chore/manual-proxy-contracts
Torres-ssf 3f4b90e
Merge branch 'master' of https://github.com/FuelLabs/fuels-ts into db…
danielbate 6abfd3b
chore: fix toml
danielbate File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
--- | ||
--- | ||
|
||
docs: proxy contract cookbook |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" } | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")) | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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, andforc
:There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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?There was a problem hiding this comment.
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 fromfuels
.Will feedback soon.
There was a problem hiding this comment.
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.