From 190f849f0f7d3b2748140b8db5ea18690444a543 Mon Sep 17 00:00:00 2001 From: SkymanOne Date: Thu, 24 Aug 2023 21:05:05 +0100 Subject: [PATCH 1/8] add basic example --- .../delegator/.gitignore | 9 + .../delegator/Cargo.toml | 29 +++ .../delegator/delegatee/.gitignore | 9 + .../delegator/delegatee/Cargo.toml | 25 +++ .../delegator/delegatee/lib.rs | 37 ++++ .../upgradeable-contracts/delegator/lib.rs | 202 ++++++++++++++++++ .../flipper copy/.gitignore | 9 + .../flipper copy/Cargo.toml | 28 +++ .../upgradeable-contracts/flipper copy/lib.rs | 111 ++++++++++ 9 files changed, 459 insertions(+) create mode 100644 integration-tests/upgradeable-contracts/delegator/.gitignore create mode 100644 integration-tests/upgradeable-contracts/delegator/Cargo.toml create mode 100644 integration-tests/upgradeable-contracts/delegator/delegatee/.gitignore create mode 100644 integration-tests/upgradeable-contracts/delegator/delegatee/Cargo.toml create mode 100644 integration-tests/upgradeable-contracts/delegator/delegatee/lib.rs create mode 100644 integration-tests/upgradeable-contracts/delegator/lib.rs create mode 100644 integration-tests/upgradeable-contracts/flipper copy/.gitignore create mode 100644 integration-tests/upgradeable-contracts/flipper copy/Cargo.toml create mode 100644 integration-tests/upgradeable-contracts/flipper copy/lib.rs diff --git a/integration-tests/upgradeable-contracts/delegator/.gitignore b/integration-tests/upgradeable-contracts/delegator/.gitignore new file mode 100644 index 00000000000..bf910de10af --- /dev/null +++ b/integration-tests/upgradeable-contracts/delegator/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock \ No newline at end of file diff --git a/integration-tests/upgradeable-contracts/delegator/Cargo.toml b/integration-tests/upgradeable-contracts/delegator/Cargo.toml new file mode 100644 index 00000000000..13a08a2cb5f --- /dev/null +++ b/integration-tests/upgradeable-contracts/delegator/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "delegator" +version = "4.2.0" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../../crates/ink", default-features = false } +delegatee = { path = "delegatee", default-features = false, features = ["ink-as-dependency"] } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } + +[dev-dependencies] +ink_e2e = { path = "../../../crates/e2e" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/integration-tests/upgradeable-contracts/delegator/delegatee/.gitignore b/integration-tests/upgradeable-contracts/delegator/delegatee/.gitignore new file mode 100644 index 00000000000..bf910de10af --- /dev/null +++ b/integration-tests/upgradeable-contracts/delegator/delegatee/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock \ No newline at end of file diff --git a/integration-tests/upgradeable-contracts/delegator/delegatee/Cargo.toml b/integration-tests/upgradeable-contracts/delegator/delegatee/Cargo.toml new file mode 100644 index 00000000000..1cb333c554b --- /dev/null +++ b/integration-tests/upgradeable-contracts/delegator/delegatee/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "delegatee" +version = "4.2.0" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../../../crates/ink", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/integration-tests/upgradeable-contracts/delegator/delegatee/lib.rs b/integration-tests/upgradeable-contracts/delegator/delegatee/lib.rs new file mode 100644 index 00000000000..3d8bb050d14 --- /dev/null +++ b/integration-tests/upgradeable-contracts/delegator/delegatee/lib.rs @@ -0,0 +1,37 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +pub mod delegatee { + use ink::storage::{Mapping, traits::ManualKey}; + #[ink(storage)] + pub struct Delegatee { + addresses: Mapping>, + counter: i32, + } + + impl Delegatee { + /// Creates a new delegator smart contract initialized with the given value. + #[ink(constructor)] + pub fn new(init_value: i32) -> Self { + let v = Mapping::new(); + Self { + addresses: v, + counter: init_value, + } + } + + /// Increments the current value. + #[ink(message)] + pub fn inc(&mut self) { + self.counter += 2; + } + + + /// Adds current value of counter to the `addresses` + #[ink(message)] + pub fn append_address_value(&mut self) { + let caller = self.env().caller(); + self.addresses.insert(caller, &self.counter); + } + } +} diff --git a/integration-tests/upgradeable-contracts/delegator/lib.rs b/integration-tests/upgradeable-contracts/delegator/lib.rs new file mode 100644 index 00000000000..a9697c5af91 --- /dev/null +++ b/integration-tests/upgradeable-contracts/delegator/lib.rs @@ -0,0 +1,202 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +pub mod delegator { + use ink::{ + env::CallFlags, + storage::Mapping, + }; + + use ink::{ + env::{ + call::{ + build_call, + ExecutionInput, + Selector, + }, + DefaultEnvironment, + }, + storage::traits::ManualKey, + }; + + #[ink(storage)] + pub struct Delegator { + addresses: Mapping>, + counter: i32, + } + + impl Delegator { + /// Creates a new delegator smart contract initialized with the given value. + #[ink(constructor)] + pub fn new(init_value: i32) -> Self { + let v = Mapping::new(); + Self { + addresses: v, + counter: init_value, + } + } + + /// Creates a new contract with default values. + #[ink(constructor)] + pub fn new_default() -> Self { + Self::new(Default::default()) + } + + /// Increment the current value using delegate call. + #[ink(message)] + pub fn inc_delegate(&mut self, hash: Hash) { + let selector = ink::selector_bytes!("inc"); + let _ = build_call::() + .delegate(hash) + // We specify `set_tail_call(true)` to use the delegatee last memory frame + // as the end of the execution cycle. + // So any mutations to `Packed` types, made by delegatee, + // will be flushed to storage. + // + // If we don't specify this flag. + // The storage state before the delegate call will be flushed to storage instead. + // See https://substrate.stackexchange.com/questions/3336/i-found-set-allow-reentry-may-have-some-problems/3352#3352 + .call_flags(CallFlags::default().set_tail_call(true)) + .exec_input(ExecutionInput::new(Selector::new(selector))) + .returns::<()>() + .try_invoke(); + } + + /// Adds entry to `addresses` using delegate call. + /// Note that we don't need `set_tail_call(true)` flag + /// because `Mapping` updates the storage instantly on-demand. + #[ink(message)] + pub fn add_entry_delegate(&mut self, hash: Hash) { + let selector = ink::selector_bytes!("append_address_value"); + let _ = build_call::() + .delegate(hash) + .exec_input(ExecutionInput::new(Selector::new(selector))) + .returns::<()>() + .try_invoke(); + } + + /// Returns the current value of the counter. + #[ink(message)] + pub fn get_counter(&self) -> i32 { + self.counter + } + + /// Returns the current value of the address. + #[ink(message)] + pub fn get_value(&self, address: AccountId) -> (AccountId, Option) { + (self.env().caller(), self.addresses.get(address)) + } + } + + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + use super::*; + use ink_e2e::{ + ChainBackend, + ContractsBackend, + }; + + type E2EResult = std::result::Result>; + + #[ink_e2e::test] + async fn e2e_counter_mutated( + mut client: Client, + ) -> E2EResult<()> { + // given + let origin = client + .create_and_fund_account(&ink_e2e::alice(), 10_000_000_000_000) + .await; + + let constructor = DelegatorRef::new_default(); + let call_builder = client + .instantiate("delegator", &origin, constructor, 0, None) + .await + .expect("instantiate failed"); + let mut call_builder_call = call_builder.call::(); + + let code_hash = client + .upload("delegatee", &origin, None) + .await + .expect("upload `delegatee` failed") + .code_hash; + + // when + let call_delegate = call_builder_call.inc_delegate(code_hash); + + let result = client.call(&origin, &call_delegate, 0, None).await; + assert!(result.is_ok(), "delegate call failed."); + + let result = client.call(&origin, &call_delegate, 0, None).await; + assert!(result.is_ok(), "second delegate call failed."); + + // then + let expected_value = 4; + let call = call_builder.call::(); + + let call_get = call.get_counter(); + let call_get_result = client + .call_dry_run(&origin, &call_get, 0, None) + .await + .return_value(); + + // This fails + assert_eq!( + call_get_result, expected_value, + "Decoded an unexpected value from the call." + ); + + Ok(()) + } + + #[ink_e2e::test] + async fn e2e_mapping_mutated( + mut client: Client, + ) -> E2EResult<()> { + let origin = client + .create_and_fund_account(&ink_e2e::alice(), 10_000_000_000_000) + .await; + + // given + let constructor = DelegatorRef::new(10); + let call_builder = client + .instantiate("delegator", &origin, constructor, 0, None) + .await + .expect("instantiate failed"); + let mut call_builder_call = call_builder.call::(); + + let code_hash = client + .upload("delegatee", &origin, None) + .await + .expect("upload `delegatee` failed") + .code_hash; + + // when + let call_delegate = call_builder_call.add_entry_delegate(code_hash); + let result = client.call(&origin, &call_delegate, 0, None).await; + assert!(result.is_ok(), "delegate call failed."); + + // then + + // because we initialize the counter with `10` we expect this value be + // assigned to Alice. + let expected_value = 10; + // Alice's address + let address = AccountId::from(origin.public_key().to_account_id().0); + + let call_get_value = call_builder_call.get_value(address); + let call_get_result = client + .call(&origin, &call_get_value, 0, None) + .await + .unwrap() + .return_value(); + + assert_eq!( + call_get_result, + (address, Some(expected_value)), + "Decoded an unexpected value from the call." + ); + + Ok(()) + } + } +} diff --git a/integration-tests/upgradeable-contracts/flipper copy/.gitignore b/integration-tests/upgradeable-contracts/flipper copy/.gitignore new file mode 100644 index 00000000000..bf910de10af --- /dev/null +++ b/integration-tests/upgradeable-contracts/flipper copy/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock \ No newline at end of file diff --git a/integration-tests/upgradeable-contracts/flipper copy/Cargo.toml b/integration-tests/upgradeable-contracts/flipper copy/Cargo.toml new file mode 100644 index 00000000000..a9416e6f50d --- /dev/null +++ b/integration-tests/upgradeable-contracts/flipper copy/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "flipper" +version = "4.2.0" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../crates/ink", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } + +[dev-dependencies] +ink_e2e = { path = "../../crates/e2e" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/integration-tests/upgradeable-contracts/flipper copy/lib.rs b/integration-tests/upgradeable-contracts/flipper copy/lib.rs new file mode 100644 index 00000000000..79fc8a8a83a --- /dev/null +++ b/integration-tests/upgradeable-contracts/flipper copy/lib.rs @@ -0,0 +1,111 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +pub mod flipper { + #[ink(storage)] + pub struct Flipper { + value: bool, + } + + impl Flipper { + /// Creates a new flipper smart contract initialized with the given value. + #[ink(constructor)] + pub fn new(init_value: bool) -> Self { + Self { value: init_value } + } + + /// Creates a new flipper smart contract initialized to `false`. + #[ink(constructor)] + pub fn new_default() -> Self { + Self::new(Default::default()) + } + + /// Flips the current value of the Flipper's boolean. + #[ink(message)] + pub fn flip(&mut self) { + self.value = !self.value; + } + + /// Returns the current value of the Flipper's boolean. + #[ink(message)] + pub fn get(&self) -> bool { + self.value + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[ink::test] + fn default_works() { + let flipper = Flipper::new_default(); + assert!(!flipper.get()); + } + + #[ink::test] + fn it_works() { + let mut flipper = Flipper::new(false); + assert!(!flipper.get()); + flipper.flip(); + assert!(flipper.get()); + } + } + + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + use super::*; + use ink_e2e::ContractsBackend; + + type E2EResult = std::result::Result>; + + #[ink_e2e::test] + async fn it_works(mut client: Client) -> E2EResult<()> { + // given + let constructor = FlipperRef::new(false); + let contract = client + .instantiate("flipper", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed"); + let mut call = contract.call::(); + + let get = call.get(); + let get_res = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await; + assert!(matches!(get_res.return_value(), false)); + + // when + let flip = call.flip(); + let _flip_res = client + .call(&ink_e2e::bob(), &flip, 0, None) + .await + .expect("flip failed"); + + // then + let get = call.get(); + let get_res = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await; + assert!(matches!(get_res.return_value(), true)); + + Ok(()) + } + + #[ink_e2e::test] + async fn default_works(mut client: Client) -> E2EResult<()> { + // given + let constructor = FlipperRef::new_default(); + + // when + let contract = client + .instantiate("flipper", &ink_e2e::bob(), constructor, 0, None) + .await + .expect("instantiate failed"); + let call = contract.call::(); + + // then + let get = call.get(); + let get_res = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await; + assert!(matches!(get_res.return_value(), false)); + + Ok(()) + } + } +} From 8a21dc67ba516ea7194db44f44b36042e5598d0c Mon Sep 17 00:00:00 2001 From: SkymanOne Date: Thu, 24 Aug 2023 21:35:13 +0100 Subject: [PATCH 2/8] add docs and readme --- .gitlab-ci.yml | 23 +++- .../upgradeable-contracts/README.md | 11 ++ .../delegator/delegatee/lib.rs | 18 +-- .../flipper copy/.gitignore | 9 -- .../flipper copy/Cargo.toml | 28 ----- .../upgradeable-contracts/flipper copy/lib.rs | 111 ------------------ .../set-code-hash/Cargo.toml | 4 +- .../set-code-hash/lib.rs | 0 .../updated-incrementer/Cargo.toml | 2 +- .../set-code-hash/updated-incrementer/lib.rs | 0 10 files changed, 44 insertions(+), 162 deletions(-) create mode 100644 integration-tests/upgradeable-contracts/README.md delete mode 100644 integration-tests/upgradeable-contracts/flipper copy/.gitignore delete mode 100644 integration-tests/upgradeable-contracts/flipper copy/Cargo.toml delete mode 100644 integration-tests/upgradeable-contracts/flipper copy/lib.rs rename integration-tests/{ => upgradeable-contracts}/set-code-hash/Cargo.toml (83%) rename integration-tests/{ => upgradeable-contracts}/set-code-hash/lib.rs (100%) rename integration-tests/{ => upgradeable-contracts}/set-code-hash/updated-incrementer/Cargo.toml (88%) rename integration-tests/{ => upgradeable-contracts}/set-code-hash/updated-incrementer/lib.rs (100%) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cda00fae49f..1c2ccc12f72 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -32,6 +32,7 @@ variables: ALL_CRATES: "${PURELY_STD_CRATES} ${ALSO_WASM_CRATES}" MULTI_CONTRACT_CALLER_SUBCONTRACTS: "accumulator adder subber" LANG_ERR_INTEGRATION_CONTRACTS: "integration-flipper call-builder contract-ref constructors-return-value" + UPGRADEABLE_CONTRACTS: "delegator set-code-hash" # TODO `cargo clippy --verbose --all-targets --all-features` for this crate # currently fails on `stable`, but succeeds on `nightly`. This is due to # this fix not yet in stable: https://github.com/rust-lang/rust-clippy/issues/8895. @@ -125,10 +126,12 @@ examples-fmt: - for contract in ${MULTI_CONTRACT_CALLER_SUBCONTRACTS}; do cargo +nightly fmt --verbose --manifest-path ./integration-tests/multi-contract-caller/${contract}/Cargo.toml -- --check; done + - for contract in ${UPGRADEABLE_CONTRACTS}; do + cargo +nightly fmt --verbose --manifest-path ./integration-tests/upgradeable-contracts/${contract}/Cargo.toml -- --check; + done - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do cargo +nightly fmt --verbose --manifest-path ./integration-tests/lang-err-integration-tests/${contract}/Cargo.toml -- --check; done - - cargo +nightly fmt --verbose --manifest-path ./integration-tests/set-code-hash/updated-incrementer/Cargo.toml -- --check # This file is not a part of the cargo project, so it wouldn't be formatted the usual way - rustfmt +nightly --verbose --check ./integration-tests/psp22-extension/runtime/psp22-extension-example.rs allow_failure: true @@ -164,10 +167,12 @@ examples-clippy-std: - for contract in ${MULTI_CONTRACT_CALLER_SUBCONTRACTS}; do cargo clippy --verbose --all-targets --manifest-path ./integration-tests/multi-contract-caller/${contract}/Cargo.toml -- -D warnings -A $CLIPPY_ALLOWED; done + - for contract in ${UPGRADEABLE_CONTRACTS}; do + cargo clippy --verbose --manifest-path ./integration-tests/upgradeable-contracts/${contract}/Cargo.toml -- -D warnings -A $CLIPPY_ALLOWED; + done - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do cargo clippy --verbose --all-targets --manifest-path ./integration-tests/lang-err-integration-tests/${contract}/Cargo.toml -- -D warnings -A $CLIPPY_ALLOWED; done - - cargo clippy --verbose --all-targets --manifest-path ./integration-tests/set-code-hash/updated-incrementer/Cargo.toml -- -D warnings -A $CLIPPY_ALLOWED; allow_failure: true examples-clippy-wasm: @@ -182,6 +187,9 @@ examples-clippy-wasm: - for contract in ${MULTI_CONTRACT_CALLER_SUBCONTRACTS}; do cargo clippy --verbose --manifest-path ./integration-tests/multi-contract-caller/${contract}/Cargo.toml --no-default-features --target wasm32-unknown-unknown -- -D warnings -A $CLIPPY_ALLOWED; done + - for contract in ${UPGRADEABLE_CONTRACTS}; do + cargo clippy --verbose --manifest-path ./integration-tests/upgradeable-contracts/${contract}/Cargo.toml --no-default-features --target wasm32-unknown-unknown -- -D warnings -A $CLIPPY_ALLOWED; + done - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do cargo clippy --verbose --manifest-path ./integration-tests/lang-err-integration-tests/${contract}/Cargo.toml --no-default-features --target wasm32-unknown-unknown -- -D warnings -A $CLIPPY_ALLOWED; done @@ -396,13 +404,15 @@ examples-test: - for contract in ${MULTI_CONTRACT_CALLER_SUBCONTRACTS}; do cargo test --verbose --manifest-path ./integration-tests/multi-contract-caller/${contract}/Cargo.toml; done + - for contract in ${UPGRADEABLE_CONTRACTS}; do + cargo test --verbose --manifest-path ./integration-tests/upgradeable-contracts/${contract}/Cargo.toml --features e2e-tests; + done # TODO (#1502): We need to clean before running, otherwise the CI fails with a # linking error. - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do cargo clean --verbose --manifest-path ./integration-tests/lang-err-integration-tests/${contract}/Cargo.toml; cargo test --verbose --manifest-path ./integration-tests/lang-err-integration-tests/${contract}/Cargo.toml --features e2e-tests; done - - cargo test --verbose --manifest-path ./integration-tests/set-code-hash/updated-incrementer/Cargo.toml; examples-contract-build: stage: examples @@ -435,7 +445,9 @@ examples-contract-build: - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do cargo contract build --manifest-path ./integration-tests/lang-err-integration-tests/${contract}/Cargo.toml; done - - cargo contract build --manifest-path ./integration-tests/set-code-hash/updated-incrementer/Cargo.toml + - for contract in ${UPGRADEABLE_CONTRACTS}; do + cargo contract build --manifest-path ./integration-tests/upgradeable-contracts/${contract}/Cargo.toml; + done # TODO: Use cargo contract as soon as it has RISC-V support examples-contract-build-riscv: @@ -483,6 +495,9 @@ examples-docs: - for contract in ${MULTI_CONTRACT_CALLER_SUBCONTRACTS}; do cargo doc --manifest-path ./integration-tests/multi-contract-caller/${contract}/Cargo.toml --document-private-items --verbose --no-deps; done + - for contract in ${UPGRADEABLE_CONTRACTS}; do + cargo doc --manifest-path ./integration-tests/upgradeable-contracts/${contract}/Cargo.toml --document-private-items --verbose --no-deps; + done - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do cargo doc --manifest-path ./integration-tests/lang-err-integration-tests/${contract}/Cargo.toml --document-private-items --verbose --no-deps; done diff --git a/integration-tests/upgradeable-contracts/README.md b/integration-tests/upgradeable-contracts/README.md new file mode 100644 index 00000000000..fa8bfcef784 --- /dev/null +++ b/integration-tests/upgradeable-contracts/README.md @@ -0,0 +1,11 @@ +# Upgradeable Contracts + +There are different ways a contract can be upgraded in ink! + +This folder illustrates some of the common and best practises to achieve upgradeability in your contracts. + +## [`set-code-hash`](/set-code-hash/) + +TODO + +## [Delegator](/delegator/) diff --git a/integration-tests/upgradeable-contracts/delegator/delegatee/lib.rs b/integration-tests/upgradeable-contracts/delegator/delegatee/lib.rs index 3d8bb050d14..9e64e75cbb2 100644 --- a/integration-tests/upgradeable-contracts/delegator/delegatee/lib.rs +++ b/integration-tests/upgradeable-contracts/delegator/delegatee/lib.rs @@ -7,17 +7,21 @@ pub mod delegatee { pub struct Delegatee { addresses: Mapping>, counter: i32, + // Uncommenting below line will break storage compatibility. + // flag: bool, } impl Delegatee { - /// Creates a new delegator smart contract initialized with the given value. + /// When using the delegate call. You only upload the code of the delegatee contract. + /// However, the code and storage do not get initialized. + /// + /// Because of this. The constructor actually never gets called. + #[allow(clippy::new_without_default)] #[ink(constructor)] - pub fn new(init_value: i32) -> Self { - let v = Mapping::new(); - Self { - addresses: v, - counter: init_value, - } + pub fn new() -> Self { + unreachable!( + "Constructors are not called when upgrading using `set_code_hash`." + ) } /// Increments the current value. diff --git a/integration-tests/upgradeable-contracts/flipper copy/.gitignore b/integration-tests/upgradeable-contracts/flipper copy/.gitignore deleted file mode 100644 index bf910de10af..00000000000 --- a/integration-tests/upgradeable-contracts/flipper copy/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -# Ignore build artifacts from the local tests sub-crate. -/target/ - -# Ignore backup files creates by cargo fmt. -**/*.rs.bk - -# Remove Cargo.lock when creating an executable, leave it for libraries -# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock -Cargo.lock \ No newline at end of file diff --git a/integration-tests/upgradeable-contracts/flipper copy/Cargo.toml b/integration-tests/upgradeable-contracts/flipper copy/Cargo.toml deleted file mode 100644 index a9416e6f50d..00000000000 --- a/integration-tests/upgradeable-contracts/flipper copy/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "flipper" -version = "4.2.0" -authors = ["Parity Technologies "] -edition = "2021" -publish = false - -[dependencies] -ink = { path = "../../crates/ink", default-features = false } - -scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } -scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } - -[dev-dependencies] -ink_e2e = { path = "../../crates/e2e" } - -[lib] -path = "lib.rs" - -[features] -default = ["std"] -std = [ - "ink/std", - "scale/std", - "scale-info/std", -] -ink-as-dependency = [] -e2e-tests = [] diff --git a/integration-tests/upgradeable-contracts/flipper copy/lib.rs b/integration-tests/upgradeable-contracts/flipper copy/lib.rs deleted file mode 100644 index 79fc8a8a83a..00000000000 --- a/integration-tests/upgradeable-contracts/flipper copy/lib.rs +++ /dev/null @@ -1,111 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::contract] -pub mod flipper { - #[ink(storage)] - pub struct Flipper { - value: bool, - } - - impl Flipper { - /// Creates a new flipper smart contract initialized with the given value. - #[ink(constructor)] - pub fn new(init_value: bool) -> Self { - Self { value: init_value } - } - - /// Creates a new flipper smart contract initialized to `false`. - #[ink(constructor)] - pub fn new_default() -> Self { - Self::new(Default::default()) - } - - /// Flips the current value of the Flipper's boolean. - #[ink(message)] - pub fn flip(&mut self) { - self.value = !self.value; - } - - /// Returns the current value of the Flipper's boolean. - #[ink(message)] - pub fn get(&self) -> bool { - self.value - } - } - - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn default_works() { - let flipper = Flipper::new_default(); - assert!(!flipper.get()); - } - - #[ink::test] - fn it_works() { - let mut flipper = Flipper::new(false); - assert!(!flipper.get()); - flipper.flip(); - assert!(flipper.get()); - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink_e2e::ContractsBackend; - - type E2EResult = std::result::Result>; - - #[ink_e2e::test] - async fn it_works(mut client: Client) -> E2EResult<()> { - // given - let constructor = FlipperRef::new(false); - let contract = client - .instantiate("flipper", &ink_e2e::alice(), constructor, 0, None) - .await - .expect("instantiate failed"); - let mut call = contract.call::(); - - let get = call.get(); - let get_res = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await; - assert!(matches!(get_res.return_value(), false)); - - // when - let flip = call.flip(); - let _flip_res = client - .call(&ink_e2e::bob(), &flip, 0, None) - .await - .expect("flip failed"); - - // then - let get = call.get(); - let get_res = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await; - assert!(matches!(get_res.return_value(), true)); - - Ok(()) - } - - #[ink_e2e::test] - async fn default_works(mut client: Client) -> E2EResult<()> { - // given - let constructor = FlipperRef::new_default(); - - // when - let contract = client - .instantiate("flipper", &ink_e2e::bob(), constructor, 0, None) - .await - .expect("instantiate failed"); - let call = contract.call::(); - - // then - let get = call.get(); - let get_res = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await; - assert!(matches!(get_res.return_value(), false)); - - Ok(()) - } - } -} diff --git a/integration-tests/set-code-hash/Cargo.toml b/integration-tests/upgradeable-contracts/set-code-hash/Cargo.toml similarity index 83% rename from integration-tests/set-code-hash/Cargo.toml rename to integration-tests/upgradeable-contracts/set-code-hash/Cargo.toml index 86693ae0310..69dbeb43834 100644 --- a/integration-tests/set-code-hash/Cargo.toml +++ b/integration-tests/upgradeable-contracts/set-code-hash/Cargo.toml @@ -6,13 +6,13 @@ edition = "2021" publish = false [dependencies] -ink = { path = "../../crates/ink", default-features = false } +ink = { path = "../../../crates/ink", default-features = false } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } [dev-dependencies] -ink_e2e = { path = "../../crates/e2e" } +ink_e2e = { path = "../../../crates/e2e" } [lib] path = "lib.rs" diff --git a/integration-tests/set-code-hash/lib.rs b/integration-tests/upgradeable-contracts/set-code-hash/lib.rs similarity index 100% rename from integration-tests/set-code-hash/lib.rs rename to integration-tests/upgradeable-contracts/set-code-hash/lib.rs diff --git a/integration-tests/set-code-hash/updated-incrementer/Cargo.toml b/integration-tests/upgradeable-contracts/set-code-hash/updated-incrementer/Cargo.toml similarity index 88% rename from integration-tests/set-code-hash/updated-incrementer/Cargo.toml rename to integration-tests/upgradeable-contracts/set-code-hash/updated-incrementer/Cargo.toml index 3c9264cb7ba..5e73ea399cf 100644 --- a/integration-tests/set-code-hash/updated-incrementer/Cargo.toml +++ b/integration-tests/upgradeable-contracts/set-code-hash/updated-incrementer/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" publish = false [dependencies] -ink = { path = "../../../crates/ink", default-features = false } +ink = { path = "../../../../crates/ink", default-features = false } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } diff --git a/integration-tests/set-code-hash/updated-incrementer/lib.rs b/integration-tests/upgradeable-contracts/set-code-hash/updated-incrementer/lib.rs similarity index 100% rename from integration-tests/set-code-hash/updated-incrementer/lib.rs rename to integration-tests/upgradeable-contracts/set-code-hash/updated-incrementer/lib.rs From 7500a63f55c74f292af08fcfbd9fb4ae2617a452 Mon Sep 17 00:00:00 2001 From: SkymanOne Date: Thu, 24 Aug 2023 21:38:45 +0100 Subject: [PATCH 3/8] update readme --- integration-tests/upgradeable-contracts/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/upgradeable-contracts/README.md b/integration-tests/upgradeable-contracts/README.md index fa8bfcef784..0b754afa81d 100644 --- a/integration-tests/upgradeable-contracts/README.md +++ b/integration-tests/upgradeable-contracts/README.md @@ -4,8 +4,8 @@ There are different ways a contract can be upgraded in ink! This folder illustrates some of the common and best practises to achieve upgradeability in your contracts. -## [`set-code-hash`](/set-code-hash/) +## [`set-code-hash`](set-code-hash/) TODO -## [Delegator](/delegator/) +## [Delegator](delegator/) From 2f9485ec97aabeed7a9dcae2393bb1109fd0a635 Mon Sep 17 00:00:00 2001 From: SkymanOne Date: Fri, 25 Aug 2023 10:25:40 +0100 Subject: [PATCH 4/8] fix ci --- .config/cargo_spellcheck.dic | 2 ++ .gitlab-ci.yml | 6 ++++++ integration-tests/upgradeable-contracts/README.md | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.config/cargo_spellcheck.dic b/.config/cargo_spellcheck.dic index d3ca87cef7a..87286faf5ed 100644 --- a/.config/cargo_spellcheck.dic +++ b/.config/cargo_spellcheck.dic @@ -38,6 +38,8 @@ decodable decrement defrag defragmentation +delegatee +delegator deploy dereferencing deserialize/S diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1c2ccc12f72..02dcea1d23f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -121,6 +121,7 @@ examples-fmt: # Note that we disable the license header check for the examples, since they are unlicensed. - for example in integration-tests/*/; do if [ "$example" = "integration-tests/lang-err-integration-tests/" ]; then continue; fi; + if [ "$example" = "integration-tests/upgradeable-contracts/" ]; then continue; fi; cargo +nightly fmt --verbose --manifest-path ${example}/Cargo.toml -- --check; done - for contract in ${MULTI_CONTRACT_CALLER_SUBCONTRACTS}; do @@ -162,6 +163,7 @@ examples-clippy-std: script: - for example in integration-tests/*/; do if [ "$example" = "integration-tests/lang-err-integration-tests/" ]; then continue; fi; + if [ "$example" = "integration-tests/upgradeable-contracts/" ]; then continue; fi; cargo clippy --verbose --all-targets --manifest-path ${example}/Cargo.toml -- -D warnings -A $CLIPPY_ALLOWED; done - for contract in ${MULTI_CONTRACT_CALLER_SUBCONTRACTS}; do @@ -182,6 +184,7 @@ examples-clippy-wasm: script: - for example in integration-tests/*/; do if [ "$example" = "integration-tests/lang-err-integration-tests/" ]; then continue; fi; + if [ "$example" = "integration-tests/upgradeable-contracts/" ]; then continue; fi; cargo clippy --verbose --manifest-path ${example}/Cargo.toml --no-default-features --target wasm32-unknown-unknown -- -D warnings -A $CLIPPY_ALLOWED; done - for contract in ${MULTI_CONTRACT_CALLER_SUBCONTRACTS}; do @@ -386,6 +389,7 @@ examples-test: script: - for example in integration-tests/*/; do if [ "$example" = "integration-tests/lang-err-integration-tests/" ]; then continue; fi; + if [ "$example" = "integration-tests/upgradeable-contracts/" ]; then continue; fi; if [ "$example" = "integration-tests/conditional-compilation/" ]; then cargo test --verbose --manifest-path ${example}/Cargo.toml --features "foo"; cargo test --verbose --manifest-path ${example}/Cargo.toml --features "bar"; @@ -426,6 +430,7 @@ examples-contract-build: - cargo contract -V - for example in integration-tests/*/; do if [ "$example" = "integration-tests/lang-err-integration-tests/" ]; then continue; fi; + if [ "$example" = "integration-tests/upgradeable-contracts/" ]; then continue; fi; if [ "$example" = "integration-tests/conditional-compilation/" ]; then pushd $example && cargo contract build --features "foo" && @@ -490,6 +495,7 @@ examples-docs: # of this flag. - for example in integration-tests/*/; do if [ "$example" = "integration-tests/lang-err-integration-tests/" ]; then continue; fi; + if [ "$example" = "integration-tests/upgradeable-contracts/" ]; then continue; fi; cargo doc --manifest-path ${example}/Cargo.toml --document-private-items --verbose --no-deps; done - for contract in ${MULTI_CONTRACT_CALLER_SUBCONTRACTS}; do diff --git a/integration-tests/upgradeable-contracts/README.md b/integration-tests/upgradeable-contracts/README.md index 0b754afa81d..74d6f02ed4d 100644 --- a/integration-tests/upgradeable-contracts/README.md +++ b/integration-tests/upgradeable-contracts/README.md @@ -2,7 +2,7 @@ There are different ways a contract can be upgraded in ink! -This folder illustrates some of the common and best practises to achieve upgradeability in your contracts. +This folder illustrates some of the common and best practices to achieve upgradeability in your contracts. ## [`set-code-hash`](set-code-hash/) From 05dad8e46ed09a1ea85bc48a3e6bac583b788e75 Mon Sep 17 00:00:00 2001 From: SkymanOne Date: Fri, 25 Aug 2023 11:08:26 +0100 Subject: [PATCH 5/8] update paths in ci --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 02dcea1d23f..72ddd5274a3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -196,7 +196,7 @@ examples-clippy-wasm: - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do cargo clippy --verbose --manifest-path ./integration-tests/lang-err-integration-tests/${contract}/Cargo.toml --no-default-features --target wasm32-unknown-unknown -- -D warnings -A $CLIPPY_ALLOWED; done - - cargo clippy --verbose --manifest-path ./integration-tests/set-code-hash/updated-incrementer/Cargo.toml --no-default-features --target wasm32-unknown-unknown -- -D warnings -A $CLIPPY_ALLOWED; + - cargo clippy --verbose --manifest-path ./integration-tests/set-code-hash/upgradeable-contracts/updated-incrementer/Cargo.toml --no-default-features --target wasm32-unknown-unknown -- -D warnings -A $CLIPPY_ALLOWED; allow_failure: true @@ -507,7 +507,7 @@ examples-docs: - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do cargo doc --manifest-path ./integration-tests/lang-err-integration-tests/${contract}/Cargo.toml --document-private-items --verbose --no-deps; done - - cargo doc --manifest-path ./integration-tests/set-code-hash/updated-incrementer/Cargo.toml --document-private-items --verbose --no-deps + - cargo doc --manifest-path ./integration-tests/set-code-hash/upgradeable-contracts/updated-incrementer/Cargo.toml --document-private-items --verbose --no-deps #### stage: ink-waterfall From 60d08993c5ff2665c52c44bac30283ec2bef3a13 Mon Sep 17 00:00:00 2001 From: SkymanOne Date: Fri, 25 Aug 2023 12:03:43 +0100 Subject: [PATCH 6/8] fix path --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 72ddd5274a3..d31c9290b91 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -196,7 +196,7 @@ examples-clippy-wasm: - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do cargo clippy --verbose --manifest-path ./integration-tests/lang-err-integration-tests/${contract}/Cargo.toml --no-default-features --target wasm32-unknown-unknown -- -D warnings -A $CLIPPY_ALLOWED; done - - cargo clippy --verbose --manifest-path ./integration-tests/set-code-hash/upgradeable-contracts/updated-incrementer/Cargo.toml --no-default-features --target wasm32-unknown-unknown -- -D warnings -A $CLIPPY_ALLOWED; + - cargo clippy --verbose --manifest-path ./integration-tests/upgradeable-contracts/set-code-hash/updated-incrementer/Cargo.toml --no-default-features --target wasm32-unknown-unknown -- -D warnings -A $CLIPPY_ALLOWED; allow_failure: true @@ -507,7 +507,7 @@ examples-docs: - for contract in ${LANG_ERR_INTEGRATION_CONTRACTS}; do cargo doc --manifest-path ./integration-tests/lang-err-integration-tests/${contract}/Cargo.toml --document-private-items --verbose --no-deps; done - - cargo doc --manifest-path ./integration-tests/set-code-hash/upgradeable-contracts/updated-incrementer/Cargo.toml --document-private-items --verbose --no-deps + - cargo doc --manifest-path ./integration-tests/upgradeable-contracts/set-code-hash/updated-incrementer/Cargo.toml --document-private-items --verbose --no-deps #### stage: ink-waterfall From fd7a6563f5c238b1e35d3e206c881053825fe420 Mon Sep 17 00:00:00 2001 From: SkymanOne Date: Fri, 25 Aug 2023 20:33:26 +0100 Subject: [PATCH 7/8] add readme --- .../upgradeable-contracts/README.md | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/integration-tests/upgradeable-contracts/README.md b/integration-tests/upgradeable-contracts/README.md index 74d6f02ed4d..5f7f3d351ff 100644 --- a/integration-tests/upgradeable-contracts/README.md +++ b/integration-tests/upgradeable-contracts/README.md @@ -6,6 +6,31 @@ This folder illustrates some of the common and best practices to achieve upgrade ## [`set-code-hash`](set-code-hash/) -TODO +ink! provides an ability to replace the code under the given contract's address. +This is exactly what `set_code_hash()` function does. + +However, developers needs to be mindful of storage compatibility. +You can read more about storage compatibility on [use.ink](https://use.ink/basics/upgradeable-contracts#replacing-contract-code-with-set_code_hash) ## [Delegator](delegator/) + +Delegator patter is based around a low level cross contract call function `delegate_call`. +It allows a contract to delegate its execution to some on-chain uploaded code. + +It is different from a traditional cross-contract call +because the call is delegate to the **code**, not the contract. + +Similarly, the storage compatibility issue is also applicable here. +However, there are certain nuances associated with using `delegate_call`. + +First of all, as demonstrated in the example, if the delegated code intends to mutate the caller's storage, +a developer needs to be mindful. If the delegated code modifies layoutful storage +(i.e. it contains at least non-`Lazy`, non-`Mapping` field), the `.set_tail_call(true)` flag of `CallFlags` needs to be specified and the storage layouts must match. +This is due to the way ink! execution call stack is operated +(see [StackExchange Answer](https://substrate.stackexchange.com/a/3352/3098) for more explanation). + +If the delegated code only modifies `Lazy` or `Mapping` field, the keys must be identical and `.set_tail_call(true)` is optional. +This is because `Lazy` and `Mapping` interact with the storage directly instead of loading and flushing storage states. + +If your storage is completely layoutless (it only contains `Lazy` and `Mapping` fields), the order of fields and layout do not need to match for the same reason as mentioned above. + From 3e674f78f4c519291be29ecb260cc0e73c08828e Mon Sep 17 00:00:00 2001 From: SkymanOne Date: Mon, 28 Aug 2023 13:16:41 +0100 Subject: [PATCH 8/8] fix spelling --- integration-tests/upgradeable-contracts/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/upgradeable-contracts/README.md b/integration-tests/upgradeable-contracts/README.md index 5f7f3d351ff..b6fb8ca2989 100644 --- a/integration-tests/upgradeable-contracts/README.md +++ b/integration-tests/upgradeable-contracts/README.md @@ -24,10 +24,10 @@ Similarly, the storage compatibility issue is also applicable here. However, there are certain nuances associated with using `delegate_call`. First of all, as demonstrated in the example, if the delegated code intends to mutate the caller's storage, -a developer needs to be mindful. If the delegated code modifies layoutful storage +a developer needs to be mindful. If the delegated code modifies layout-full storage (i.e. it contains at least non-`Lazy`, non-`Mapping` field), the `.set_tail_call(true)` flag of `CallFlags` needs to be specified and the storage layouts must match. This is due to the way ink! execution call stack is operated -(see [StackExchange Answer](https://substrate.stackexchange.com/a/3352/3098) for more explanation). +(see [Stack Exchange Answer](https://substrate.stackexchange.com/a/3352/3098) for more explanation). If the delegated code only modifies `Lazy` or `Mapping` field, the keys must be identical and `.set_tail_call(true)` is optional. This is because `Lazy` and `Mapping` interact with the storage directly instead of loading and flushing storage states.