diff --git a/Cargo.lock b/Cargo.lock index d2d148f2849..6534b524472 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2036,6 +2036,7 @@ dependencies = [ "fuel-crypto", "fuel-tx", "fuel-vm", + "fuels", "fuels-accounts", "fuels-core", "futures", diff --git a/docs/book/src/forc/plugins/forc_client/index.md b/docs/book/src/forc/plugins/forc_client/index.md index 9c044221f47..fc552e6bece 100644 --- a/docs/book/src/forc/plugins/forc_client/index.md +++ b/docs/book/src/forc/plugins/forc_client/index.md @@ -1,26 +1,62 @@ # `forc-client` -Forc plugin for interacting with a Fuel node. +Forc plugin for interacting with a Fuel node. Since transactions are going to require some gas, you need to sign them with an account that has enough tokens to pay for them. -## Initializing the wallet and adding accounts +We offer multiple ways to sign the transaction: -If you don't have an initialized wallet or any account for your wallet you won't be able to sign transactions. + 1. Sign the transaction via your local wallet using `forc-client` which integrates with our CLI wallet, `forc-wallet`. + 2. Use the default signer to deploy to a local node + 3. Use `forc-wallet` to manually sign transactions, and copy the signed transaction back to `forc-client`. -To create a wallet you can use `forc wallet new`. It will ask you to choose a password to encrypt your wallet. After the initialization is done you will have your mnemonic phrase. +The easiest and recommended way to interact with deployed networks such as our testnets is option 1, using `forc-client` to sign your transactions which reads your default `forc-wallet` vault. For interacting with local node, we recommend using the second option, which leads `forc-client` to sign transactions with the a private key that comes pre-funded in local environments. -After you have created a wallet, you can derive a new account by running `forc wallet account new`. It will ask your password to decrypt the wallet before deriving an account. +## Option 1: Sign transactions via forc-client using your local forc-wallet vault -## Signing transactions using `forc-wallet` CLI +If you've used `forc-wallet` before, you'll already have a secure, password-protected vault holding your private key written to your file-system. `forc-client` is compatible with `forc-wallet` such that it can read that vault by asking you your password and use your account to sign transactions. -To submit the transactions created by `forc deploy` or `forc run`, you need to sign them first (unless you are using a client without UTXO validation). To sign a transaction you can use `forc-wallet` CLI. This section is going to walk you through the whole signing process. +Example: -By default `fuel-core` runs without UTXO validation, this allows you to send invalid inputs to emulate different conditions. +```console +> forc deploy -If you want to run `fuel-core` with UTXO validation, you can pass `--utxo-validation` to `fuel-core run`. + Building /Users/yourname/test-projects/test-contract + Finished release [optimized + fuel] target(s) in 11.39s + Confirming transactions [deploy impl-contract] + Network: https://testnet.fuel.network + Wallet: /Users/yourname/.fuel/wallets/.wallet +✔ Wallet password · ******** +? Wallet account › +❯ [0] fuel12pls73y9hnqdqthvduy2x44x48zt8s50pkerf32kq26f2afeqdwq6rj9ar - 0.002197245 ETH + [1] fuel1vzrm6kw9s3tv85gl25lpptsxrdguyzfhq6c8rk07tr6ft5g45nwqqh0uty - 0.001963631 ETH +? Do you agree to sign 1 transaction? (y/n) › yes + Finished deploying impl-contract https://app.fuel.network/contract/0x94b712901f04332682d14c998a5fc5a078ed15321438f46d58d0383200cde43d + Deployed in block https://app.fuel.network/block/5958351 +``` + +As it can be seen from the example, `forc-client` asks for your password to decrypt the `forc-wallet` vault, and list your accounts so that you can select the one you want to fund the transaction with. + +## Option 2: Using default signer -To install `forc-wallet` please refer to `forc-wallet`'s [GitHub repo](https://github.com/FuelLabs/forc-wallet#forc-wallet). +If you are not interacting with a deployed network, such as testnets, your local `fuel-core` environment can be structured such that it funds an account by default. Using `--default-signer` flag with `forc-client` binaries (run, deploy) will instruct `forc-client` to sign transactions with this pre-funded account. Which makes it a useful command while working against a local node. -1. Construct the transaction by using either `forc deploy` or `forc run`. To do so simply run `forc deploy` or `forc run` with your desired parameters. For a list of parameters please refer to the [forc-deploy](./forc_deploy.md) or [forc-run](./forc_run.md) section of the book. Once you run either command you will be asked the address of the wallet you are going to be signing with. After the address is given the transaction will be generated and you will be given a transaction ID. At this point CLI will actively wait for you to insert the signature. +Example: + +```console +> forc deploy --default-signer + + Building /Users/test/test-projects/test-contract + Finished release [optimized + fuel] target(s) in 11.40s + Confirming transactions [deploy impl-contract] + Network: http://127.0.0.1:4000 + Finished deploying impl-contract 0xf9fb08ef18ce226954270d6d4f67677d484b8782a5892b3d436572b405407544 + Deployed in block 00000001 +``` + +## Option 3: Manually signing through forc-wallet (Deprecated) + +This option is for creating the transaction first, signing it manually and supplying the signed transaction back to forc-client. Since it requires multiple steps, it is more error-prone and not recommended for general use case. Also this will be deprecated soon. + +1. Construct the transaction by using either `forc deploy` or `forc run`. To do so simply run `forc deploy --manual-sign` or `forc run --manual-sign` with your desired parameters. For a list of parameters please refer to the [forc-deploy](./forc_deploy.md) or [forc-run](./forc_run.md) section of the book. Once you run either command you will be asked the address of the wallet you are going to be signing with. After the address is given the transaction will be generated and you will be given a transaction ID. At this point CLI will actively wait for you to insert the signature. 2. Take the transaction ID generated in the first step and sign it with `forc wallet sign --account tx-id `. This will generate a signature. 3. Take the signature generated in the second step and provide it to `forc-deploy` (or `forc-run`). Once the signature is provided, the signed transaction will be submitted. @@ -56,7 +92,7 @@ By default `--default-signer` flag would sign your transactions with the followi ## Interacting with the testnet -To interact with the latest testnet, use the `--testnet` flag. When this flag is passed, transactions created by `forc-deploy` will be sent to the `beta-4` testnet. +To interact with the latest testnet, use the `--testnet` flag. When this flag is passed, transactions created by `forc-deploy` will be sent to the latest `testnet`. ```sh forc-deploy --testnet @@ -68,10 +104,10 @@ It is also possible to pass the exact node URL while using `forc-deploy` or `for forc-deploy --node-url https://beta-3.fuel.network ``` -Another alternative is the `--target` option, which provides useful aliases to all targets. For example if you want to deploy to `beta-3` you can use: +Another alternative is the `--target` option, which provides useful aliases to all targets. For example if you want to deploy to `beta-5` you can use: ```sh -forc-deploy --target beta-3 +forc-deploy --target beta-5 ``` Since deploying and running projects on the testnet cost gas, you will need coins to pay for them. You can get some using the [testnet faucet](https://faucet-testnet.fuel.network/). @@ -91,3 +127,40 @@ forc-deploy saves the details of each deployment in the `out/deployments` folder "deployed_block_id": "0x915c6f372252be6bc54bd70df6362dae9bf750ba652bf5582d9b31c7023ca6cf" } ``` + +## Proxy Contracts + +`forc-deploy` supports deploying proxy contracts automatically if it is enabled in the `Forc.toml` of the contract. + +```TOML +[project] +name = "test_contract" +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +implicit-std = false + +[proxy] +enabled = true +``` + +If there is no `address` field present under the proxy table, like the example above, `forc` will automatically create a proxy contract based on the [SRC-14](https://github.com/FuelLabs/sway-standards/blob/master/docs/src/src-14-simple-upgradeable-proxies.md) implementation from [sway-standards](https://github.com/FuelLabs/sway-standards). After generating and deploying the proxy contract, the target is set to the current contract, and owner of the proxy is set to the account that is signing the transaction for deployment. + +This means that if you simply enable proxy in the `Forc.toml`, forc will automatically deploy a proxy contract for you and you do not need to do anything manually aside from signing the deployment transactions for the proxy contract. After deploying the proxy contract, the its address is added into the `address` field of the proxy table. + +If you want to update the target of an [SRC-14](https://github.com/FuelLabs/sway-standards/blob/master/docs/src/src-14-simple-upgradeable-proxies.md) compliant proxy contract rather than deploying a new one, simply add its `address` in the `address` field, like the following example: + +```TOML +[project] +name = "test_contract" +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +implicit-std = false + +[proxy] +enabled = true +address = "0xd8c4b07a0d1be57b228f4c18ba7bca0c8655eb6e9d695f14080f2cf4fc7cd946" # example proxy contract address +``` + +If an `address` is present, `forc` calls into that contract to update its `target` instead of deploying a new contract. Since a new proxy deployment adds its own `address` into the `Forc.toml` automatically, you can simply enable the proxy once and after the initial deployment, `forc` will keep updating the target accordingly for each new deployment of the same contract. diff --git a/forc-pkg/src/manifest/mod.rs b/forc-pkg/src/manifest/mod.rs index 3f7bc21b1bd..28729aff757 100644 --- a/forc-pkg/src/manifest/mod.rs +++ b/forc-pkg/src/manifest/mod.rs @@ -186,6 +186,7 @@ pub struct PackageManifest { pub build_target: Option>, build_profile: Option>, pub contract_dependencies: Option>, + pub proxy: Option, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] @@ -273,6 +274,17 @@ pub struct DependencyDetails { pub(crate) ipfs: Option, } +/// Describes the details around proxy contract. +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct Proxy { + pub enabled: bool, + /// Points to the proxy contract to be updated with the new contract id. + /// If there is a value for this field, forc will try to update the proxy contract's storage + /// field such that it points to current contract's deployed instance. + pub address: Option, +} + impl DependencyDetails { /// Checks if dependency details reserved for a specific dependency type used without the main /// detail for that type. @@ -650,6 +662,11 @@ impl PackageManifest { .and_then(|patches| patches.get(patch_name)) } + /// Retrieve the proxy table for the package. + pub fn proxy(&self) -> Option<&Proxy> { + self.proxy.as_ref() + } + /// Check for the `core` and `std` packages under `[dependencies]`. If both are missing, add /// `std` implicitly. /// diff --git a/forc-pkg/src/pkg.rs b/forc-pkg/src/pkg.rs index 22b4dc6ed5c..1937933719a 100644 --- a/forc-pkg/src/pkg.rs +++ b/forc-pkg/src/pkg.rs @@ -285,7 +285,7 @@ pub struct MinifyOpts { type ContractIdConst = String; /// The set of options provided to the `build` functions. -#[derive(Default)] +#[derive(Default, Clone)] pub struct BuildOpts { pub pkg: PkgOpts, pub print: PrintOpts, @@ -318,6 +318,7 @@ pub struct BuildOpts { } /// The set of options to filter type of projects to build in a workspace. +#[derive(Clone)] pub struct MemberFilter { pub build_contracts: bool, pub build_scripts: bool, @@ -2153,6 +2154,8 @@ pub fn build_with_options(build_options: &BuildOpts) -> Result { .as_ref() .map_or_else(|| current_dir, PathBuf::from); + println_action_green("Building", &path.display().to_string()); + let build_plan = BuildPlan::from_pkg_opts(&build_options.pkg)?; let graph = build_plan.graph(); let manifest_map = build_plan.manifest_map(); diff --git a/forc-plugins/forc-client/Cargo.toml b/forc-plugins/forc-client/Cargo.toml index 285462715a9..7b9e3160333 100644 --- a/forc-plugins/forc-client/Cargo.toml +++ b/forc-plugins/forc-client/Cargo.toml @@ -27,6 +27,7 @@ fuel-core-types = { workspace = true } fuel-crypto = { workspace = true } fuel-tx = { workspace = true, features = ["test-helpers"] } fuel-vm = { workspace = true } +fuels = { workspace = true } fuels-accounts = { workspace = true } fuels-core = { workspace = true } futures = "0.3" @@ -39,6 +40,7 @@ sway-core = { version = "0.62.0", path = "../../sway-core" } sway-types = { version = "0.62.0", path = "../../sway-types" } sway-utils = { version = "0.62.0", path = "../../sway-utils" } tokio = { version = "1.8", features = ["macros", "rt-multi-thread", "process"] } +toml_edit = "0.21.1" tracing = "0.1" [dev-dependencies] diff --git a/forc-plugins/forc-client/proxy_abi/README.md b/forc-plugins/forc-client/proxy_abi/README.md new file mode 100644 index 00000000000..47a9b800ff7 --- /dev/null +++ b/forc-plugins/forc-client/proxy_abi/README.md @@ -0,0 +1,8 @@ +# Proxy Contract + +This folder contains pre-built version of the owned proxy contract, its abi and `storage-slots.json` file. + +*contract url*: [sway-standard-implementation/src-14/owned_proxy](https://github.com/FuelLabs/sway-standard-implementations/tree/61fd4ad8f69d21cec0d5cd8135bdc4495e0c125c). +*commit hash*: `61fd4ad8f69d21cec0d5cd8135bdc4495e0c125c` +*forc version*: `v0.62.0` +*build command*: `forc build --release` diff --git a/forc-plugins/forc-client/proxy_abi/proxy_contract-abi.json b/forc-plugins/forc-client/proxy_abi/proxy_contract-abi.json new file mode 100644 index 00000000000..10ad2eeedc4 --- /dev/null +++ b/forc-plugins/forc-client/proxy_abi/proxy_contract-abi.json @@ -0,0 +1,774 @@ +{ + "encoding": "1", + "types": [ + { + "typeId": 0, + "type": "()", + "components": [], + "typeParameters": null + }, + { + "typeId": 1, + "type": "b256", + "components": null, + "typeParameters": null + }, + { + "typeId": 2, + "type": "enum AccessError", + "components": [ + { + "name": "NotOwner", + "type": 0, + "typeArguments": null + } + ], + "typeParameters": null + }, + { + "typeId": 3, + "type": "enum Identity", + "components": [ + { + "name": "Address", + "type": 10, + "typeArguments": null + }, + { + "name": "ContractId", + "type": 11, + "typeArguments": null + } + ], + "typeParameters": null + }, + { + "typeId": 4, + "type": "enum InitializationError", + "components": [ + { + "name": "CannotReinitialized", + "type": 0, + "typeArguments": null + } + ], + "typeParameters": null + }, + { + "typeId": 5, + "type": "enum Option", + "components": [ + { + "name": "None", + "type": 0, + "typeArguments": null + }, + { + "name": "Some", + "type": 8, + "typeArguments": null + } + ], + "typeParameters": [ + 8 + ] + }, + { + "typeId": 6, + "type": "enum SetProxyOwnerError", + "components": [ + { + "name": "CannotUninitialize", + "type": 0, + "typeArguments": null + } + ], + "typeParameters": null + }, + { + "typeId": 7, + "type": "enum State", + "components": [ + { + "name": "Uninitialized", + "type": 0, + "typeArguments": null + }, + { + "name": "Initialized", + "type": 3, + "typeArguments": null + }, + { + "name": "Revoked", + "type": 0, + "typeArguments": null + } + ], + "typeParameters": null + }, + { + "typeId": 8, + "type": "generic T", + "components": null, + "typeParameters": null + }, + { + "typeId": 9, + "type": "str", + "components": null, + "typeParameters": null + }, + { + "typeId": 10, + "type": "struct Address", + "components": [ + { + "name": "bits", + "type": 1, + "typeArguments": null + } + ], + "typeParameters": null + }, + { + "typeId": 11, + "type": "struct ContractId", + "components": [ + { + "name": "bits", + "type": 1, + "typeArguments": null + } + ], + "typeParameters": null + }, + { + "typeId": 12, + "type": "struct ProxyOwnerSet", + "components": [ + { + "name": "new_proxy_owner", + "type": 7, + "typeArguments": null + } + ], + "typeParameters": null + }, + { + "typeId": 13, + "type": "struct ProxyTargetSet", + "components": [ + { + "name": "new_target", + "type": 11, + "typeArguments": null + } + ], + "typeParameters": null + } + ], + "functions": [ + { + "inputs": [], + "name": "proxy_target", + "output": { + "name": "", + "type": 5, + "typeArguments": [ + { + "name": "", + "type": 11, + "typeArguments": null + } + ] + }, + "attributes": [ + { + "name": "doc-comment", + "arguments": [ + " Returns the target contract of the proxy contract." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Returns" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * [Option] - The new proxy contract to which all fallback calls will be passed or `None`." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Number of Storage Accesses" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * Reads: `1`" + ] + }, + { + "name": "storage", + "arguments": [ + "read" + ] + } + ] + }, + { + "inputs": [ + { + "name": "new_target", + "type": 11, + "typeArguments": null + } + ], + "name": "set_proxy_target", + "output": { + "name": "", + "type": 0, + "typeArguments": null + }, + "attributes": [ + { + "name": "doc-comment", + "arguments": [ + " Change the target contract of the proxy contract." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Additional Information" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " This method can only be called by the `proxy_owner`." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Arguments" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * `new_target`: [ContractId] - The new proxy contract to which all fallback calls will be passed." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Reverts" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * When not called by `proxy_owner`." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Number of Storage Accesses" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * Reads: `1`" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * Write: `1`" + ] + }, + { + "name": "storage", + "arguments": [ + "read", + "write" + ] + } + ] + }, + { + "inputs": [], + "name": "proxy_owner", + "output": { + "name": "", + "type": 7, + "typeArguments": null + }, + "attributes": [ + { + "name": "doc-comment", + "arguments": [ + " Returns the owner of the proxy contract." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Returns" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * [State] - Represents the state of ownership for this contract." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Number of Storage Accesses" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * Reads: `1`" + ] + }, + { + "name": "storage", + "arguments": [ + "read" + ] + } + ] + }, + { + "inputs": [], + "name": "initialize_proxy", + "output": { + "name": "", + "type": 0, + "typeArguments": null + }, + "attributes": [ + { + "name": "doc-comment", + "arguments": [ + " Initializes the proxy contract." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Additional Information" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " This method sets the storage values using the values of the configurable constants `INITIAL_TARGET` and `INITIAL_OWNER`." + ] + }, + { + "name": "doc-comment", + "arguments": [ + " This then allows methods that write to storage to be called." + ] + }, + { + "name": "doc-comment", + "arguments": [ + " This method can only be called once." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Reverts" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * When `storage.proxy_owner` is not [State::Uninitialized]." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Number of Storage Accesses" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * Writes: `2`" + ] + }, + { + "name": "storage", + "arguments": [ + "write" + ] + } + ] + }, + { + "inputs": [ + { + "name": "new_proxy_owner", + "type": 7, + "typeArguments": null + } + ], + "name": "set_proxy_owner", + "output": { + "name": "", + "type": 0, + "typeArguments": null + }, + "attributes": [ + { + "name": "doc-comment", + "arguments": [ + " Changes proxy ownership to the passed State." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Additional Information" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " This method can be used to transfer ownership between Identities or to revoke ownership." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Arguments" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * `new_proxy_owner`: [State] - The new state of the proxy ownership." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Reverts" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * When the sender is not the current proxy owner." + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * When the new state of the proxy ownership is [State::Uninitialized]." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Number of Storage Accesses" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * Reads: `1`" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * Writes: `1`" + ] + }, + { + "name": "storage", + "arguments": [ + "write" + ] + } + ] + } + ], + "loggedTypes": [ + { + "logId": "4571204900286667806", + "loggedType": { + "name": "", + "type": 2, + "typeArguments": [] + } + }, + { + "logId": "2151606668983994881", + "loggedType": { + "name": "", + "type": 13, + "typeArguments": [] + } + }, + { + "logId": "2161305517876418151", + "loggedType": { + "name": "", + "type": 4, + "typeArguments": [] + } + }, + { + "logId": "4354576968059844266", + "loggedType": { + "name": "", + "type": 6, + "typeArguments": [] + } + }, + { + "logId": "10870989709723147660", + "loggedType": { + "name": "", + "type": 12, + "typeArguments": [] + } + }, + { + "logId": "10098701174489624218", + "loggedType": { + "name": "", + "type": 9, + "typeArguments": null + } + } + ], + "messagesTypes": [], + "configurables": [ + { + "name": "INITIAL_TARGET", + "configurableType": { + "name": "", + "type": 5, + "typeArguments": [ + { + "name": "", + "type": 11, + "typeArguments": [] + } + ] + }, + "offset": 16224 + }, + { + "name": "INITIAL_OWNER", + "configurableType": { + "name": "", + "type": 7, + "typeArguments": [] + }, + "offset": 16264 + } + ] +} \ No newline at end of file diff --git a/forc-plugins/forc-client/proxy_abi/proxy_contract-storage_slots.json b/forc-plugins/forc-client/proxy_abi/proxy_contract-storage_slots.json new file mode 100644 index 00000000000..72849c97055 --- /dev/null +++ b/forc-plugins/forc-client/proxy_abi/proxy_contract-storage_slots.json @@ -0,0 +1,18 @@ +[ + { + "key": "7bb458adc1d118713319a5baa00a2d049dd64d2916477d2688d76970c898cd55", + "value": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "key": "7bb458adc1d118713319a5baa00a2d049dd64d2916477d2688d76970c898cd56", + "value": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "key": "bb79927b15d9259ea316f2ecb2297d6cc8851888a98278c0a2e03e1a091ea754", + "value": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "key": "bb79927b15d9259ea316f2ecb2297d6cc8851888a98278c0a2e03e1a091ea755", + "value": "0000000000000000000000000000000000000000000000000000000000000000" + } +] \ No newline at end of file diff --git a/forc-plugins/forc-client/proxy_abi/proxy_contract.bin b/forc-plugins/forc-client/proxy_abi/proxy_contract.bin new file mode 100644 index 00000000000..ea52800465d Binary files /dev/null and b/forc-plugins/forc-client/proxy_abi/proxy_contract.bin differ diff --git a/forc-plugins/forc-client/src/cmd/run.rs b/forc-plugins/forc-client/src/cmd/run.rs index e23d9f6cb43..5875b270adc 100644 --- a/forc-plugins/forc-client/src/cmd/run.rs +++ b/forc-plugins/forc-client/src/cmd/run.rs @@ -52,9 +52,6 @@ pub struct Command { pub unsigned: bool, /// Set the key to be used for signing. pub signing_key: Option, - /// Sign the deployment transaction manually. - #[clap(long)] - pub manual_signing: bool, /// Arguments to pass into main function with forc run. #[clap(long)] pub args: Option>, diff --git a/forc-plugins/forc-client/src/lib.rs b/forc-plugins/forc-client/src/lib.rs index 0c8cb335f5f..cce5e5ab95e 100644 --- a/forc-plugins/forc-client/src/lib.rs +++ b/forc-plugins/forc-client/src/lib.rs @@ -1,7 +1,7 @@ pub mod cmd; -mod constants; +pub mod constants; pub mod op; -mod util; +pub mod util; use clap::Parser; use serde::{Deserialize, Serialize}; diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 1dc9efd2e6e..f41fd80532c 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -3,9 +3,12 @@ use crate::{ constants::TX_SUBMIT_TIMEOUT_MS, util::{ node_url::get_node_url, - pkg::built_pkgs, + pkg::{built_pkgs, create_proxy_contract, update_proxy_address_in_manifest}, target::Target, - tx::{prompt_forc_wallet_password, select_secret_key, WalletSelectionMode}, + tx::{ + bech32_from_secret, prompt_forc_wallet_password, select_secret_key, + update_proxy_contract_target, WalletSelectionMode, + }, }, }; use anyhow::{bail, Context, Result}; @@ -19,6 +22,7 @@ use fuel_core_client::client::FuelClient; use fuel_crypto::fuel_types::ChainId; use fuel_tx::{Salt, Transaction}; use fuel_vm::prelude::*; +use fuels::programs::contract::{LoadConfiguration, StorageConfiguration}; use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account}; use fuels_core::types::{transaction::TxPolicies, transaction_builders::CreateTransactionBuilder}; use futures::FutureExt; @@ -28,14 +32,16 @@ use std::{ collections::BTreeMap, path::{Path, PathBuf}, str::FromStr, + sync::Arc, + time::Duration, }; -use std::{sync::Arc, time::Duration}; use sway_core::language::parsed::TreeType; use sway_core::BuildTarget; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct DeployedContract { pub id: fuel_tx::ContractId, + pub proxy: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -116,6 +122,58 @@ fn validate_and_parse_salts<'a>( Ok(contract_salt_map) } +/// Deploys a new proxy contract for the given package. +async fn deploy_new_proxy( + pkg_name: &str, + impl_contract: &fuel_tx::ContractId, + provider: &Provider, + signing_key: &SecretKey, +) -> Result { + fuels::macros::abigen!(Contract( + name = "ProxyContract", + abi = "forc-plugins/forc-client/proxy_abi/proxy_contract-abi.json" + )); + let proxy_dir_output = create_proxy_contract(pkg_name)?; + let address = bech32_from_secret(signing_key)?; + let wallet = WalletUnlocked::new_from_private_key(*signing_key, Some(provider.clone())); + + let storage_path = proxy_dir_output.join("proxy-storage_slots.json"); + let storage_configuration = + StorageConfiguration::default().add_slot_overrides_from_file(storage_path)?; + + let configurables = ProxyContractConfigurables::default() + .with_INITIAL_TARGET(Some(*impl_contract))? + .with_INITIAL_OWNER(State::Initialized(Address::from(address).into()))?; + + let configuration = LoadConfiguration::default() + .with_storage_configuration(storage_configuration) + .with_configurables(configurables); + + let proxy_contract_id = fuels::programs::contract::Contract::load_from( + proxy_dir_output.join("proxy.bin"), + configuration, + )? + .deploy(&wallet, TxPolicies::default()) + .await?; + + let chain_info = provider.chain_info().await?; + let target = Target::from_str(&chain_info.name).unwrap_or(Target::testnet()); + let contract_url = match target.explorer_url() { + Some(explorer_url) => format!("{explorer_url}/contract/0x"), + None => "".to_string(), + }; + + println_action_green( + "Finished", + &format!("deploying proxy contract for {pkg_name} {contract_url}{proxy_contract_id}"), + ); + + let instance = ProxyContract::new(&proxy_contract_id, wallet); + instance.methods().initialize_proxy().call().await?; + println_action_green("Initialized", &format!("proxy contract for {pkg_name}")); + Ok(proxy_contract_id.into()) +} + /// Builds and deploys contract(s). If the given path corresponds to a workspace, all deployable members /// will be built and deployed. /// @@ -127,7 +185,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { println_warning("--unsigned flag is deprecated, please prefer using --default-signer. Assuming `--default-signer` is passed. This means your transaction will be signed by an account that is funded by fuel-core by default for testing purposes."); } - let mut contract_ids = Vec::new(); + let mut deployed_contracts = Vec::new(); let curr_dir = if let Some(ref path) = command.pkg.path { PathBuf::from(path) } else { @@ -148,7 +206,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { if pkgs_to_deploy.is_empty() { println_warning("No deployable contracts found in the current directory."); - return Ok(contract_ids); + return Ok(deployed_contracts); } let contract_salt_map = if let Some(salt_input) = &command.salt { @@ -219,18 +277,57 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { bail!("Both `--salt` and `--default-salt` were specified: must choose one") } }; - let contract_id = deploy_pkg( - &command, - pkg, - salt, - &provider, - &signing_key, - node_url.clone(), - ) - .await?; - contract_ids.push(contract_id); + let deployed_contract_id = deploy_pkg(&command, pkg, salt, &provider, &signing_key).await?; + + let proxy_id = match &pkg.descriptor.manifest_file.proxy { + Some(forc_pkg::manifest::Proxy { + enabled: true, + address: Some(proxy_addr), + }) => { + // Make a call into the contract to update impl contract address to 'deployed_contract'. + + // Create a contract instance for the proxy contract using default proxy contract abi and + // specified address. + let proxy_contract = + ContractId::from_str(proxy_addr).map_err(|e| anyhow::anyhow!(e))?; + + update_proxy_contract_target( + &provider, + signing_key, + proxy_contract, + deployed_contract_id, + ) + .await?; + Some(proxy_contract) + } + Some(forc_pkg::manifest::Proxy { + enabled: true, + address: None, + }) => { + let pkg_name = &pkg.descriptor.name; + // Deploy a new proxy contract. + let deployed_proxy_contract = + deploy_new_proxy(pkg_name, &deployed_contract_id, &provider, &signing_key) + .await?; + + // Update manifest file such that the proxy address field points to the new proxy contract. + update_proxy_address_in_manifest( + &format!("0x{}", deployed_proxy_contract), + &pkg.descriptor.manifest_file, + )?; + Some(deployed_proxy_contract) + } + // Proxy not enabled. + _ => None, + }; + + let deployed_contract = DeployedContract { + id: deployed_contract_id, + proxy: proxy_id, + }; + deployed_contracts.push(deployed_contract); } - Ok(contract_ids) + Ok(deployed_contracts) } /// Prompt the user to confirm the transactions required for deployment, as well as the signing key. @@ -240,14 +337,34 @@ async fn confirm_transaction_details( node_url: String, ) -> Result<(Provider, SecretKey)> { // Confirmation step. Summarize the transaction(s) for the deployment. + let mut tx_count = 0; let tx_summary = pkgs_to_deploy .iter() - .map(|pkg| format!("deploy {}", pkg.descriptor.manifest_file.project_name())) + .map(|pkg| { + tx_count += 1; + let proxy_text = match &pkg.descriptor.manifest_file.proxy { + Some(forc_pkg::manifest::Proxy { + enabled: true, + address, + }) => { + tx_count += 1; + if address.is_some() { + " + update proxy" + } else { + " + deploy proxy" + } + } + _ => "", + }; + + format!( + "deploy {}{proxy_text}", + pkg.descriptor.manifest_file.project_name() + ) + }) .collect::>() .join(" + "); - let tx_count = pkgs_to_deploy.len(); - println_action_green("Confirming", &format!("transactions [{tx_summary}]")); println_action_green("", &format!("Network: {node_url}")); @@ -284,10 +401,10 @@ pub async fn deploy_pkg( salt: Salt, provider: &Provider, signing_key: &SecretKey, - node_url: String, -) -> Result { +) -> Result { let manifest = &compiled.descriptor.manifest_file; - let client = FuelClient::new(node_url.clone())?; + let node_url = provider.url(); + let client = FuelClient::new(node_url)?; let bytecode = &compiled.bytecode.bytes; @@ -398,7 +515,7 @@ pub async fn deploy_pkg( &contract_id ) })??; - Ok(DeployedContract { id: contract_id }) + Ok(contract_id) } fn build_opts_from_cmd(cmd: &cmd::Deploy) -> pkg::BuildOpts { diff --git a/forc-plugins/forc-client/src/util/mod.rs b/forc-plugins/forc-client/src/util/mod.rs index 3b99a89659f..d8024081b28 100644 --- a/forc-plugins/forc-client/src/util/mod.rs +++ b/forc-plugins/forc-client/src/util/mod.rs @@ -3,4 +3,4 @@ pub(crate) mod gas; pub(crate) mod node_url; pub(crate) mod pkg; pub(crate) mod target; -pub(crate) mod tx; +pub mod tx; diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index f7b6384cb74..94a068a5bea 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -1,9 +1,62 @@ use anyhow::Result; use forc_pkg::manifest::GenericManifestFile; use forc_pkg::{self as pkg, manifest::ManifestFile, BuildOpts, BuildPlan}; -use pkg::{build_with_options, BuiltPackage}; +use forc_util::user_forc_directory; +use pkg::{build_with_options, BuiltPackage, PackageManifestFile}; +use std::fs::File; +use std::io::{Read, Write}; +use std::path::PathBuf; use std::{collections::HashMap, path::Path, sync::Arc}; +/// The name of the folder that forc generated proxy contract project will reside at. +pub const PROXY_CONTRACT_FOLDER_NAME: &str = ".generated_proxy_contracts"; +pub const PROXY_CONTRACT_BIN: &[u8] = include_bytes!("../../proxy_abi/proxy_contract.bin"); +pub const PROXY_CONTRACT_STORAGE_SLOTS: &str = + include_str!("../../proxy_abi/proxy_contract-storage_slots.json"); +pub const PROXY_BIN_FILE_NAME: &str = "proxy.bin"; +pub const PROXY_STORAGE_SLOTS_FILE_NAME: &str = "proxy-storage_slots.json"; + +/// Updates the given package manifest file such that the address field under the proxy table updated to the given value. +/// Updated manifest file is written back to the same location, without thouching anything else such as comments etc. +/// A safety check is done to ensure the proxy table exists before attempting to udpdate the value. +pub(crate) fn update_proxy_address_in_manifest( + address: &str, + manifest: &PackageManifestFile, +) -> Result<()> { + let mut toml = String::new(); + let mut file = File::open(manifest.path())?; + file.read_to_string(&mut toml)?; + let mut manifest_toml = toml.parse::()?; + if manifest.proxy().is_some() { + manifest_toml["proxy"]["address"] = toml_edit::value(address); + let mut file = std::fs::OpenOptions::new() + .write(true) + .truncate(true) + .open(manifest.path())?; + file.write_all(manifest_toml.to_string().as_bytes())?; + } + Ok(()) +} + +/// Creates a proxy contract project at the given path, adds a forc.toml and source file. +pub(crate) fn create_proxy_contract(pkg_name: &str) -> Result { + // Create the proxy contract folder. + let proxy_contract_dir = user_forc_directory() + .join(PROXY_CONTRACT_FOLDER_NAME) + .join(pkg_name); + std::fs::create_dir_all(&proxy_contract_dir)?; + std::fs::write( + proxy_contract_dir.join(PROXY_BIN_FILE_NAME), + PROXY_CONTRACT_BIN, + )?; + std::fs::write( + proxy_contract_dir.join(PROXY_STORAGE_SLOTS_FILE_NAME), + PROXY_CONTRACT_STORAGE_SLOTS, + )?; + + Ok(proxy_contract_dir) +} + pub(crate) fn built_pkgs(path: &Path, build_opts: &BuildOpts) -> Result>> { let manifest_file = ManifestFile::from_dir(path)?; let lock_path = manifest_file.lock_path()?; diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index d2a82c7f493..f71501a0941 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -2,7 +2,7 @@ use crate::{constants::DEFAULT_PRIVATE_KEY, util::target::Target}; use anyhow::{Error, Result}; use async_trait::async_trait; use dialoguer::{theme::ColorfulTheme, Confirm, Password, Select}; -use forc_tracing::println_warning; +use forc_tracing::{println_action_green, println_warning}; use forc_wallet::{ account::{derive_secret_key, new_at_index_cli}, balance::{ @@ -11,13 +11,18 @@ use forc_wallet::{ new::{new_wallet_cli, New}, utils::default_wallet_path, }; -use fuel_crypto::{Message, SecretKey, Signature}; +use fuel_crypto::{Message, PublicKey, SecretKey, Signature}; use fuel_tx::{ field, Address, AssetId, Buildable, ContractId, Input, Output, TransactionBuilder, Witness, }; -use fuels_accounts::{provider::Provider, wallet::Wallet, ViewOnlyAccount}; +use fuels::{macros::abigen, programs::responses::CallResponse}; +use fuels_accounts::{ + provider::Provider, + wallet::{Wallet, WalletUnlocked}, + ViewOnlyAccount, +}; use fuels_core::types::{ - bech32::Bech32Address, + bech32::{Bech32Address, FUEL_BECH32_HRP}, coin_type::CoinType, transaction_builders::{create_coin_input, create_coin_message_input}, }; @@ -116,6 +121,13 @@ pub(crate) fn secret_key_from_forc_wallet( Ok(secret_key) } +pub(crate) fn bech32_from_secret(secret_key: &SecretKey) -> Result { + let public_key = PublicKey::from(secret_key); + let hashed = public_key.hash(); + let bech32 = Bech32Address::new(FUEL_BECH32_HRP, hashed); + Ok(bech32) +} + pub(crate) fn select_manual_secret_key( default_signer: bool, signing_key: Option, @@ -244,6 +256,33 @@ pub(crate) async fn select_secret_key( Ok(signing_key) } +pub async fn update_proxy_contract_target( + provider: &Provider, + secret_key: SecretKey, + proxy_contract_id: ContractId, + new_target: ContractId, +) -> Result> { + abigen!(Contract( + name = "ProxyContract", + abi = "forc-plugins/forc-client/proxy_abi/proxy_contract-abi.json" + )); + + let wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider.clone())); + + let proxy_contract = ProxyContract::new(proxy_contract_id, wallet); + + let result = proxy_contract + .methods() + .set_proxy_target(new_target) + .call() + .await?; + println_action_green( + "Updated", + &format!("proxy contract target to 0x{new_target}"), + ); + Ok(result) +} + #[async_trait] pub trait TransactionBuilderExt { fn add_contract(&mut self, contract_id: ContractId) -> &mut Self; diff --git a/forc-plugins/forc-client/test/data/contract_with_dep/Forc.lock b/forc-plugins/forc-client/test/data/contract_with_dep/Forc.lock deleted file mode 100644 index 9722a217392..00000000000 --- a/forc-plugins/forc-client/test/data/contract_with_dep/Forc.lock +++ /dev/null @@ -1,19 +0,0 @@ -[[package]] -name = "contract_with_dep" -source = "member" -dependencies = ["std"] -contract-dependencies = ["standalone_contract (0000000000000000000000000000000000000000000000000000000000000001)"] - -[[package]] -name = "core" -source = "path+from-root-9B9D657E3F1FCA11" - -[[package]] -name = "standalone_contract" -source = "path+from-root-9B9D657E3F1FCA11" -dependencies = ["std"] - -[[package]] -name = "std" -source = "path+from-root-9B9D657E3F1FCA11" -dependencies = ["core"] diff --git a/forc-plugins/forc-client/test/data/standalone_contract/Forc.lock b/forc-plugins/forc-client/test/data/standalone_contract/Forc.lock deleted file mode 100644 index 7b517045569..00000000000 --- a/forc-plugins/forc-client/test/data/standalone_contract/Forc.lock +++ /dev/null @@ -1,13 +0,0 @@ -[[package]] -name = "core" -source = "path+from-root-79BB3EA8498403DE" - -[[package]] -name = "standalone_contract" -source = "member" -dependencies = ["std"] - -[[package]] -name = "std" -source = "path+from-root-79BB3EA8498403DE" -dependencies = ["core"] diff --git a/forc-plugins/forc-client/test/data/standalone_contract/standalone_contract-abi.json b/forc-plugins/forc-client/test/data/standalone_contract/standalone_contract-abi.json new file mode 100644 index 00000000000..f8ba2707608 --- /dev/null +++ b/forc-plugins/forc-client/test/data/standalone_contract/standalone_contract-abi.json @@ -0,0 +1,26 @@ +{ + "encoding": "1", + "types": [ + { + "typeId": 0, + "type": "bool", + "components": null, + "typeParameters": null + } + ], + "functions": [ + { + "inputs": [], + "name": "test_function", + "output": { + "name": "", + "type": 0, + "typeArguments": null + }, + "attributes": null + } + ], + "loggedTypes": [], + "messagesTypes": [], + "configurables": [] +} \ No newline at end of file diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index fa269b1dff7..8f45d4a3478 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -2,10 +2,16 @@ use forc::cli::shared::Pkg; use forc_client::{ cmd, op::{deploy, DeployedContract}, + util::tx::update_proxy_contract_target, NodeTarget, }; +use forc_pkg::manifest::Proxy; +use fuel_crypto::SecretKey; use fuel_tx::{ContractId, Salt}; +use fuels::{macros::abigen, types::transaction::TxPolicies}; +use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account}; use portpicker::Port; +use rand::thread_rng; use rexpect::spawn; use std::{ fs, @@ -14,7 +20,7 @@ use std::{ str::FromStr, }; use tempfile::tempdir; -use toml_edit::{Document, InlineTable, Item, Value}; +use toml_edit::{value, Document, InlineTable, Item, Table, Value}; fn get_workspace_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -77,6 +83,34 @@ fn patch_manifest_file_with_path_std(manifest_dir: &Path) -> anyhow::Result<()> Ok(()) } +fn patch_manifest_file_with_proxy_table(manifest_dir: &Path, proxy: Proxy) -> anyhow::Result<()> { + let toml_path = manifest_dir.join(sway_utils::constants::MANIFEST_FILE_NAME); + let toml_content = fs::read_to_string(&toml_path)?; + let mut doc = toml_content.parse::()?; + + let proxy_table = doc.entry("proxy").or_insert(Item::Table(Table::new())); + let proxy_table = proxy_table.as_table_mut().unwrap(); + + proxy_table.insert("enabled", value(proxy.enabled)); + + if let Some(address) = proxy.address { + proxy_table.insert("address", value(address)); + } else { + proxy_table.remove("address"); + } + + fs::write(&toml_path, doc.to_string())?; + Ok(()) +} + +fn update_main_sw(tmp_dir: &Path) -> anyhow::Result<()> { + let main_sw_path = tmp_dir.join("src").join("main.sw"); + let content = fs::read_to_string(&main_sw_path)?; + let updated_content = content.replace("true", "false"); + fs::write(main_sw_path, updated_content)?; + Ok(()) +} + #[tokio::test] async fn test_simple_deploy() { let (mut node, port) = run_node(); @@ -110,11 +144,235 @@ async fn test_simple_deploy() { "822c8d3672471f64f14f326447793c7377b6e430122db23b622880ccbd8a33ef", ) .unwrap(), + proxy: None, }]; assert_eq!(contract_ids, expected) } +#[tokio::test] +async fn test_deploy_fresh_proxy() { + let (mut node, port) = run_node(); + let tmp_dir = tempdir().unwrap(); + let project_dir = test_data_path().join("standalone_contract"); + copy_dir(&project_dir, tmp_dir.path()).unwrap(); + patch_manifest_file_with_path_std(tmp_dir.path()).unwrap(); + let proxy = Proxy { + enabled: true, + address: None, + }; + patch_manifest_file_with_proxy_table(tmp_dir.path(), proxy).unwrap(); + + let pkg = Pkg { + path: Some(tmp_dir.path().display().to_string()), + ..Default::default() + }; + + let node_url = format!("http://127.0.0.1:{}/v1/graphql", port); + let target = NodeTarget { + node_url: Some(node_url), + target: None, + testnet: false, + }; + let cmd = cmd::Deploy { + pkg, + salt: Some(vec![format!("{}", Salt::default())]), + node: target, + default_signer: true, + ..Default::default() + }; + let contract_ids = deploy(cmd).await.unwrap(); + node.kill().unwrap(); + let impl_contract = DeployedContract { + id: ContractId::from_str( + "822c8d3672471f64f14f326447793c7377b6e430122db23b622880ccbd8a33ef", + ) + .unwrap(), + proxy: Some( + ContractId::from_str( + "3da2f8ee967c62496db4b71df0acd7c3fea1e494fee1de0cd16e7abd22e6057f", + ) + .unwrap(), + ), + }; + let expected = vec![impl_contract]; + + assert_eq!(contract_ids, expected) +} + +#[tokio::test] +async fn test_proxy_contract_re_routes_call() { + let (mut node, port) = run_node(); + let tmp_dir = tempdir().unwrap(); + let project_dir = test_data_path().join("standalone_contract"); + copy_dir(&project_dir, tmp_dir.path()).unwrap(); + patch_manifest_file_with_path_std(tmp_dir.path()).unwrap(); + let proxy = Proxy { + enabled: true, + address: None, + }; + patch_manifest_file_with_proxy_table(tmp_dir.path(), proxy).unwrap(); + + let pkg = Pkg { + path: Some(tmp_dir.path().display().to_string()), + ..Default::default() + }; + + let node_url = format!("http://127.0.0.1:{}/v1/graphql", port); + let target = NodeTarget { + node_url: Some(node_url.clone()), + target: None, + testnet: false, + }; + let cmd = cmd::Deploy { + pkg, + salt: Some(vec![format!("{}", Salt::default())]), + node: target, + default_signer: true, + ..Default::default() + }; + let contract_ids = deploy(cmd).await.unwrap(); + // At this point we deployed a contract with proxy. + let proxy_contract_id = contract_ids[0].proxy.unwrap(); + let impl_contract_id = contract_ids[0].id; + // Make a contract call into proxy contract, and check if the initial + // contract returns a true. + let provider = Provider::connect(&node_url).await.unwrap(); + let secret_key = SecretKey::from_str(forc_client::constants::DEFAULT_PRIVATE_KEY).unwrap(); + let wallet_unlocked = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); + + abigen!(Contract( + name = "ImplementationContract", + abi = "forc-plugins/forc-client/test/data/standalone_contract/standalone_contract-abi.json" + )); + + let impl_contract_a = ImplementationContract::new(proxy_contract_id, wallet_unlocked.clone()); + let res = impl_contract_a + .methods() + .test_function() + .with_contract_ids(&[impl_contract_id.into()]) + .call() + .await + .unwrap(); + assert!(res.value); + + update_main_sw(tmp_dir.path()).unwrap(); + let target = NodeTarget { + node_url: Some(node_url.clone()), + target: None, + testnet: false, + }; + let pkg = Pkg { + path: Some(tmp_dir.path().display().to_string()), + ..Default::default() + }; + + let cmd = cmd::Deploy { + pkg, + salt: Some(vec![format!("{}", Salt::default())]), + node: target, + default_signer: true, + ..Default::default() + }; + let contract_ids = deploy(cmd).await.unwrap(); + // proxy contract id should be the same. + let proxy_contract_after_update = contract_ids[0].proxy.unwrap(); + assert_eq!(proxy_contract_id, proxy_contract_after_update); + let impl_contract_id_after_update = contract_ids[0].id; + assert!(impl_contract_id != impl_contract_id_after_update); + let impl_contract_a = ImplementationContract::new(proxy_contract_after_update, wallet_unlocked); + let res = impl_contract_a + .methods() + .test_function() + .with_contract_ids(&[impl_contract_id_after_update.into()]) + .call() + .await + .unwrap(); + assert!(!res.value); + node.kill().unwrap(); +} + +#[tokio::test] +async fn test_non_owner_fails_to_set_target() { + let (mut node, port) = run_node(); + let tmp_dir = tempdir().unwrap(); + let project_dir = test_data_path().join("standalone_contract"); + copy_dir(&project_dir, tmp_dir.path()).unwrap(); + patch_manifest_file_with_path_std(tmp_dir.path()).unwrap(); + let proxy = Proxy { + enabled: true, + address: None, + }; + patch_manifest_file_with_proxy_table(tmp_dir.path(), proxy).unwrap(); + + let pkg = Pkg { + path: Some(tmp_dir.path().display().to_string()), + ..Default::default() + }; + + let node_url = format!("http://127.0.0.1:{}/v1/graphql", port); + let target = NodeTarget { + node_url: Some(node_url.clone()), + target: None, + testnet: false, + }; + let cmd = cmd::Deploy { + pkg, + salt: Some(vec![format!("{}", Salt::default())]), + node: target, + default_signer: true, + ..Default::default() + }; + let contract_id = deploy(cmd).await.unwrap(); + // Proxy contract's id. + let proxy_id = contract_id.first().and_then(|f| f.proxy).unwrap(); + + // Create and fund an owner account and an attacker account. + let provider = Provider::connect(&node_url).await.unwrap(); + let attacker_secret_key = SecretKey::random(&mut thread_rng()); + let attacker_wallet = + WalletUnlocked::new_from_private_key(attacker_secret_key, Some(provider.clone())); + + let owner_secret_key = + SecretKey::from_str(forc_client::constants::DEFAULT_PRIVATE_KEY).unwrap(); + let owner_wallet = + WalletUnlocked::new_from_private_key(owner_secret_key, Some(provider.clone())); + let base_asset_id = provider.base_asset_id(); + + // Fund attacker wallet so that it can try to make a set proxy target call. + owner_wallet + .transfer( + attacker_wallet.address(), + 100000, + *base_asset_id, + TxPolicies::default(), + ) + .await + .unwrap(); + + let dummy_contract_id_target = ContractId::default(); + abigen!(Contract( + name = "ProxyContract", + abi = "forc-plugins/forc-client/proxy_abi/proxy_contract-abi.json" + )); + + // Try to change target of the proxy with a random wallet which is not the owner of the proxy. + let res = update_proxy_contract_target( + &provider, + attacker_secret_key, + proxy_id, + dummy_contract_id_target, + ) + .await + .err() + .unwrap(); + + node.kill().unwrap(); + assert!(res + .to_string() + .starts_with("transaction reverted: NotOwner")); +} + // TODO: https://github.com/FuelLabs/sway/issues/6283 // Add interactive tests for the happy path cases. This requires starting the node with funded accounts and setting up // the wallet with the correct password. The tests should be run in a separate test suite that is not run by default.