diff --git a/CHANGELOG.md b/CHANGELOG.md index c5580e8129b..2ec78ff01b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [Linter] Publish the linting crates on crates.io - [#2060](https://github.com/paritytech/ink/pull/2060) - [E2E] Added `create_call_builder` for testing existing contracts - [#2075](https://github.com/paritytech/ink/pull/2075) - `call_v2` cross-contract calls with additional limit parameters - [#2077](https://github.com/paritytech/ink/pull/2077) +- `delegate_dependency` api calls - [#2076](https://github.com/paritytech/ink/pull/2076) ### Changed - `Mapping`: Reflect all possible failure cases in comments ‒ [#2079](https://github.com/paritytech/ink/pull/2079) diff --git a/Cargo.lock b/Cargo.lock index 93eb33830fe..7a4a0844256 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2626,7 +2626,6 @@ name = "ink" version = "5.0.0-rc.1" dependencies = [ "derive_more", - "ink-pallet-contracts-uapi", "ink_env 5.0.0-rc.1", "ink_ir", "ink_macro", @@ -2634,6 +2633,7 @@ dependencies = [ "ink_prelude 5.0.0-rc.1", "ink_primitives 5.0.0-rc.1", "ink_storage", + "pallet-contracts-uapi-next", "parity-scale-codec", "scale-info", "trybuild", @@ -2647,7 +2647,7 @@ checksum = "dd3e608f5410d03e529145875eb736305e0d7cae4b989faf54f932eff31bc048" dependencies = [ "bitflags 1.3.2", "paste", - "polkavm-derive", + "polkavm-derive 0.4.0", ] [[package]] @@ -2743,8 +2743,8 @@ version = "5.0.0-rc.1" dependencies = [ "blake2", "derive_more", - "ink-pallet-contracts-uapi", "ink_primitives 5.0.0-rc.1", + "pallet-contracts-uapi-next", "parity-scale-codec", "secp256k1", "sha2 0.10.8", @@ -2776,13 +2776,13 @@ dependencies = [ "const_env", "derive_more", "ink", - "ink-pallet-contracts-uapi", "ink_allocator 5.0.0-rc.1", "ink_engine 5.0.0-rc.1", "ink_prelude 5.0.0-rc.1", "ink_primitives 5.0.0-rc.1", "ink_storage_traits 5.0.0-rc.1", "num-traits", + "pallet-contracts-uapi-next", "parity-scale-codec", "paste", "rlibc", @@ -2946,13 +2946,13 @@ dependencies = [ "cfg-if", "derive_more", "ink", - "ink-pallet-contracts-uapi", "ink_env 5.0.0-rc.1", "ink_metadata 5.0.0-rc.1", "ink_prelude 5.0.0-rc.1", "ink_primitives 5.0.0-rc.1", "ink_storage_traits 5.0.0-rc.1", "itertools 0.12.1", + "pallet-contracts-uapi-next", "parity-scale-codec", "quickcheck", "quickcheck_macros", @@ -3862,10 +3862,21 @@ dependencies = [ "bitflags 1.3.2", "parity-scale-codec", "paste", - "polkavm-derive", + "polkavm-derive 0.4.0", "scale-info", ] +[[package]] +name = "pallet-contracts-uapi-next" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e941fe56abf3b3d127c80d0a82989323f240ad81e6222421a56f1a3142db1e16" +dependencies = [ + "bitflags 1.3.2", + "paste", + "polkavm-derive 0.5.0", +] + [[package]] name = "pallet-timestamp" version = "27.0.0" @@ -4092,13 +4103,29 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fecd2caacfc4a7ee34243758dd7348859e6dec73f5e5df059890f5742ee46f0e" +[[package]] +name = "polkavm-common" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b4e215c80fe876147f3d58158d5dfeae7dabdd6047e175af77095b78d0035c" + [[package]] name = "polkavm-derive" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db65a500d4adf574893c726ae365e37e4fbb7f2cbd403f6eaa1b665457456adc" dependencies = [ - "polkavm-derive-impl", + "polkavm-derive-impl 0.4.0", + "syn 2.0.48", +] + +[[package]] +name = "polkavm-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6380dbe1fb03ecc74ad55d841cfc75480222d153ba69ddcb00977866cbdabdb8" +dependencies = [ + "polkavm-derive-impl 0.5.0", "syn 2.0.48", ] @@ -4108,7 +4135,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c99f4e7a9ff434ef9c885b874c99d824c3a5693bf5e3e8569bb1d2245a8c1b7f" dependencies = [ - "polkavm-common", + "polkavm-common 0.4.0", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "polkavm-derive-impl" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc8211b3365bbafb2fb32057d68b0e1ca55d079f5cf6f9da9b98079b94b3987d" +dependencies = [ + "polkavm-common 0.5.0", "proc-macro2", "quote", "syn 2.0.48", diff --git a/Cargo.toml b/Cargo.toml index e3f42283b4a..efe7a198893 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,7 @@ const_env = { version = "0.1"} # Substrate dependencies pallet-contracts = { version = "27.0.0", default-features = false } -pallet-contracts-uapi = { package = "ink-pallet-contracts-uapi", version = "=6.0.0", default-features = false } +pallet-contracts-uapi = { package = "pallet-contracts-uapi-next", version = "=6.0.1", default-features = false } sp-core = { version = "28.0.0", default-features = false } sp-keyring = { version = "31.0.0", default-features = false } sp-runtime = { version = "31.0.1", default-features = false } diff --git a/crates/e2e/src/backend.rs b/crates/e2e/src/backend.rs index b3de9755104..2f9b395dddb 100644 --- a/crates/e2e/src/backend.rs +++ b/crates/e2e/src/backend.rs @@ -16,6 +16,7 @@ use super::Keypair; use crate::{ backend_calls::{ InstantiateBuilder, + RemoveCodeBuilder, UploadBuilder, }, builders::CreateBuilderPartial, @@ -159,6 +160,29 @@ pub trait ContractsBackend { UploadBuilder::new(self, contract_name, caller) } + /// Start building a remove code call. + /// + /// # Example + /// + /// ```ignore + /// let contract = client + /// .remove_code(&ink_e2e::alice(), code_hash) + /// // Submit the call for on-chain execution. + /// .submit() + /// .await + /// .expect("remove failed"); + /// ``` + fn remove_code<'a>( + &'a mut self, + caller: &'a Keypair, + code_hash: E::Hash, + ) -> RemoveCodeBuilder + where + Self: Sized + BuilderClient, + { + RemoveCodeBuilder::new(self, caller, code_hash) + } + /// Start building a call using a builder pattern. /// /// # Example @@ -193,8 +217,8 @@ pub trait ContractsBackend { #[async_trait] pub trait BuilderClient: ContractsBackend { - /// Executes a bare `call` for the contract at `account_id`. This function does - /// perform a dry-run, and user is expected to provide the gas limit. + /// Executes a bare `call` for the contract at `account_id`. This function does not + /// perform a dry-run, and the user is expected to provide the gas limit. /// /// Use it when you want to have a more precise control over submitting extrinsic. /// @@ -239,6 +263,13 @@ pub trait BuilderClient: ContractsBackend { storage_deposit_limit: Option, ) -> Result, Self::Error>; + /// Removes the code of the contract at `code_hash`. + async fn bare_remove_code( + &mut self, + caller: &Keypair, + code_hash: E::Hash, + ) -> Result; + /// Bare instantiate call. This function does not perform a dry-run, /// and user is expected to provide the gas limit. /// diff --git a/crates/e2e/src/backend_calls.rs b/crates/e2e/src/backend_calls.rs index ebf30f6c2b6..dc192e59c65 100644 --- a/crates/e2e/src/backend_calls.rs +++ b/crates/e2e/src/backend_calls.rs @@ -400,6 +400,37 @@ where } } +/// Allows to build an end-to-end remove code call using a builder pattern. +pub struct RemoveCodeBuilder<'a, E, B> +where + E: Environment, + B: BuilderClient, +{ + client: &'a mut B, + caller: &'a Keypair, + code_hash: E::Hash, +} + +impl<'a, E, B> RemoveCodeBuilder<'a, E, B> +where + E: Environment, + B: BuilderClient, +{ + /// Initialize a remove code builder with essential values. + pub fn new(client: &'a mut B, caller: &'a Keypair, code_hash: E::Hash) -> Self { + Self { + client, + caller, + code_hash, + } + } + + /// Submit the remove code extrinsic. + pub async fn submit(&mut self) -> Result { + B::bare_remove_code(self.client, self.caller, self.code_hash).await + } +} + fn calculate_weight( mut proof_size: u64, mut ref_time: u64, diff --git a/crates/e2e/src/drink_client.rs b/crates/e2e/src/drink_client.rs index 0fd3bb34e34..746b9ce1543 100644 --- a/crates/e2e/src/drink_client.rs +++ b/crates/e2e/src/drink_client.rs @@ -208,7 +208,7 @@ where #[async_trait] impl< AccountId: Clone + Send + Sync + From<[u8; 32]> + AsRef<[u8; 32]>, - Hash: Copy + From<[u8; 32]>, + Hash: Copy + Send + From<[u8; 32]>, Config: SandboxConfig, E: Environment< AccountId = AccountId, @@ -341,6 +341,14 @@ where }) } + async fn bare_remove_code( + &mut self, + _caller: &Keypair, + _code_hash: E::Hash, + ) -> Result { + unimplemented!("drink! sandbox does not yet support remove_code") + } + async fn bare_call( &mut self, caller: &Keypair, @@ -417,7 +425,7 @@ where impl< AccountId: Clone + Send + Sync + From<[u8; 32]> + AsRef<[u8; 32]>, - Hash: Copy + From<[u8; 32]>, + Hash: Copy + Send + From<[u8; 32]>, Config: SandboxConfig, E: Environment< AccountId = AccountId, diff --git a/crates/e2e/src/error.rs b/crates/e2e/src/error.rs index b41b7d4d9ea..d3d2a294943 100644 --- a/crates/e2e/src/error.rs +++ b/crates/e2e/src/error.rs @@ -44,6 +44,9 @@ pub enum Error { /// The `call` extrinsic failed. #[error("Call extrinsic error: {0}")] CallExtrinsic(DispatchError), + /// The `remove_code` extrinsic failed. + #[error("Remove code extrinsic error: {0}")] + RemoveCodeExtrinsic(DispatchError), /// Error fetching account balance. #[error("Fetching account Balance error: {0}")] Balance(String), diff --git a/crates/e2e/src/subxt_client.rs b/crates/e2e/src/subxt_client.rs index 7c68a122d21..2439f34026d 100644 --- a/crates/e2e/src/subxt_client.rs +++ b/crates/e2e/src/subxt_client.rs @@ -468,7 +468,7 @@ where E::AccountId: Debug + Send + Sync, E::Balance: Clone + Debug + Send + Sync + From + scale::HasCompact + serde::Serialize, - E::Hash: Debug + Send + scale::Encode, + E::Hash: Debug + Send + Sync + scale::Encode, { async fn bare_instantiate( &mut self, @@ -536,6 +536,30 @@ where Ok(ret) } + async fn bare_remove_code( + &mut self, + caller: &Keypair, + code_hash: E::Hash, + ) -> Result { + let tx_events = self.api.remove_code(caller, code_hash).await; + + for evt in tx_events.iter() { + let evt = evt.unwrap_or_else(|err| { + panic!("unable to unwrap event: {err:?}"); + }); + + if is_extrinsic_failed_event(&evt) { + let metadata = self.api.client.metadata(); + let dispatch_error = + DispatchError::decode_from(evt.field_bytes(), metadata) + .map_err(|e| Error::Decoding(e.to_string()))?; + return Err(Error::RemoveCodeExtrinsic(dispatch_error)) + } + } + + Ok(tx_events) + } + async fn bare_call( &mut self, caller: &Keypair, @@ -571,7 +595,7 @@ where if is_extrinsic_failed_event(&evt) { let metadata = self.api.client.metadata(); let dispatch_error = - subxt::error::DispatchError::decode_from(evt.field_bytes(), metadata) + DispatchError::decode_from(evt.field_bytes(), metadata) .map_err(|e| Error::Decoding(e.to_string()))?; log_error(&format!("extrinsic for call failed: {dispatch_error}")); return Err(Error::CallExtrinsic(dispatch_error)) @@ -660,13 +684,14 @@ where C::Address: From, C::Signature: From, C::Address: Send + Sync, - <::ExtrinsicParams as subxt::config::ExtrinsicParams>::OtherParams: Default + Send + Sync, + <::ExtrinsicParams as ExtrinsicParams>::OtherParams: + Default + Send + Sync, E: Environment, E::AccountId: Debug + Send + Sync, E::Balance: Clone + Debug + Send + Sync + From + scale::HasCompact + serde::Serialize, - E::Hash: Debug + Send + scale::Encode, + E::Hash: Debug + Send + Sync + scale::Encode, { } diff --git a/crates/e2e/src/xts.rs b/crates/e2e/src/xts.rs index 28e64f3ae70..458ea425726 100644 --- a/crates/e2e/src/xts.rs +++ b/crates/e2e/src/xts.rs @@ -144,6 +144,13 @@ pub enum Determinism { Relaxed, } +/// A raw call to `pallet-contracts`'s `remove_code`. +#[derive(Debug, scale::Encode, scale::Decode, scale_encode::EncodeAsType)] +#[encode_as_type(trait_bounds = "", crate_path = "subxt::ext::scale_encode")] +pub struct RemoveCode { + code_hash: E::Hash, +} + /// A raw call to `pallet-contracts`'s `upload`. #[derive(Debug, scale::Encode, scale::Decode, scale_encode::EncodeAsType)] #[encode_as_type(trait_bounds = "", crate_path = "subxt::ext::scale_encode")] @@ -471,6 +478,25 @@ where self.submit_extrinsic(&call, signer).await } + /// Submits an extrinsic to remove the code at the given hash. + /// + /// Returns when the transaction is included in a block. The return value + /// contains all events that are associated with this transaction. + pub async fn remove_code( + &self, + signer: &Keypair, + code_hash: E::Hash, + ) -> ExtrinsicEvents { + let call = subxt::tx::Payload::new( + "Contracts", + "remove_code", + RemoveCode:: { code_hash }, + ) + .unvalidated(); + + self.submit_extrinsic(&call, signer).await + } + /// Dry runs a call of the contract at `contract` with the given parameters. pub async fn call_dry_run( &self, diff --git a/crates/env/src/api.rs b/crates/env/src/api.rs index daac732e899..a532f593c8b 100644 --- a/crates/env/src/api.rs +++ b/crates/env/src/api.rs @@ -823,3 +823,42 @@ where TypedEnvBackend::call_runtime::(instance, call) }) } + +/// Adds a new delegate dependency lock to the contract. +/// +/// This guarantees that the code of the dependency cannot be removed without first +/// calling [`unlock_delegate_dependency`]. It charges a fraction of the code +/// deposit, see [`pallet_contracts::Config::CodeHashLockupDepositPercent`](https://docs.rs/pallet-contracts/latest/pallet_contracts/pallet/trait.Config.html#associatedtype.CodeHashLockupDepositPercent) for details. +/// +/// # Errors +/// +/// - If the supplied `code_hash` cannot be found on-chain. +/// - If the `code_hash` is the same as the calling contract. +/// - If the maximum number of delegate dependencies is reached. +/// - If the delegate dependency already exists. +pub fn lock_delegate_dependency(code_hash: &E::Hash) +where + E: Environment, +{ + ::on_instance(|instance| { + instance.lock_delegate_dependency::(code_hash) + }) +} + +/// Unlocks the delegate dependency from the contract. +/// +/// This removes the lock and refunds the deposit from the call to +/// [`lock_delegate_dependency`]. The code of the dependency can be removed if the +/// reference count for the code hash is now zero. +/// +/// # Errors +/// +/// - If the delegate dependency does not exist. +pub fn unlock_delegate_dependency(code_hash: &E::Hash) +where + E: Environment, +{ + ::on_instance(|instance| { + instance.unlock_delegate_dependency::(code_hash) + }) +} diff --git a/crates/env/src/backend.rs b/crates/env/src/backend.rs index 43597082b37..55af65dc965 100644 --- a/crates/env/src/backend.rs +++ b/crates/env/src/backend.rs @@ -418,4 +418,24 @@ pub trait TypedEnvBackend: EnvBackend { where E: Environment, Call: scale::Encode; + + /// Adds a new delegate dependency lock to the contract. + /// + /// # Note + /// + /// For more details visit: + /// [`lock_delegate_dependency`][`crate::lock_delegate_dependency`] + fn lock_delegate_dependency(&mut self, code_hash: &E::Hash) + where + E: Environment; + + /// Unlocks the delegate dependency from the contract. + /// + /// # Note + /// + /// For more details visit: + /// [`unlock_delegate_dependency`][`crate::unlock_delegate_dependency`]. + fn unlock_delegate_dependency(&mut self, code_hash: &E::Hash) + where + E: Environment; } diff --git a/crates/env/src/engine/off_chain/impls.rs b/crates/env/src/engine/off_chain/impls.rs index 2e567004f2b..92321ae6e9f 100644 --- a/crates/env/src/engine/off_chain/impls.rs +++ b/crates/env/src/engine/off_chain/impls.rs @@ -560,4 +560,18 @@ impl TypedEnvBackend for EnvInstance { { unimplemented!("off-chain environment does not support `call_runtime`") } + + fn lock_delegate_dependency(&mut self, _code_hash: &E::Hash) + where + E: Environment, + { + unimplemented!("off-chain environment does not support delegate dependencies") + } + + fn unlock_delegate_dependency(&mut self, _code_hash: &E::Hash) + where + E: Environment, + { + unimplemented!("off-chain environment does not support delegate dependencies") + } } diff --git a/crates/env/src/engine/on_chain/impls.rs b/crates/env/src/engine/on_chain/impls.rs index ce36767ab90..10f5af7d155 100644 --- a/crates/env/src/engine/on_chain/impls.rs +++ b/crates/env/src/engine/on_chain/impls.rs @@ -447,6 +447,7 @@ impl TypedEnvBackend for EnvInstance { }; let output = &mut scope.take_rest(); let flags = params.call_flags(); + #[allow(deprecated)] let call_result = ext::call_v1( *flags, enc_callee, @@ -570,6 +571,7 @@ impl TypedEnvBackend for EnvInstance { let salt = params.salt_bytes().as_ref(); let out_return_value = &mut scoped.take_rest(); + #[allow(deprecated)] let instantiate_result = ext::instantiate_v1( enc_code_hash, gas_limit, @@ -660,4 +662,22 @@ impl TypedEnvBackend for EnvInstance { let enc_call = scope.take_encoded(call); ext::call_runtime(enc_call).map_err(Into::into) } + + fn lock_delegate_dependency(&mut self, code_hash: &E::Hash) + where + E: Environment, + { + let mut scope = self.scoped_buffer(); + let enc_code_hash = scope.take_encoded(code_hash); + ext::lock_delegate_dependency(enc_code_hash) + } + + fn unlock_delegate_dependency(&mut self, code_hash: &E::Hash) + where + E: Environment, + { + let mut scope = self.scoped_buffer(); + let enc_code_hash = scope.take_encoded(code_hash); + ext::unlock_delegate_dependency(enc_code_hash) + } } diff --git a/crates/ink/src/env_access.rs b/crates/ink/src/env_access.rs index 7dc17992ae9..a19f9ed7b8b 100644 --- a/crates/ink/src/env_access.rs +++ b/crates/ink/src/env_access.rs @@ -1183,4 +1183,67 @@ where pub fn call_runtime(self, call: &Call) -> Result<()> { ink_env::call_runtime::(call) } + + /// Adds a new delegate dependency lock for the given `code_hash` to prevent it from + /// being deleted. + /// + /// # Example + /// + /// ``` + /// # #[ink::contract] + /// # pub mod my_contract { + /// # #[ink(storage)] + /// # pub struct MyContract { } + /// # + /// # impl MyContract { + /// # #[ink(constructor)] + /// # pub fn new() -> Self { + /// # Self {} + /// # } + /// # + /// #[ink(message)] + /// pub fn lock_delegate_dependency(&mut self, code_hash: Hash) { + /// self.env().lock_delegate_dependency(&code_hash) + /// } + /// # } + /// # } + /// ``` + /// + /// # Note + /// + /// For more details visit: [`ink_env::lock_delegate_dependency`] + pub fn lock_delegate_dependency(self, code_hash: &E::Hash) { + ink_env::lock_delegate_dependency::(code_hash) + } + + /// Removes the delegate dependency lock from this contract for the given `code_hash`. + /// + /// # Example + /// + /// ``` + /// # #[ink::contract] + /// # pub mod my_contract { + /// # #[ink(storage)] + /// # pub struct MyContract { } + /// # + /// # impl MyContract { + /// # #[ink(constructor)] + /// # pub fn new() -> Self { + /// # Self {} + /// # } + /// # + /// #[ink(message)] + /// pub fn unlock_delegate_dependency(&mut self, code_hash: Hash) { + /// self.env().unlock_delegate_dependency(&code_hash) + /// } + /// # } + /// # } + /// ``` + /// + /// # Note + /// + /// For more details visit: [`ink_env::unlock_delegate_dependency`] + pub fn unlock_delegate_dependency(self, code_hash: &E::Hash) { + ink_env::unlock_delegate_dependency::(code_hash) + } } diff --git a/integration-tests/upgradeable-contracts/delegator/.gitignore b/integration-tests/upgradeable-contracts/delegator/.gitignore deleted file mode 100644 index bf910de10af..00000000000 --- a/integration-tests/upgradeable-contracts/delegator/.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/delegator/Cargo.toml b/integration-tests/upgradeable-contracts/delegator/Cargo.toml index bf1cca71edd..fcac593edb5 100644 --- a/integration-tests/upgradeable-contracts/delegator/Cargo.toml +++ b/integration-tests/upgradeable-contracts/delegator/Cargo.toml @@ -8,6 +8,7 @@ publish = false [dependencies] ink = { path = "../../../crates/ink", default-features = false } delegatee = { path = "delegatee", default-features = false, features = ["ink-as-dependency"] } +delegatee2 = { path = "delegatee2", default-features = false, features = ["ink-as-dependency"] } [dev-dependencies] ink_e2e = { path = "../../../crates/e2e" } diff --git a/integration-tests/upgradeable-contracts/delegator/delegatee/.gitignore b/integration-tests/upgradeable-contracts/delegator/delegatee/.gitignore deleted file mode 100644 index bf910de10af..00000000000 --- a/integration-tests/upgradeable-contracts/delegator/delegatee/.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/delegator/delegatee2/Cargo.toml b/integration-tests/upgradeable-contracts/delegator/delegatee2/Cargo.toml new file mode 100644 index 00000000000..3a0a91ca0a8 --- /dev/null +++ b/integration-tests/upgradeable-contracts/delegator/delegatee2/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "delegatee2" +version = "5.0.0-rc.1" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../../../crates/ink", default-features = false } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/integration-tests/upgradeable-contracts/delegator/delegatee2/lib.rs b/integration-tests/upgradeable-contracts/delegator/delegatee2/lib.rs new file mode 100644 index 00000000000..447db46e00f --- /dev/null +++ b/integration-tests/upgradeable-contracts/delegator/delegatee2/lib.rs @@ -0,0 +1,37 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +pub mod delegatee2 { + use ink::storage::{ + traits::ManualKey, + Mapping, + }; + #[ink(storage)] + pub struct Delegatee2 { + addresses: Mapping>, + counter: i32, + } + + impl Delegatee2 { + #[allow(clippy::new_without_default)] + #[ink(constructor)] + pub fn new() -> Self { + unreachable!( + "Constructors are not called when upgrading using `set_code_hash`." + ) + } + + /// Increments the current value. + #[ink(message)] + pub fn inc(&mut self) { + self.counter = self.counter.checked_add(3).unwrap(); + } + + /// 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 index 10c26dc62fb..75b43bb04ea 100644 --- a/integration-tests/upgradeable-contracts/delegator/lib.rs +++ b/integration-tests/upgradeable-contracts/delegator/lib.rs @@ -2,11 +2,6 @@ #[ink::contract] pub mod delegator { - use ink::{ - env::CallFlags, - storage::Mapping, - }; - use ink::{ env::{ call::{ @@ -14,40 +9,67 @@ pub mod delegator { ExecutionInput, Selector, }, + CallFlags, DefaultEnvironment, }, - storage::traits::ManualKey, + storage::{ + traits::ManualKey, + Lazy, + Mapping, + }, }; #[ink(storage)] pub struct Delegator { addresses: Mapping>, counter: i32, + delegate_to: Lazy, } impl Delegator { - /// Creates a new delegator smart contract initialized with the given value. + /// Creates a new delegator smart contract with an initial value, and the hash of + /// the contract code to delegate to. + /// + /// Additionally, this code hash will be locked to prevent its deletion, since + /// this contract depends on it. #[ink(constructor)] - pub fn new(init_value: i32) -> Self { + pub fn new(init_value: i32, hash: Hash) -> Self { let v = Mapping::new(); + + // Initialize the hash of the contract to delegate to. + // Adds a delegate dependency lock, ensuring that the delegated to code cannot + // be removed. + let mut delegate_to = Lazy::new(); + delegate_to.set(&hash); + Self::env().lock_delegate_dependency(&hash); + Self { addresses: v, counter: init_value, + delegate_to, } } - /// Creates a new contract with default values. - #[ink(constructor)] - pub fn new_default() -> Self { - Self::new(Default::default()) + /// Update the hash of the contract to delegate to. + /// - Unlocks the old delegate dependency, releasing the deposit and allowing old + /// delegated to code to be removed. + /// - Adds a new delegate dependency lock, ensuring that the new delegated to code + /// cannot be removed. + #[ink(message)] + pub fn update_delegate_to(&mut self, hash: Hash) { + if let Some(old_hash) = self.delegate_to.get() { + self.env().unlock_delegate_dependency(&old_hash) + } + self.env().lock_delegate_dependency(&hash); + self.delegate_to.set(&hash); } /// Increment the current value using delegate call. #[ink(message)] - pub fn inc_delegate(&mut self, hash: Hash) { + pub fn inc_delegate(&mut self) { let selector = ink::selector_bytes!("inc"); let _ = build_call::() - .delegate(hash) + .delegate(self.delegate_to()) // We specify `CallFlags::TAIL_CALL` to use the delegatee last memory frame // as the end of the execution cycle. // So any mutations to `Packed` types, made by delegatee, @@ -66,10 +88,10 @@ pub mod delegator { /// Note that we don't need `CallFlags::TAIL_CALL` flag /// because `Mapping` updates the storage instantly on-demand. #[ink(message)] - pub fn add_entry_delegate(&mut self, hash: Hash) { + pub fn add_entry_delegate(&mut self) { let selector = ink::selector_bytes!("append_address_value"); let _ = build_call::() - .delegate(hash) + .delegate(self.delegate_to()) .exec_input(ExecutionInput::new(Selector::new(selector))) .returns::<()>() .try_invoke(); @@ -86,6 +108,12 @@ pub mod delegator { pub fn get_value(&self, address: AccountId) -> (AccountId, Option) { (self.env().caller(), self.addresses.get(address)) } + + fn delegate_to(&self) -> Hash { + self.delegate_to + .get() + .expect("delegate_to always has a value") + } } #[cfg(all(test, feature = "e2e-tests"))] @@ -107,14 +135,6 @@ pub mod delegator { .create_and_fund_account(&ink_e2e::alice(), 10_000_000_000_000) .await; - let mut constructor = DelegatorRef::new_default(); - let contract = client - .instantiate("delegator", &origin, &mut constructor) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - let code_hash = client .upload("delegatee", &origin) .submit() @@ -122,8 +142,16 @@ pub mod delegator { .expect("upload `delegatee` failed") .code_hash; + let mut constructor = DelegatorRef::new(0, code_hash); + let contract = client + .instantiate("delegator", &origin, &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + // when - let call_delegate = call_builder.inc_delegate(code_hash); + let call_delegate = call_builder.inc_delegate(); let result = client.call(&origin, &call_delegate).submit().await; assert!(result.is_ok(), "delegate call failed."); @@ -159,8 +187,15 @@ pub mod delegator { .create_and_fund_account(&ink_e2e::alice(), 10_000_000_000_000) .await; + let code_hash = client + .upload("delegatee", &origin) + .submit() + .await + .expect("upload `delegatee` failed") + .code_hash; + // given - let mut constructor = DelegatorRef::new(10); + let mut constructor = DelegatorRef::new(10, code_hash); let contract = client .instantiate("delegator", &origin, &mut constructor) .submit() @@ -168,15 +203,8 @@ pub mod delegator { .expect("instantiate failed"); let mut call_builder = contract.call_builder::(); - let code_hash = client - .upload("delegatee", &origin) - .submit() - .await - .expect("upload `delegatee` failed") - .code_hash; - // when - let call_delegate = call_builder.add_entry_delegate(code_hash); + let call_delegate = call_builder.add_entry_delegate(); let result = client.call(&origin, &call_delegate).submit().await; assert!(result.is_ok(), "delegate call failed."); @@ -204,5 +232,57 @@ pub mod delegator { Ok(()) } + + #[ink_e2e::test] + async fn update_delegate( + mut client: Client, + ) -> E2EResult<()> { + // given + let origin = client + .create_and_fund_account(&ink_e2e::alice(), 10_000_000_000_000) + .await; + + let code_hash = client + .upload("delegatee", &origin) + .submit() + .await + .expect("upload `delegatee` failed") + .code_hash; + + let code_hash2 = client + .upload("delegatee2", &origin) + .submit() + .await + .expect("upload `delegatee2` failed") + .code_hash; + + let mut constructor = DelegatorRef::new(10, code_hash); + let contract = client + .instantiate("delegator", &origin, &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + // when + let call_delegate = call_builder.update_delegate_to(code_hash2); + let result = client.call(&origin, &call_delegate).submit().await; + assert!(result.is_ok(), "update_delegate_to failed."); + + // then + + // remove the original delegatee code. + // should succeed because the delegate dependency has been removed. + let original_code_removed = + client.remove_code(&origin, code_hash).submit().await; + assert!(original_code_removed.is_ok()); + + // attempt to remove the new delegatee code. + // should fail because of the delegate dependency. + let new_code_removed = client.remove_code(&origin, code_hash2).submit().await; + assert!(new_code_removed.is_err()); + + Ok(()) + } } }