From 91661338ce1445fe5309abc78fefe88f96b4a029 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Mon, 8 Jan 2024 19:50:28 +0100 Subject: [PATCH 01/13] Add blacklist logic --- examples/cis2-multi/src/lib.rs | 105 ++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index 4b00bde3c..c075b4be5 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -1,3 +1,4 @@ +//! TODO: Explain Blacklist. //! A multi token example implementation of the Concordium Token Standard CIS2 //! and the Concordium Sponsored Transaction Standard CIS3. //! @@ -75,8 +76,9 @@ const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = const SUPPORTS_PERMIT_ENTRYPOINTS: [EntrypointName; 2] = [EntrypointName::new_unchecked("updateOperator"), EntrypointName::new_unchecked("transfer")]; -/// Tag for the CIS3 Nonce event. +/// Event tags. pub const NONCE_EVENT_TAG: u8 = u8::MAX - 5; +pub const UPDATE_BLACKLIST_EVENT_TAG: u8 = u8::MAX - 6; /// Tagged events to be serialized for the event log. #[derive(Debug, Serial, Deserial, PartialEq, Eq)] @@ -86,6 +88,9 @@ pub enum Event { /// whenever the `permit` function is invoked. #[concordium(tag = 250)] Nonce(NonceEvent), + /// The event is logged whenever an address is added or removed from the blacklist. + #[concordium(tag = 249)] + UpdateBlacklist(UpdateBlacklistEvent), /// Cis2 token events. #[concordium(forward = cis2_events)] Cis2Event(Cis2Event), @@ -101,6 +106,15 @@ pub struct NonceEvent { pub nonce: u64, } +/// The UpdateBlacklistEvent is logged when an address is added to or removed from the blacklist. +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] +pub struct UpdateBlacklistEvent { + /// The update to the address. + pub update: BlacklistUpdate, + /// The address which is added/removed from the blacklist. + pub address: Address, +} + // Implementing a custom schemaType for the `Event` struct containing all // CIS2/CIS3 events. This custom implementation flattens the fields to avoid one // level of nesting. Deriving the schemaType would result in e.g.: {"Nonce": @@ -119,6 +133,16 @@ impl schema::SchemaType for Event { ]), ), ); + event_map.insert( + UPDATE_BLACKLIST_EVENT_TAG, + ( + "UpdateBlacklistEvent".to_string(), + schema::Fields::Named(vec![ + (String::from("update"), BlacklistUpdate::get_type()), + (String::from("address"), Address::get_type()), + ]), + ), + ); event_map.insert( TRANSFER_EVENT_TAG, ( @@ -242,7 +266,11 @@ struct State { nonces_registry: StateMap, /// The amount of tokens airdropped when the mint function is invoked. mint_airdrop: TokenAmountU64, + /// Set of addresses that are not allowed to receive new tokens, sent + /// their tokens, or burn their tokens. + blacklist: StateSet, } + /// The parameter type for the contract function `supportsPermit`. #[derive(Debug, Serialize, SchemaType)] pub struct SupportsPermitQueryParams { @@ -378,6 +406,7 @@ impl State { implementors: state_builder.new_map(), nonces_registry: state_builder.new_map(), mint_airdrop, + blacklist: state_builder.new_set(), } } @@ -499,6 +528,14 @@ impl State { }); } + /// Update the state adding a new address to the blacklist. + /// Succeeds even if the `address` is already in the blacklist. + fn add_blacklist(&mut self, address: &Address) { self.blacklist.insert(*address); } + + /// Update the state removing an address from the blacklist. + /// Succeeds even if the `address` is not in the list. + fn remove_blacklist(&mut self, address: &Address) { self.blacklist.remove(address); } + /// Check if state contains any implementors for a given standard. fn have_implementors(&self, std_id: &StandardIdentifierOwned) -> SupportResult { if let Some(addresses) = self.implementors.get(std_id) { @@ -1280,3 +1317,69 @@ fn contract_set_implementor(ctx: &ReceiveContext, host: &mut Host) -> Con host.state_mut().set_implementors(params.id, params.implementors); Ok(()) } + +/// The update to an address with respect to the blacklist. +#[derive(Debug, Serialize, Clone, Copy, SchemaType, PartialEq, Eq)] +pub enum BlacklistUpdate { + /// Remove from blacklist. + Remove, + /// Add to blacklist. + Add, +} + +/// A single update of an address with respect to the blacklist. +#[derive(Debug, Serialize, Clone, SchemaType, PartialEq, Eq)] +pub struct UpdateBlacklist { + /// The update for this address. + pub update: BlacklistUpdate, + /// The address which is either added to or removed from the blacklist. + pub address: Address, +} + +/// The parameter type for the contract function `updateBlacklist`. +#[derive(Debug, Serialize, Clone, SchemaType)] +#[concordium(transparent)] +pub struct UpdateBlacklistParams(#[concordium(size_length = 2)] pub Vec); + +/// Add addresses or remove addresses from the blacklist. +/// Logs an `UpdateBlacklist` event. +/// +/// It rejects if: +/// - Sender is not the owner of the contract instance. +/// - It fails to parse the parameter. +/// - Fails to log event. +#[receive( + contract = "cis2_multi", + name = "updateBlacklist", + parameter = "UpdateBlacklistParams", + error = "ContractError", + enable_logger, + mutable +)] +fn contract_update_blacklist( + ctx: &ReceiveContext, + host: &mut Host, + logger: &mut impl HasLogger, +) -> ContractResult<()> { + // Authorize the sender. + ensure!(ctx.sender().matches_account(&ctx.owner()), ContractError::Unauthorized); + + // Parse the parameter. + let UpdateBlacklistParams(params) = ctx.parameter_cursor().get()?; + + for param in params { + // Add/remove address from the blacklist. + match param.update { + BlacklistUpdate::Add => host.state_mut().add_blacklist(¶m.address), + BlacklistUpdate::Remove => host.state_mut().remove_blacklist(¶m.address), + } + + // Log the nonce event. + logger.log(&Event::UpdateBlacklist(UpdateBlacklistEvent { + address: param.address, + update: param.update, + }))?; + } + + Ok(()) +} From 4fefd2daa5078ec8e775307d981207bcbd44bea8 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 9 Jan 2024 13:11:09 +0100 Subject: [PATCH 02/13] Add blacklist tests --- examples/cis2-multi/src/lib.rs | 44 ++++++++++- examples/cis2-multi/tests/tests.rs | 113 +++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 4 deletions(-) diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index c075b4be5..73edd1f90 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -88,7 +88,8 @@ pub enum Event { /// whenever the `permit` function is invoked. #[concordium(tag = 250)] Nonce(NonceEvent), - /// The event is logged whenever an address is added or removed from the blacklist. + /// The event is logged whenever an address is added or removed from the + /// blacklist. #[concordium(tag = 249)] UpdateBlacklist(UpdateBlacklistEvent), /// Cis2 token events. @@ -106,7 +107,8 @@ pub struct NonceEvent { pub nonce: u64, } -/// The UpdateBlacklistEvent is logged when an address is added to or removed from the blacklist. +/// The UpdateBlacklistEvent is logged when an address is added to or removed +/// from the blacklist. #[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] pub struct UpdateBlacklistEvent { /// The update to the address. @@ -530,7 +532,7 @@ impl State { /// Update the state adding a new address to the blacklist. /// Succeeds even if the `address` is already in the blacklist. - fn add_blacklist(&mut self, address: &Address) { self.blacklist.insert(*address); } + fn add_blacklist(&mut self, address: Address) { self.blacklist.insert(address); } /// Update the state removing an address from the blacklist. /// Succeeds even if the `address` is not in the list. @@ -1052,6 +1054,40 @@ fn contract_operator_of( Ok(result) } +/// The parameter type for the contract functions `isBlacklisted`. +#[derive(Debug, Serialize, SchemaType)] +#[concordium(transparent)] +pub struct VecOfAddresses { + /// List of queries. + #[concordium(size_length = 2)] + pub queries: Vec
, +} + +/// Takes a list of queries. Each query contains an address which is checked if +/// that address is blacklisted. +/// +/// It rejects if: +/// - It fails to parse the parameter. +#[receive( + contract = "cis2_multi", + name = "isBlacklisted", + parameter = "VecOfAddresses", + return_value = "Vec", + error = "ContractError" +)] +fn contract_is_blacklisted(ctx: &ReceiveContext, host: &Host) -> ContractResult> { + // Parse the parameter. + let params: VecOfAddresses = ctx.parameter_cursor().get()?; + // Build the response. + let mut response = Vec::with_capacity(params.queries.len()); + for address in params.queries { + // Query the state if address is blacklisted. + let is_blacklisted = host.state().blacklist.contains(&address); + response.push(is_blacklisted); + } + Ok(response) +} + /// Response type for the function `publicKeyOf`. #[derive(Debug, Serialize, SchemaType)] #[concordium(transparent)] @@ -1370,7 +1406,7 @@ fn contract_update_blacklist( for param in params { // Add/remove address from the blacklist. match param.update { - BlacklistUpdate::Add => host.state_mut().add_blacklist(¶m.address), + BlacklistUpdate::Add => host.state_mut().add_blacklist(param.address), BlacklistUpdate::Remove => host.state_mut().remove_blacklist(¶m.address), } diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs index 6ea494597..4d066c83d 100644 --- a/examples/cis2-multi/tests/tests.rs +++ b/examples/cis2-multi/tests/tests.rs @@ -635,6 +635,98 @@ fn test_public_key_of_query() { assert!(public_keys_of.0[1].is_none(), "Non existing account should have no public keys"); } +/// Test adding and removing from blacklist works. +#[test] +fn test_adding_to_blacklist() { + let (mut chain, _keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Check that Alice and Bob are not blacklisted. + let rv: Vec = is_blacklisted(&chain, contract_address); + assert_eq!(rv, [false, false]); + + // Create input parameters to add Bob to the blacklist. + let update_blacklist = UpdateBlacklist { + update: BlacklistUpdate::Add, + address: BOB_ADDR, + }; + let update_blacklist_params = UpdateBlacklistParams(vec![update_blacklist]); + + // Update blacklist. + let update = chain + .contract_update( + Signer::with_one_key(), + ALICE, + ALICE_ADDR, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi.updateBlacklist".to_string(), + ), + message: OwnedParameter::from_serial(&update_blacklist_params) + .expect("Update blacklist params"), + }, + ) + .expect("Should be able to update blacklist"); + + // Check that the correct events occurred. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>(); + + assert_eq!(events, [Event::UpdateBlacklist(UpdateBlacklistEvent { + address: BOB_ADDR, + update: BlacklistUpdate::Add, + })]); + + // Check that Alice is not blacklisted and Bob is blacklisted. + let rv: Vec = is_blacklisted(&chain, contract_address); + assert_eq!(rv, [false, true]); + + // Create input parameters to remove Bob from the blacklist. + let update_blacklist = UpdateBlacklist { + update: BlacklistUpdate::Remove, + address: BOB_ADDR, + }; + let update_blacklist_params = UpdateBlacklistParams(vec![update_blacklist]); + + // Update blacklist. + let update = chain + .contract_update( + Signer::with_one_key(), + ALICE, + ALICE_ADDR, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi.updateBlacklist".to_string(), + ), + message: OwnedParameter::from_serial(&update_blacklist_params) + .expect("Update blacklist params"), + }, + ) + .expect("Should be able to update blacklist"); + + // Check that the correct events occurred. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>(); + + assert_eq!(events, [Event::UpdateBlacklist(UpdateBlacklistEvent { + address: BOB_ADDR, + update: BlacklistUpdate::Remove, + })]); + + // Check that Alice and Bob are not blacklisted. + let rv: Vec = is_blacklisted(&chain, contract_address); + assert_eq!(rv, [false, false]); +} + /// Check if Bob is an operator of Alice. fn operator_of(chain: &Chain, contract_address: ContractAddress) -> OperatorOfQueryResponse { let operator_of_params = OperatorOfQueryParams { @@ -658,6 +750,27 @@ fn operator_of(chain: &Chain, contract_address: ContractAddress) -> OperatorOfQu rv } +/// Check if Alice and Bob are blacklisted. +fn is_blacklisted(chain: &Chain, contract_address: ContractAddress) -> Vec { + let addresses_query_vector = VecOfAddresses { + queries: vec![ALICE_ADDR, BOB_ADDR], + }; + + // Invoke the isBlacklisted entrypoint. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.isBlacklisted".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&addresses_query_vector) + .expect("Should be a valid inut parameter"), + }) + .expect("Invoke isBlacklisted"); + + let rv: Vec = invoke.parse_return_value().expect("Vec return value"); + rv +} + /// Get the `TOKEN_1` balances for Alice and Bob. fn get_balances( chain: &Chain, From d72967dcc74fca79c99bd69d6eaab02f5280d8b5 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 9 Jan 2024 14:12:48 +0100 Subject: [PATCH 03/13] Add burn function and test --- examples/cis2-multi/src/lib.rs | 79 ++++++++++++++++++++++++++++++ examples/cis2-multi/tests/tests.rs | 47 ++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index 73edd1f90..ed7477f07 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -226,6 +226,18 @@ pub struct MintParams { pub token_id: ContractTokenId, } +/// The parameter for the contract function `burn` which burns a number +/// of tokens from the owner's address. +#[derive(Serialize, SchemaType)] +pub struct BurnParams { + /// Owner of the tokens. + pub owner: Address, + /// The amount of tokens to burn. + pub amount: ContractTokenAmount, + /// The token_id of the tokens to be burned. + pub token_id: ContractTokenId, +} + /// The state for each address. #[derive(Serial, DeserialWithState, Deletable)] #[concordium(state_parameter = "S")] @@ -441,6 +453,25 @@ impl State { } } + /// Burns an amount of tokens from a given owner address. + fn burn( + &mut self, + token_id: &ContractTokenId, + amount: ContractTokenAmount, + owner: &Address, + ) -> ContractResult<()> { + ensure!(self.contains_token(token_id), ContractError::InvalidTokenId); + + let mut owner_state = + self.state.entry(*owner).occupied_or(ContractError::InsufficientFunds)?; + + let mut owner_balance = owner_state.balances.entry(*token_id).or_insert(0.into()); + ensure!(*owner_balance >= amount, ContractError::InsufficientFunds); + *owner_balance -= amount; + + Ok(()) + } + /// Check that the token ID currently exists in this contract. #[inline(always)] fn contains_token(&self, token_id: &ContractTokenId) -> bool { @@ -671,6 +702,54 @@ fn contract_mint( Ok(()) } +/// Burns an amount of tokens from the +/// `owner` address. +/// +/// Logs a `Burn` event for each token. +/// +/// It rejects if: +/// - Fails to parse parameter. +/// - The sender is not the token owner or an operator of the token owner. +/// - The token owner owns an insufficient token amount to burn from. +/// - Fails to log Burn event. +#[receive( + contract = "cis2_multi", + name = "burn", + parameter = "BurnParams", + error = "ContractError", + enable_logger, + mutable +)] +fn contract_burn( + ctx: &ReceiveContext, + host: &mut Host, + logger: &mut impl HasLogger, +) -> ContractResult<()> { + // Parse the parameter. + let params: BurnParams = ctx.parameter_cursor().get()?; + + // Get the sender who invoked this contract function. + let sender = ctx.sender(); + + // Authenticate the sender for the token burns. + ensure!( + params.owner == sender || host.state().is_operator(&sender, ¶ms.owner), + ContractError::Unauthorized + ); + + // Burn the token in the state. + host.state_mut().burn(¶ms.token_id, params.amount, ¶ms.owner)?; + + // Event for burned tokens. + logger.log(&Cis2Event::Burn(BurnEvent { + token_id: params.token_id, + amount: params.amount, + owner: params.owner, + }))?; + + Ok(()) +} + type TransferParameter = TransferParams; /// Internal `transfer/permit` helper function. Invokes the `transfer` diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs index 4d066c83d..04d0041e1 100644 --- a/examples/cis2-multi/tests/tests.rs +++ b/examples/cis2-multi/tests/tests.rs @@ -635,6 +635,53 @@ fn test_public_key_of_query() { assert!(public_keys_of.0[1].is_none(), "Non existing account should have no public keys"); } +/// Test burning tokens. +#[test] +fn test_burning_tokens() { + let (mut chain, _keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Create input parameters to burn one of Alice's tokens. + let burn_params = BurnParams { + owner: ALICE_ADDR, + amount: TokenAmountU64(1), + token_id: TOKEN_1, + }; + + // Burn one of Alice's tokens. + let update = chain + .contract_update( + Signer::with_one_key(), + ALICE, + ALICE_ADDR, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.burn".to_string()), + message: OwnedParameter::from_serial(&burn_params) + .expect("Should be a valid inut parameter"), + }, + ) + .expect("Should be able to burn tokens"); + + // Check that the event is logged. + let events = update.events().flat_map(|(_addr, events)| events); + + let events: Vec> = + events.map(|e| e.parse().expect("Deserialize event")).collect(); + + assert_eq!(events, [Cis2Event::Burn(BurnEvent { + owner: ALICE_ADDR, + amount: TokenAmountU64(1), + token_id: TOKEN_1, + })]); + + // Check balances in state. + let balance_of_alice_and_bob = get_balances(&chain, contract_address); + + assert_eq!(balance_of_alice_and_bob.0, [TokenAmountU64(99), TokenAmountU64(0)]); +} + /// Test adding and removing from blacklist works. #[test] fn test_adding_to_blacklist() { From b4425d0b341f19a6e1aa1a1d5570d315b34aa2b2 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 9 Jan 2024 17:30:12 +0100 Subject: [PATCH 04/13] Add logic to prevent transfering tokens when blacklisted --- examples/cis2-multi/src/lib.rs | 83 ++++++++++++----- examples/cis2-multi/tests/tests.rs | 145 +++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+), 25 deletions(-) diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index ed7477f07..b3510c954 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -1,4 +1,3 @@ -//! TODO: Explain Blacklist. //! A multi token example implementation of the Concordium Token Standard CIS2 //! and the Concordium Sponsored Transaction Standard CIS3. //! @@ -7,15 +6,10 @@ //! types each identified by a token ID. A token type is then globally //! identified by the contract address together with the token ID. //! -//! In this example the contract is initialized with no tokens, and tokens can -//! be minted through a `mint` contract function, which can be called by anyone. -//! The `mint` function airdrops the `MINT_AIRDROP` amount of tokens to a -//! specified `owner` address in the input parameter. No functionality to burn -//! token is defined in this example. -//! //! Note: The word 'address' refers to either an account address or a //! contract address. //! +//! ## CIS3 standard (sponsored transactions): //! This contract implements the Concordium Token Standard CIS2. In addition, it //! implements the CIS3 standard which includes features for sponsored //! transactions. @@ -31,22 +25,6 @@ //! service provider submits the transaction on behalf of the user and pays the //! transaction fee to execute the transaction on-chain. //! -//! As follows from the CIS2 specification, the contract has a `transfer` -//! function for transferring an amount of a specific token type from one -//! address to another address. An address can enable and disable one or more -//! addresses as operators with the `updateOperator` function. An operator of -//! an address is allowed to transfer any tokens owned by this address. -//! As follows from the CIS3 specification, the contract has a `permit` -//! function. It is the sponsored counterpart to the `transfer/updateOperator` -//! function and can be executed by anyone on behalf of an account given a -//! signed message. -//! -//! This contract also contains an example of a function to be called when -//! receiving tokens. In which case the contract will forward the tokens to -//! the contract owner. -//! This function is not very useful and is only there to showcase a simple -//! implementation of a token receive hook. -//! //! Concordium supports natively multi-sig accounts. Each account address on //! Concordium is controlled by one or several credential(s) (real-world //! identities) and each credential has one or several public-private key @@ -63,7 +41,40 @@ //! (that have exactly one credential and exactly one public key for that //! credential), the signaturesMaps/publicKeyMaps in this contract, will have //! only one value at key 0 in the inner and outer maps. - +//! +//! ## `Transfer` and `updateOperator` functions: +//! As follows from the CIS2 specification, the contract has a `transfer` +//! function for transferring an amount of a specific token type from one +//! address to another address. An address can enable and disable one or more +//! addresses as operators with the `updateOperator` function. An operator of +//! an address is allowed to transfer any tokens owned by this address. +//! As follows from the CIS3 specification, the contract has a `permit` +//! function. It is the sponsored counterpart to the `transfer/updateOperator` +//! function and can be executed by anyone on behalf of an account given a +//! signed message. +//! +//! ## `Mint` und `burn` functions: +//! In this example, the contract is initialized with no tokens, and tokens can +//! be minted through a `mint` contract function, which can be called by anyone +//! (unprotected function). The `mint` function airdrops the `MINT_AIRDROP` +//! amount of tokens to a specified `owner` address in the input parameter. +//! ATTENTION: You most likley want to add your custom access control mechanism +//! to the `mint` function. +//! +//! A token owner (or any of its operator addresses) can burn some of the token +//! owner's tokens by invoking the `burn` function. +//! +//! ## `onReceivingCIS2` functions: +//! This contract also contains an example of a function to be called when +//! receiving tokens. In which case the contract will forward the tokens to +//! the contract owner. +//! This function is not very useful and is only there to showcase a simple +//! implementation of a token receive hook. +//! +//! ## Blacklist: +//! This contract includes a blacklist. The account with `BLACKLISTER` role can +//! add/remove addresses to/from that list. Blacklisted addresses can not +//! transfer their tokens, receive new tokens, or burn their tokens. #![cfg_attr(not(feature = "std"), no_std)] use concordium_cis2::*; use concordium_std::{collections::BTreeMap, EntrypointName, *}; @@ -375,6 +386,8 @@ pub enum CustomContractError { WrongEntryPoint, // -12 /// Failed signature verification: Signature is expired. Expired, // -13 + /// Token owner address is blacklisted. + Blacklisted, // -14 } pub type ContractError = Cis2Error; @@ -677,6 +690,11 @@ fn contract_mint( // Parse the parameter. let params: MintParams = ctx.parameter_cursor().get()?; + let is_blacklisted = host.state().blacklist.contains(¶ms.owner); + + // Check token owner is not blacklisted. + ensure!(!is_blacklisted, CustomContractError::Blacklisted.into()); + let (state, builder) = host.state_and_builder(); // Mint the token in the state. let token_metadata = state.mint( @@ -737,6 +755,11 @@ fn contract_burn( ContractError::Unauthorized ); + let is_blacklisted = host.state().blacklist.contains(¶ms.owner); + + // Check token owner is not blacklisted. + ensure!(!is_blacklisted, CustomContractError::Blacklisted.into()); + // Burn the token in the state. host.state_mut().burn(¶ms.token_id, params.amount, ¶ms.owner)?; @@ -760,9 +783,19 @@ fn transfer( host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { + let to_address = transfer.to.address(); + + // Check token receiver is not blacklisted. + ensure!(!host.state().blacklist.contains(&to_address), CustomContractError::Blacklisted.into()); + + // Check token owner is not blacklisted. + ensure!( + !host.state().blacklist.contains(&transfer.from), + CustomContractError::Blacklisted.into() + ); + let (state, builder) = host.state_and_builder(); - let to_address = transfer.to.address(); // Update the contract state state.transfer(&transfer.token_id, transfer.amount, &transfer.from, &to_address, builder)?; diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs index 04d0041e1..4ea3278f1 100644 --- a/examples/cis2-multi/tests/tests.rs +++ b/examples/cis2-multi/tests/tests.rs @@ -774,6 +774,151 @@ fn test_adding_to_blacklist() { assert_eq!(rv, [false, false]); } +/// Test blacklisted address cannot receive tokens, send tokens, or burn tokens. +#[test] +fn test_token_balance_of_blacklisted_address_can_not_change() { + let (mut chain, _keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Send some tokens to Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU64(1), + data: AdditionalData::empty(), + }]); + + let _update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect("Transfer tokens"); + + // Create input parameters to add Bob to the blacklist. + let update_blacklist = UpdateBlacklist { + update: BlacklistUpdate::Add, + address: BOB_ADDR, + }; + let update_blacklist_params = UpdateBlacklistParams(vec![update_blacklist]); + + // Update blacklist with BOB's address. + let _update = chain + .contract_update( + Signer::with_one_key(), + ALICE, + ALICE_ADDR, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi.updateBlacklist".to_string(), + ), + message: OwnedParameter::from_serial(&update_blacklist_params) + .expect("Update blacklist params"), + }, + ) + .expect("Should be able to update blacklist"); + + // Bob cannot receive tokens via `transfer`. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU64(1), + data: AdditionalData::empty(), + }]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect_err("Transfer tokens"); + + // Check that the correct error is returned. + let rv: ContractError = update.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, CustomContractError::Blacklisted.into()); + + // Bob cannot transfer tokens via `transfer`. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: BOB_ADDR, + to: Receiver::Account(ALICE), + token_id: TOKEN_0, + amount: TokenAmountU64(1), + data: AdditionalData::empty(), + }]); + + let update = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect_err("Transfer tokens"); + + // Check that the correct error is returned. + let rv: ContractError = update.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, CustomContractError::Blacklisted.into()); + + // Bob cannot mint tokens to its address. + let mint_params = MintParams { + owner: BOB_ADDR, + token_id: TOKEN_0, + metadata_url: MetadataUrl { + url: "https://some.example/token/02".to_string(), + hash: None, + }, + }; + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.mint".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&mint_params).expect("Mint params"), + }) + .expect_err("Mint tokens"); + + // Check that the correct error is returned. + let rv: ContractError = update.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, CustomContractError::Blacklisted.into()); + + // Bob cannot burn tokens from its address. + let burn_params = BurnParams { + owner: BOB_ADDR, + amount: TokenAmountU64(1), + token_id: TOKEN_1, + }; + + // Burn one of Alice's tokens. + let update = chain + .contract_update( + Signer::with_one_key(), + BOB, + BOB_ADDR, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.burn".to_string()), + message: OwnedParameter::from_serial(&burn_params) + .expect("Should be a valid inut parameter"), + }, + ) + .expect_err("Burn tokens"); + + // Check that the correct error is returned. + let rv: ContractError = update.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, CustomContractError::Blacklisted.into()); +} + /// Check if Bob is an operator of Alice. fn operator_of(chain: &Chain, contract_address: ContractAddress) -> OperatorOfQueryResponse { let operator_of_params = OperatorOfQueryParams { From d6121e61090a99acfed1aea4163e620cf37cc310 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 9 Jan 2024 18:04:30 +0100 Subject: [PATCH 05/13] Add logic to convert to canonical address --- examples/cis2-multi/src/lib.rs | 39 +++++++++++++++++++++++------- examples/cis2-multi/tests/tests.rs | 8 ++++-- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index b3510c954..d6c74dd14 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -388,6 +388,8 @@ pub enum CustomContractError { Expired, // -13 /// Token owner address is blacklisted. Blacklisted, // -14 + /// Account address has no canonical address. + NoCanonicalAddress, // -15 } pub type ContractError = Cis2Error; @@ -690,7 +692,7 @@ fn contract_mint( // Parse the parameter. let params: MintParams = ctx.parameter_cursor().get()?; - let is_blacklisted = host.state().blacklist.contains(¶ms.owner); + let is_blacklisted = host.state().blacklist.contains(&get_canonical_address(params.owner)?); // Check token owner is not blacklisted. ensure!(!is_blacklisted, CustomContractError::Blacklisted.into()); @@ -755,7 +757,7 @@ fn contract_burn( ContractError::Unauthorized ); - let is_blacklisted = host.state().blacklist.contains(¶ms.owner); + let is_blacklisted = host.state().blacklist.contains(&get_canonical_address(params.owner)?); // Check token owner is not blacklisted. ensure!(!is_blacklisted, CustomContractError::Blacklisted.into()); @@ -786,11 +788,14 @@ fn transfer( let to_address = transfer.to.address(); // Check token receiver is not blacklisted. - ensure!(!host.state().blacklist.contains(&to_address), CustomContractError::Blacklisted.into()); + ensure!( + !host.state().blacklist.contains(&get_canonical_address(to_address)?), + CustomContractError::Blacklisted.into() + ); // Check token owner is not blacklisted. ensure!( - !host.state().blacklist.contains(&transfer.from), + !host.state().blacklist.contains(&get_canonical_address(transfer.from)?), CustomContractError::Blacklisted.into() ); @@ -1194,7 +1199,7 @@ fn contract_is_blacklisted(ctx: &ReceiveContext, host: &Host) -> Contract let mut response = Vec::with_capacity(params.queries.len()); for address in params.queries { // Query the state if address is blacklisted. - let is_blacklisted = host.state().blacklist.contains(&address); + let is_blacklisted = host.state().blacklist.contains(&get_canonical_address(address)?); response.push(is_blacklisted); } Ok(response) @@ -1516,18 +1521,34 @@ fn contract_update_blacklist( let UpdateBlacklistParams(params) = ctx.parameter_cursor().get()?; for param in params { + let canonical_address = get_canonical_address(param.address)?; + // Add/remove address from the blacklist. match param.update { - BlacklistUpdate::Add => host.state_mut().add_blacklist(param.address), - BlacklistUpdate::Remove => host.state_mut().remove_blacklist(¶m.address), + BlacklistUpdate::Add => host.state_mut().add_blacklist(canonical_address), + BlacklistUpdate::Remove => host.state_mut().remove_blacklist(&canonical_address), } - // Log the nonce event. + // Log the update blacklist event. logger.log(&Event::UpdateBlacklist(UpdateBlacklistEvent { - address: param.address, + address: canonical_address, update: param.update, }))?; } Ok(()) } + +/// Convert the address into its canonical account address (in case it is an +/// account address). Account addresses on Concordium have account aliases. We +/// call the alias 0 for every account the canonical account address. +/// https://developer.concordium.software/en/mainnet/net/references/transactions.html#account-aliases +fn get_canonical_address(address: Address) -> ContractResult
{ + let canonical_address = match address { + Address::Account(account) => { + Address::Account(account.get_alias(0).ok_or(CustomContractError::NoCanonicalAddress)?) + } + Address::Contract(contract) => Address::Contract(contract), + }; + Ok(canonical_address) +} diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs index 4ea3278f1..f084019b5 100644 --- a/examples/cis2-multi/tests/tests.rs +++ b/examples/cis2-multi/tests/tests.rs @@ -11,6 +11,10 @@ use concordium_std::{ const ALICE: AccountAddress = AccountAddress([0; 32]); const ALICE_ADDR: Address = Address::Account(ALICE); const BOB: AccountAddress = AccountAddress([1; 32]); +const BOB_CANONICAL: AccountAddress = AccountAddress([ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, +]); +const BOB_CANONICAL_ADDR: Address = Address::Account(BOB_CANONICAL); const BOB_ADDR: Address = Address::Account(BOB); const NON_EXISTING_ACCOUNT: AccountAddress = AccountAddress([2u8; 32]); @@ -724,7 +728,7 @@ fn test_adding_to_blacklist() { .collect::>(); assert_eq!(events, [Event::UpdateBlacklist(UpdateBlacklistEvent { - address: BOB_ADDR, + address: BOB_CANONICAL_ADDR, update: BlacklistUpdate::Add, })]); @@ -765,7 +769,7 @@ fn test_adding_to_blacklist() { .collect::>(); assert_eq!(events, [Event::UpdateBlacklist(UpdateBlacklistEvent { - address: BOB_ADDR, + address: BOB_CANONICAL_ADDR, update: BlacklistUpdate::Remove, })]); From 06fc6a36a68f87aad81ebef279532d05797997ec Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Wed, 10 Jan 2024 11:01:30 +0100 Subject: [PATCH 06/13] Add upgradability --- examples/cis2-multi/src/lib.rs | 132 ++++++++++++++++++++++++++--- examples/cis2-multi/tests/tests.rs | 98 +++++++++++++++++---- 2 files changed, 201 insertions(+), 29 deletions(-) diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index d6c74dd14..101652e93 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -75,6 +75,34 @@ //! This contract includes a blacklist. The account with `BLACKLISTER` role can //! add/remove addresses to/from that list. Blacklisted addresses can not //! transfer their tokens, receive new tokens, or burn their tokens. +//! +//! ## Native upgradability: +//! This contract can be upgraded. The contract +//! has a function to upgrade the smart contract instance to a new module and +//! call optionally a migration function after the upgrade. To use this example, +//! deploy `contract-version1` and then upgrade the smart contract instance to +//! `contract-version2` by invoking the `upgrade` function with the below JSON +//! inputParameter: +//! +//! ```json +//! { +//! "migrate": { +//! "Some": [ +//! [ +//! "migration", +//! "" +//! ] +//! ] +//! }, +//! "module": "" +//! } +//! ``` +//! +//! This initial contract (`contract-version1`) has no migration function. +//! The next version of the contract (`contract-version2`), could have a +//! migration function added to e.g. change the shape of the smart contract +//! state from `contract-version1` to `contract-version2`. +//! https://github.com/Concordium/concordium-rust-smart-contracts/blob/main/examples/smart-contract-upgrade/contract-version2/src/lib.rs #![cfg_attr(not(feature = "std"), no_std)] use concordium_cis2::*; use concordium_std::{collections::BTreeMap, EntrypointName, *}; @@ -294,6 +322,8 @@ struct State { /// Set of addresses that are not allowed to receive new tokens, sent /// their tokens, or burn their tokens. blacklist: StateSet, + // TODO: replace with role based access. + owner: Address, } /// The parameter type for the contract function `supportsPermit`. @@ -390,12 +420,32 @@ pub enum CustomContractError { Blacklisted, // -14 /// Account address has no canonical address. NoCanonicalAddress, // -15 + /// Upgrade failed because the new module does not exist. + FailedUpgradeMissingModule, // -16 + /// Upgrade failed because the new module does not contain a contract with a + /// matching name. + FailedUpgradeMissingContract, // -17 + /// Upgrade failed because the smart contract version of the module is not + /// supported. + FailedUpgradeUnsupportedModuleVersion, // -18 } pub type ContractError = Cis2Error; pub type ContractResult = Result; +/// Mapping errors related to contract upgrades to CustomContractError. +impl From for CustomContractError { + #[inline(always)] + fn from(ue: UpgradeError) -> Self { + match ue { + UpgradeError::MissingModule => Self::FailedUpgradeMissingModule, + UpgradeError::MissingContract => Self::FailedUpgradeMissingContract, + UpgradeError::UnsupportedModuleVersion => Self::FailedUpgradeUnsupportedModuleVersion, + } + } +} + /// Mapping account signature error to CustomContractError impl From for CustomContractError { fn from(e: CheckAccountSignatureError) -> Self { @@ -436,6 +486,7 @@ impl State { nonces_registry: state_builder.new_map(), mint_airdrop, blacklist: state_builder.new_set(), + owner: Address::Account(AccountAddress([0; 32])), } } @@ -603,6 +654,20 @@ impl State { } } +/// Convert the address into its canonical account address (in case it is an +/// account address). Account addresses on Concordium have account aliases. We +/// call the alias 0 for every account the canonical account address. +/// https://developer.concordium.software/en/mainnet/net/references/transactions.html#account-aliases +fn get_canonical_address(address: Address) -> ContractResult
{ + let canonical_address = match address { + Address::Account(account) => { + Address::Account(account.get_alias(0).ok_or(CustomContractError::NoCanonicalAddress)?) + } + Address::Contract(contract) => Address::Contract(contract), + }; + Ok(canonical_address) +} + // Contract functions /// Initialize contract instance with no token types. @@ -1539,16 +1604,59 @@ fn contract_update_blacklist( Ok(()) } -/// Convert the address into its canonical account address (in case it is an -/// account address). Account addresses on Concordium have account aliases. We -/// call the alias 0 for every account the canonical account address. -/// https://developer.concordium.software/en/mainnet/net/references/transactions.html#account-aliases -fn get_canonical_address(address: Address) -> ContractResult
{ - let canonical_address = match address { - Address::Account(account) => { - Address::Account(account.get_alias(0).ok_or(CustomContractError::NoCanonicalAddress)?) - } - Address::Contract(contract) => Address::Contract(contract), - }; - Ok(canonical_address) +/// The parameter type for the contract function `upgrade`. +/// Takes the new module and optionally an entrypoint to call in the new module +/// after triggering the upgrade. The upgrade is reverted if the entrypoint +/// fails. This is useful for doing migration in the same transaction triggering +/// the upgrade. +#[derive(Serialize, SchemaType)] +pub struct UpgradeParams { + /// The new module reference. + pub module: ModuleReference, + /// Optional entrypoint to call in the new module after upgrade. + pub migrate: Option<(OwnedEntrypointName, OwnedParameter)>, +} + +/// Upgrade this smart contract instance to a new module and call optionally a +/// migration function after the upgrade. +/// +/// It rejects if: +/// - Sender has not the role 'UPGRADER'. +/// - It fails to parse the parameter. +/// - If the ugrade fails. +/// - If the migration invoke fails. +/// +/// This function is marked as `low_level`. This is **necessary** since the +/// high-level mutable functions store the state of the contract at the end of +/// execution. This conflicts with migration since the shape of the state +/// **might** be changed by the migration function. If the state is then written +/// by this function it would overwrite the state stored by the migration +/// function. +#[receive( + contract = "cis2_multi", + name = "upgrade", + parameter = "UpgradeParams", + error = "CustomContractError", + low_level +)] +fn contract_upgrade(ctx: &ReceiveContext, host: &mut LowLevelHost) -> ContractResult<()> { + // Read the top-level contract state. + let state: State = host.state().read_root()?; + + // Check that only the owner is authorized to upgrade the smart contract. + ensure_eq!(ctx.sender(), state.owner, ContractError::Unauthorized); + // Parse the parameter. + let params: UpgradeParams = ctx.parameter_cursor().get()?; + // Trigger the upgrade. + host.upgrade(params.module)?; + // Call the migration function if provided. + if let Some((func, parameters)) = params.migrate { + host.invoke_contract_raw( + &ctx.self_address(), + parameters.as_parameter(), + func.as_entrypoint_name(), + Amount::zero(), + )?; + } + Ok(()) } diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs index f084019b5..8ab75bfa4 100644 --- a/examples/cis2-multi/tests/tests.rs +++ b/examples/cis2-multi/tests/tests.rs @@ -38,7 +38,8 @@ const DUMMY_SIGNATURE: SignatureEd25519 = SignatureEd25519([ /// the appropriate events are logged. #[test] fn test_minting() { - let (chain, _keypairs, contract_address, update) = initialize_contract_with_alice_tokens(); + let (chain, _keypairs, contract_address, update, _module_reference) = + initialize_contract_with_alice_tokens(); // Invoke the view entrypoint and check that the tokens are owned by Alice. let invoke = chain @@ -83,7 +84,8 @@ fn test_minting() { /// Test regular transfer where sender is the owner. #[test] fn test_account_transfer() { - let (mut chain, _keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + let (mut chain, _keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); // Transfer one token from Alice to Bob. let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { @@ -144,7 +146,8 @@ fn test_account_transfer() { /// Then add Bob as an operator for Alice. #[test] fn test_add_operator() { - let (mut chain, _keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + let (mut chain, _keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); // Add Bob as an operator for Alice. let params = UpdateOperatorParams(vec![UpdateOperator { @@ -200,7 +203,8 @@ fn test_add_operator() { /// himself. #[test] fn test_unauthorized_sender() { - let (mut chain, _keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + let (mut chain, _keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); // Construct a transfer of `TOKEN_0` from Alice to Bob, which will be submitted // by Bob. @@ -230,7 +234,8 @@ fn test_unauthorized_sender() { /// Test that an operator can make a transfer. #[test] fn test_operator_can_transfer() { - let (mut chain, _keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + let (mut chain, _keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); // Add Bob as an operator for Alice. let params = UpdateOperatorParams(vec![UpdateOperator { @@ -291,7 +296,8 @@ fn test_operator_can_transfer() { /// case. ALICE adds BOB as an operator. #[test] fn test_inside_signature_permit_update_operator() { - let (mut chain, keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + let (mut chain, keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); // Check operator in state let bob_is_operator_of_alice = operator_of(&chain, contract_address); @@ -389,7 +395,8 @@ fn test_inside_signature_permit_update_operator() { /// TOKEN_1 is transferred from Alice to Bob. #[test] fn test_inside_signature_permit_transfer() { - let (mut chain, keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + let (mut chain, keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); // Check balances in state. let balance_of_alice_and_bob = get_balances(&chain, contract_address); @@ -493,7 +500,8 @@ fn test_inside_signature_permit_transfer() { /// transaction. We check that the nonce of `NON_EXISTING_ACCOUNT` is 0. #[test] fn test_nonce_of_query() { - let (mut chain, keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + let (mut chain, keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); // To increase the nonce of `ALICE's` account, we invoke the // `update_permit` function with a valid signature from ALICE account. @@ -605,7 +613,8 @@ fn test_nonce_of_query() { /// not exist on chain. #[test] fn test_public_key_of_query() { - let (chain, keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + let (chain, keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); let public_key_of_query_vector = VecOfAccountAddresses { queries: vec![ALICE, NON_EXISTING_ACCOUNT], @@ -642,7 +651,8 @@ fn test_public_key_of_query() { /// Test burning tokens. #[test] fn test_burning_tokens() { - let (mut chain, _keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + let (mut chain, _keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); // Create input parameters to burn one of Alice's tokens. let burn_params = BurnParams { @@ -689,7 +699,8 @@ fn test_burning_tokens() { /// Test adding and removing from blacklist works. #[test] fn test_adding_to_blacklist() { - let (mut chain, _keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + let (mut chain, _keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); // Check that Alice and Bob are not blacklisted. let rv: Vec = is_blacklisted(&chain, contract_address); @@ -781,7 +792,8 @@ fn test_adding_to_blacklist() { /// Test blacklisted address cannot receive tokens, send tokens, or burn tokens. #[test] fn test_token_balance_of_blacklisted_address_can_not_change() { - let (mut chain, _keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + let (mut chain, _keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); // Send some tokens to Bob. let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { @@ -923,6 +935,58 @@ fn test_token_balance_of_blacklisted_address_can_not_change() { assert_eq!(rv, CustomContractError::Blacklisted.into()); } +/// Upgrade the contract to itself without invoking a migration function. +#[test] +fn test_upgrade_without_migration_function() { + let (mut chain, _keypairs, contract_address, _update, module_reference) = + initialize_contract_with_alice_tokens(); + + let input_parameter = UpgradeParams { + module: module_reference, + migrate: None, + }; + + // Upgrade `contract_version1` to `contract_version2`. + let update = chain.contract_update( + Signer::with_one_key(), + ALICE, + ALICE_ADDR, + Energy::from(10000), + UpdateContractPayload { + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.upgrade".into()), + message: OwnedParameter::from_serial(&input_parameter) + .expect("`UpgradeParams` should be a valid inut parameter"), + amount: Amount::from_ccd(0), + }, + ); + + assert!( + !update.expect("Upgrade should succeed").state_changed, + "State should not be changed because no `migration` function was called" + ); + + // Invoke the view entrypoint and check that the state of the contract can be + // read. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + + // Check that the tokens (as set up in the + // `initialize_contract_with_alice_tokens` function) are owned by Alice. + let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); + assert_eq!(rv.tokens[..], [TOKEN_0, TOKEN_1]); + assert_eq!(rv.state, vec![(ALICE_ADDR, ViewAddressState { + balances: vec![(TOKEN_0, 100.into()), (TOKEN_1, 100.into())], + operators: Vec::new(), + })]); +} + /// Check if Bob is an operator of Alice. fn operator_of(chain: &Chain, contract_address: ContractAddress) -> OperatorOfQueryResponse { let operator_of_params = OperatorOfQueryParams { @@ -1004,8 +1068,8 @@ fn get_balances( /// Alice's account is created with keys. /// Hence, Alice's account signature can be checked in the test cases. fn initialize_contract_with_alice_tokens( -) -> (Chain, AccountKeys, ContractAddress, ContractInvokeSuccess) { - let (mut chain, keypairs, contract_address) = initialize_chain_and_contract(); +) -> (Chain, AccountKeys, ContractAddress, ContractInvokeSuccess, ModuleReference) { + let (mut chain, keypairs, contract_address, module_reference) = initialize_chain_and_contract(); let mint_params = MintParams { owner: ALICE_ADDR, @@ -1045,7 +1109,7 @@ fn initialize_contract_with_alice_tokens( }) .expect("Mint tokens"); - (chain, keypairs, contract_address, update) + (chain, keypairs, contract_address, update, module_reference) } /// Setup chain and contract. @@ -1053,7 +1117,7 @@ fn initialize_contract_with_alice_tokens( /// Also creates the two accounts, Alice and Bob. /// /// Alice is the owner of the contract. -fn initialize_chain_and_contract() -> (Chain, AccountKeys, ContractAddress) { +fn initialize_chain_and_contract() -> (Chain, AccountKeys, ContractAddress, ModuleReference) { let mut chain = Chain::new(); let rng = &mut rand::thread_rng(); @@ -1084,5 +1148,5 @@ fn initialize_chain_and_contract() -> (Chain, AccountKeys, ContractAddress) { }) .expect("Initialize contract"); - (chain, keypairs, init.contract_address) + (chain, keypairs, init.contract_address, deployment.module_reference) } From 3820d10929ad7ad0749d16a866053568fe255dc2 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Wed, 10 Jan 2024 19:26:15 +0100 Subject: [PATCH 07/13] Add pausing --- examples/cis2-multi/src/lib.rs | 142 +++++++++++++++++++++++------ examples/cis2-multi/tests/tests.rs | 4 +- 2 files changed, 114 insertions(+), 32 deletions(-) diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index 101652e93..fe317c49a 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -317,13 +317,15 @@ struct State { /// mapping keeps track of the next nonce that needs to be used by the /// account to generate a signature. nonces_registry: StateMap, - /// The amount of tokens airdropped when the mint function is invoked. - mint_airdrop: TokenAmountU64, /// Set of addresses that are not allowed to receive new tokens, sent /// their tokens, or burn their tokens. blacklist: StateSet, + /// The amount of tokens airdropped when the mint function is invoked. + mint_airdrop: ContractTokenAmount, // TODO: replace with role based access. owner: Address, + /// Specifies if the contract is paused. + paused: bool, } /// The parameter type for the contract function `supportsPermit`. @@ -383,6 +385,14 @@ pub struct PermitParamPartial { signer: AccountAddress, } +/// The parameter type for the contract function `setPaused`. +#[derive(Serialize, SchemaType)] +#[repr(transparent)] +struct SetPausedParams { + /// Specifies if contract is paused. + paused: bool, +} + /// The different errors the contract can produce. #[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] pub enum CustomContractError { @@ -428,6 +438,8 @@ pub enum CustomContractError { /// Upgrade failed because the smart contract version of the module is not /// supported. FailedUpgradeUnsupportedModuleVersion, // -18 + /// Contract is paused. + Paused, // -19 } pub type ContractError = Cis2Error; @@ -478,7 +490,7 @@ impl From for ContractError { impl State { /// Construct a state with no tokens - fn empty(state_builder: &mut StateBuilder, mint_airdrop: TokenAmountU64) -> Self { + fn empty(state_builder: &mut StateBuilder, mint_airdrop: ContractTokenAmount) -> Self { State { state: state_builder.new_map(), tokens: state_builder.new_map(), @@ -487,6 +499,7 @@ impl State { mint_airdrop, blacklist: state_builder.new_set(), owner: Address::Account(AccountAddress([0; 32])), + paused: false, } } @@ -499,7 +512,7 @@ impl State { token_id: &ContractTokenId, metadata_url: &MetadataUrl, owner: &Address, - mint_airdrop: TokenAmountU64, + mint_airdrop: ContractTokenAmount, state_builder: &mut StateBuilder, ) -> MetadataUrl { let token_metadata = self.tokens.get(token_id).map(|x| x.to_owned()); @@ -674,11 +687,11 @@ fn get_canonical_address(address: Address) -> ContractResult
{ #[init( contract = "cis2_multi", event = "Cis2Event", - parameter = "TokenAmountU64" + parameter = "ContractTokenAmount" )] fn contract_init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { // Parse the parameter. - let mint_airdrop: TokenAmountU64 = ctx.parameter_cursor().get()?; + let mint_airdrop: ContractTokenAmount = ctx.parameter_cursor().get()?; // Construct the initial contract state. Ok(State::empty(state_builder, mint_airdrop)) @@ -692,8 +705,12 @@ pub struct ViewAddressState { #[derive(Serialize, SchemaType, PartialEq, Eq)] pub struct ViewState { - pub state: Vec<(Address, ViewAddressState)>, - pub tokens: Vec, + pub state: Vec<(Address, ViewAddressState)>, + pub tokens: Vec, + pub nonces_registry: Vec<(AccountAddress, u64)>, + pub blacklist: Vec
, + pub mint_airdrop: ContractTokenAmount, + pub paused: bool, } /// View function for testing. This reports on the entire state of the contract @@ -702,30 +719,36 @@ pub struct ViewState { fn contract_view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { let state = host.state(); - let mut inner_state = Vec::new(); - for (k, a_state) in state.state.iter() { - let mut balances = Vec::new(); - let mut operators = Vec::new(); - for (token_id, amount) in a_state.balances.iter() { - balances.push((*token_id, *amount)); - } - for o in a_state.operators.iter() { - operators.push(*o); - } - - inner_state.push((*k, ViewAddressState { - balances, - operators, - })); - } - let mut tokens = Vec::new(); - for v in state.tokens.iter() { - tokens.push(*v.0); - } + let contract_state = state + .state + .iter() + .map(|(key, value)| { + let mut balances = Vec::new(); + let mut operators = Vec::new(); + for (token_id, amount) in value.balances.iter() { + balances.push((*token_id, *amount)); + } + for operator in value.operators.iter() { + operators.push(*operator); + } + (*key, ViewAddressState { + balances, + operators, + }) + }) + .collect(); + + let tokens = state.tokens.iter().map(|a| *a.0).collect(); + let nonces_registry = state.nonces_registry.iter().map(|(a, b)| (*a, *b)).collect(); + let blacklist = state.blacklist.iter().map(|a| *a).collect(); Ok(ViewState { - state: inner_state, + state: contract_state, tokens, + nonces_registry, + blacklist, + mint_airdrop: host.state().mint_airdrop, + paused: host.state().paused, }) } @@ -762,6 +785,9 @@ fn contract_mint( // Check token owner is not blacklisted. ensure!(!is_blacklisted, CustomContractError::Blacklisted.into()); + // Check that contract is not paused. + ensure!(!host.state().paused, CustomContractError::Paused.into()); + let (state, builder) = host.state_and_builder(); // Mint the token in the state. let token_metadata = state.mint( @@ -822,6 +848,9 @@ fn contract_burn( ContractError::Unauthorized ); + // Check that contract is not paused. + ensure!(!host.state().paused, CustomContractError::Paused.into()); + let is_blacklisted = host.state().blacklist.contains(&get_canonical_address(params.owner)?); // Check token owner is not blacklisted. @@ -864,6 +893,9 @@ fn transfer( CustomContractError::Blacklisted.into() ); + // Check that contract is not paused. + ensure!(!host.state().paused, CustomContractError::Paused.into()); + let (state, builder) = host.state_and_builder(); // Update the contract state @@ -1089,6 +1121,7 @@ fn contract_permit( for update in updates { update_operator( + state.paused, update.update, concordium_std::Address::Account(param.signer), update.operator, @@ -1115,6 +1148,7 @@ fn contract_permit( /// Logs a `UpdateOperator` event. The function assumes that the sender is /// authorized to do the `updateOperator` action. fn update_operator( + paused: bool, update: OperatorUpdate, sender: Address, operator: Address, @@ -1122,6 +1156,9 @@ fn update_operator( builder: &mut StateBuilder, logger: &mut impl HasLogger, ) -> ContractResult<()> { + // Check that contract is not paused. + ensure!(!paused, CustomContractError::Paused.into()); + // Update the operator in the state. match update { OperatorUpdate::Add => state.add_operator(&sender, &operator, builder), @@ -1165,7 +1202,15 @@ fn contract_update_operator( let sender = ctx.sender(); let (state, builder) = host.state_and_builder(); for param in params { - update_operator(param.update, sender, param.operator, state, builder, logger)?; + update_operator( + state.paused, + param.update, + sender, + param.operator, + state, + builder, + logger, + )?; } Ok(()) } @@ -1660,3 +1705,40 @@ fn contract_upgrade(ctx: &ReceiveContext, host: &mut LowLevelHost) -> ContractRe } Ok(()) } + +/// Pause/Unpause this smart contract instance by the PAUSER. The transfer, +/// updateOperator, mint, and burn functions cannot be +/// executed when the contract is paused. +/// +/// It rejects if: +/// - Sender is not the PAUSER of the contract instance. +/// - It fails to parse the parameter. +#[receive( + contract = "cis2_multi", + name = "setPaused", + parameter = "SetPausedParams", + error = "CustomContractError", + mutable +)] +fn contract_set_paused( + ctx: &impl HasReceiveContext, + host: &mut impl HasHost, StateApiType = S>, +) -> ContractResult<()> { + // Get the sender who invoked this contract function. + // let sender = ctx.sender(); + + // TODO add pauser roll + // Check that only the PAUSER is authorized to pause the contract. + // ensure!( + // host.state().has_role(&sender, Roles::PAUSER), + // ContractError::Unauthorized + // ); + + // Parse the parameter. + let params: SetPausedParams = ctx.parameter_cursor().get()?; + + // Update the paused variable. + host.state_mut().paused = params.paused; + + Ok(()) +} diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs index 8ab75bfa4..7fb6dfeb7 100644 --- a/examples/cis2-multi/tests/tests.rs +++ b/examples/cis2-multi/tests/tests.rs @@ -183,7 +183,7 @@ fn test_add_operator() { }], }; - // Invoke the operatorOf view entrypoint and check that Bob is an operator for + // Invoke the operatorOf entrypoint and check that Bob is an operator for // Alice. let invoke = chain .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { @@ -192,7 +192,7 @@ fn test_add_operator() { address: contract_address, message: OwnedParameter::from_serial(&query_params).expect("OperatorOf params"), }) - .expect("Invoke view"); + .expect("Invoke opeatorOF"); let rv: OperatorOfQueryResponse = invoke.parse_return_value().expect("OperatorOf return value"); assert_eq!(rv, OperatorOfQueryResponse(vec![true])); From 80fce4d384c925e735c654ed7d8ef58017c67829 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 11 Jan 2024 16:43:23 +0100 Subject: [PATCH 08/13] Add roles --- examples/cis2-multi/src/lib.rs | 240 ++++++++++++++++++++++++++--- examples/cis2-multi/tests/tests.rs | 63 ++++++-- 2 files changed, 270 insertions(+), 33 deletions(-) diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index fe317c49a..600cb2e92 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -118,6 +118,8 @@ const SUPPORTS_PERMIT_ENTRYPOINTS: [EntrypointName; 2] = /// Event tags. pub const NONCE_EVENT_TAG: u8 = u8::MAX - 5; pub const UPDATE_BLACKLIST_EVENT_TAG: u8 = u8::MAX - 6; +pub const GRANT_ROLE_EVENT_TAG: u8 = u8::MAX - 7; +pub const REVOKE_ROLE_EVENT_TAG: u8 = u8::MAX - 8; /// Tagged events to be serialized for the event log. #[derive(Debug, Serial, Deserial, PartialEq, Eq)] @@ -131,6 +133,10 @@ pub enum Event { /// blacklist. #[concordium(tag = 249)] UpdateBlacklist(UpdateBlacklistEvent), + /// The event tracks when a new role is granted to an address. + GrantRole(GrantRoleEvent), + /// The event tracks when a role is removed from an address. + RevokeRole(RevokeRoleEvent), /// Cis2 token events. #[concordium(forward = cis2_events)] Cis2Event(Cis2Event), @@ -156,6 +162,24 @@ pub struct UpdateBlacklistEvent { pub address: Address, } +/// The GrantRoleEvent is logged when a new role is granted to an address. +#[derive(Serialize, SchemaType, Debug, PartialEq, Eq)] +pub struct GrantRoleEvent { + /// The address that has been its role granted. + address: Address, + /// The role that was granted to the above address. + role: Roles, +} + +/// The RevokeRoleEvent is logged when a role is removed from an address. +#[derive(Serialize, SchemaType, Debug, PartialEq, Eq)] +pub struct RevokeRoleEvent { + /// Address that has been its role revoked. + address: Address, + /// The role that was revoked from the above address. + role: Roles, +} + // Implementing a custom schemaType for the `Event` struct containing all // CIS2/CIS3 events. This custom implementation flattens the fields to avoid one // level of nesting. Deriving the schemaType would result in e.g.: {"Nonce": @@ -277,6 +301,47 @@ pub struct BurnParams { pub token_id: ContractTokenId, } +/// The parameter for the contract function `grantRole` which grants a role to +/// an address. +#[derive(Serialize, SchemaType)] +pub struct GrantRoleParams { + /// The address that has been its role granted. + pub address: Address, + /// The role that has been granted to the above address. + pub role: Roles, +} + +/// The parameter for the contract function `removeRole` which revokes a role +/// from an address. +#[derive(Serialize, SchemaType)] +pub struct RemoveRoleParams { + /// The address that has been its role revoked. + pub address: Address, + /// The role that has been revoked from the above address. + pub role: Roles, +} + +/// A struct containing a set of roles granted to an address. +#[derive(Serial, DeserialWithState, Deletable)] +#[concordium(state_parameter = "S")] +struct AddressRoleState { + /// Set of roles. + roles: StateSet, +} + +/// Enum of available roles in this contract. +#[derive(Serialize, PartialEq, Eq, Reject, SchemaType, Clone, Copy, Debug)] +pub enum Roles { + /// Admin role. + ADMIN, + /// Upgrader role. + UPGRADER, + /// Blacklister role. + BLACKLISTER, + /// Pauser role. + PAUSER, +} + /// The state for each address. #[derive(Serial, DeserialWithState, Deletable)] #[concordium(state_parameter = "S")] @@ -307,7 +372,7 @@ struct State { state: StateMap, S>, /// All of the token IDs. tokens: StateMap, - /// Map with contract addresses providing implementations of additional + /// A map with contract addresses providing implementations of additional /// standards. implementors: StateMap, S>, /// A registry to link an account to its next nonce. The nonce is used to @@ -326,6 +391,8 @@ struct State { owner: Address, /// Specifies if the contract is paused. paused: bool, + /// A map containing all roles granted to addresses. + roles: StateMap, S>, } /// The parameter type for the contract function `supportsPermit`. @@ -440,6 +507,10 @@ pub enum CustomContractError { FailedUpgradeUnsupportedModuleVersion, // -18 /// Contract is paused. Paused, // -19 + /// Failed to remove role because it was not granted in the first place. + RoleWasNotGranted, // -20 + /// Failed to grant role because it was granted already in the first place. + RoleWasAlreadyGranted, // -21 } pub type ContractError = Cis2Error; @@ -500,6 +571,7 @@ impl State { blacklist: state_builder.new_set(), owner: Address::Account(AccountAddress([0; 32])), paused: false, + roles: state_builder.new_map(), } } @@ -665,6 +737,32 @@ impl State { ) { self.implementors.insert(std_id, implementors); } + + /// Grant role to an address. + fn grant_role(&mut self, account: &Address, role: Roles, state_builder: &mut StateBuilder) { + self.roles.entry(*account).or_insert_with(|| AddressRoleState { + roles: state_builder.new_set(), + }); + + self.roles.entry(*account).and_modify(|entry| { + entry.roles.insert(role); + }); + } + + /// Remove role from an address. + fn remove_role(&mut self, account: &Address, role: Roles) { + self.roles.entry(*account).and_modify(|entry| { + entry.roles.remove(&role); + }); + } + + /// Check if an address has an role. + fn has_role(&self, account: &Address, role: Roles) -> bool { + return match self.roles.get(account) { + None => false, + Some(roles) => roles.roles.contains(&role), + }; + } } /// Convert the address into its canonical account address (in case it is an @@ -686,15 +784,32 @@ fn get_canonical_address(address: Address) -> ContractResult
{ /// Initialize contract instance with no token types. #[init( contract = "cis2_multi", + parameter = "ContractTokenAmount", event = "Cis2Event", - parameter = "ContractTokenAmount" + enable_logger )] -fn contract_init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { +fn contract_init( + ctx: &InitContext, + state_builder: &mut StateBuilder, + logger: &mut impl HasLogger, +) -> InitResult { // Parse the parameter. let mint_airdrop: ContractTokenAmount = ctx.parameter_cursor().get()?; // Construct the initial contract state. - Ok(State::empty(state_builder, mint_airdrop)) + let mut state = State::empty(state_builder, mint_airdrop); + + // Get the instantiater of this contract instance. + let invoker = Address::Account(ctx.init_origin()); + + // Grant ADMIN role. + state.grant_role(&invoker, Roles::ADMIN, state_builder); + logger.log(&Event::GrantRole(GrantRoleEvent { + address: invoker, + role: Roles::ADMIN, + }))?; + + Ok(state) } #[derive(Serialize, SchemaType, PartialEq, Eq, Debug)] @@ -1608,7 +1723,7 @@ pub struct UpdateBlacklistParams(#[concordium(size_length = 2)] pub Vec, logger: &mut impl HasLogger, ) -> ContractResult<()> { - // Authorize the sender. - ensure!(ctx.sender().matches_account(&ctx.owner()), ContractError::Unauthorized); + // Get the sender who invoked this contract function. + let sender = ctx.sender(); + + // Check that only the BLACKLISTER is authorized to blacklist an address. + ensure!(host.state().has_role(&sender, Roles::BLACKLISTER), ContractError::Unauthorized); // Parse the parameter. let UpdateBlacklistParams(params) = ctx.parameter_cursor().get()?; @@ -1666,7 +1784,7 @@ pub struct UpgradeParams { /// migration function after the upgrade. /// /// It rejects if: -/// - Sender has not the role 'UPGRADER'. +/// - Sender is not the UPGRADER of the contract instance. /// - It fails to parse the parameter. /// - If the ugrade fails. /// - If the migration invoke fails. @@ -1688,8 +1806,11 @@ fn contract_upgrade(ctx: &ReceiveContext, host: &mut LowLevelHost) -> ContractRe // Read the top-level contract state. let state: State = host.state().read_root()?; - // Check that only the owner is authorized to upgrade the smart contract. - ensure_eq!(ctx.sender(), state.owner, ContractError::Unauthorized); + // Get the sender who invoked this contract function. + let sender = ctx.sender(); + + // Check that only the UPGRADER is authorized to upgrade the contract. + ensure!(state.has_role(&sender, Roles::UPGRADER), ContractError::Unauthorized); // Parse the parameter. let params: UpgradeParams = ctx.parameter_cursor().get()?; // Trigger the upgrade. @@ -1720,19 +1841,12 @@ fn contract_upgrade(ctx: &ReceiveContext, host: &mut LowLevelHost) -> ContractRe error = "CustomContractError", mutable )] -fn contract_set_paused( - ctx: &impl HasReceiveContext, - host: &mut impl HasHost, StateApiType = S>, -) -> ContractResult<()> { +fn contract_set_paused(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { // Get the sender who invoked this contract function. - // let sender = ctx.sender(); + let sender = ctx.sender(); - // TODO add pauser roll // Check that only the PAUSER is authorized to pause the contract. - // ensure!( - // host.state().has_role(&sender, Roles::PAUSER), - // ContractError::Unauthorized - // ); + ensure!(host.state().has_role(&sender, Roles::PAUSER), ContractError::Unauthorized); // Parse the parameter. let params: SetPausedParams = ctx.parameter_cursor().get()?; @@ -1742,3 +1856,89 @@ fn contract_set_paused( Ok(()) } + +/// Add role to an address. +/// +/// It rejects if: +/// - It fails to parse the parameter. +/// - Sender is not the ADMIN of the contract instance. +/// - The `address` is already holding the specified role to be granted. +#[receive( + contract = "cis2_multi", + name = "grantRole", + parameter = "GrantRoleParams", + enable_logger, + mutable +)] +fn contract_grant_role( + ctx: &ReceiveContext, + host: &mut Host, + logger: &mut impl HasLogger, +) -> ContractResult<()> { + // Parse the parameter. + let params: GrantRoleParams = ctx.parameter_cursor().get()?; + + let (state, state_builder) = host.state_and_builder(); + + // Get the sender who invoked this contract function. + let sender = ctx.sender(); + // Check that only the ADMIN is authorized to grant roles. + ensure!(state.has_role(&sender, Roles::ADMIN), ContractError::Unauthorized); + + // Check that the `address` had previously not hold the specified role. + ensure!( + !state.has_role(¶ms.address, params.role), + CustomContractError::RoleWasAlreadyGranted.into() + ); + + // Grant role. + state.grant_role(¶ms.address, params.role, state_builder); + logger.log(&Event::GrantRole(GrantRoleEvent { + address: params.address, + role: params.role, + }))?; + Ok(()) +} + +/// Remove role from an address. +/// +/// It rejects if: +/// - It fails to parse the parameter. +/// - Sender is not the ADMIN of the contract instance. +/// - The `address` is not holding the specified role to be removed. +#[receive( + contract = "cis2_multi", + name = "removeRole", + parameter = "RemoveRoleParams", + enable_logger, + mutable +)] +fn contract_remove_role( + ctx: &ReceiveContext, + host: &mut Host, + logger: &mut impl HasLogger, +) -> ContractResult<()> { + // Parse the parameter. + let params: RemoveRoleParams = ctx.parameter_cursor().get()?; + + let (state, _) = host.state_and_builder(); + + // Get the sender who invoked this contract function. + let sender = ctx.sender(); + // Check that only the ADMIN is authorized to revoke roles. + ensure!(state.has_role(&sender, Roles::ADMIN), ContractError::Unauthorized); + + // Check that the `address` had previously hold the specified role. + ensure!( + state.has_role(¶ms.address, params.role), + CustomContractError::RoleWasNotGranted.into() + ); + + // Remove role. + state.remove_role(¶ms.address, params.role); + logger.log(&Event::RevokeRole(RevokeRoleEvent { + address: params.address, + role: params.role, + }))?; + Ok(()) +} diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs index 7fb6dfeb7..f14c10090 100644 --- a/examples/cis2-multi/tests/tests.rs +++ b/examples/cis2-multi/tests/tests.rs @@ -16,7 +16,11 @@ const BOB_CANONICAL: AccountAddress = AccountAddress([ ]); const BOB_CANONICAL_ADDR: Address = Address::Account(BOB_CANONICAL); const BOB_ADDR: Address = Address::Account(BOB); -const NON_EXISTING_ACCOUNT: AccountAddress = AccountAddress([2u8; 32]); +const UPGRADER: AccountAddress = AccountAddress([2; 32]); +const UPGRADER_ADDR: Address = Address::Account(UPGRADER); +const BLACKLISTER: AccountAddress = AccountAddress([3; 32]); +const BLACKLISTER_ADDR: Address = Address::Account(BLACKLISTER); +const NON_EXISTING_ACCOUNT: AccountAddress = AccountAddress([99u8; 32]); /// Token IDs. const TOKEN_0: ContractTokenId = TokenIdU8(2); @@ -717,8 +721,8 @@ fn test_adding_to_blacklist() { let update = chain .contract_update( Signer::with_one_key(), - ALICE, - ALICE_ADDR, + BLACKLISTER, + BLACKLISTER_ADDR, Energy::from(10000), UpdateContractPayload { amount: Amount::zero(), @@ -758,8 +762,8 @@ fn test_adding_to_blacklist() { let update = chain .contract_update( Signer::with_one_key(), - ALICE, - ALICE_ADDR, + BLACKLISTER, + BLACKLISTER_ADDR, Energy::from(10000), UpdateContractPayload { amount: Amount::zero(), @@ -824,8 +828,8 @@ fn test_token_balance_of_blacklisted_address_can_not_change() { let _update = chain .contract_update( Signer::with_one_key(), - ALICE, - ALICE_ADDR, + BLACKLISTER, + BLACKLISTER_ADDR, Energy::from(10000), UpdateContractPayload { amount: Amount::zero(), @@ -949,8 +953,8 @@ fn test_upgrade_without_migration_function() { // Upgrade `contract_version1` to `contract_version2`. let update = chain.contract_update( Signer::with_one_key(), - ALICE, - ALICE_ADDR, + UPGRADER, + UPGRADER_ADDR, Energy::from(10000), UpdateContractPayload { address: contract_address, @@ -1113,10 +1117,9 @@ fn initialize_contract_with_alice_tokens( } /// Setup chain and contract. -/// -/// Also creates the two accounts, Alice and Bob. -/// -/// Alice is the owner of the contract. +/// The function creates the four accounts: ALICE, BOB, UPGRADER, and +/// BLACKLISTER. The function grants ALICE the ADMIN role, the UPGRADER the +/// UPGRADE role, and the BLACKLISTER the BLACKLISTE role. fn initialize_chain_and_contract() -> (Chain, AccountKeys, ContractAddress, ModuleReference) { let mut chain = Chain::new(); @@ -1133,6 +1136,8 @@ fn initialize_chain_and_contract() -> (Chain, AccountKeys, ContractAddress, Modu // Create some accounts on the chain. chain.create_account(Account::new_with_keys(ALICE, balance, (&keypairs).into())); chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(UPGRADER, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(BLACKLISTER, ACC_INITIAL_BALANCE)); // Load and deploy the module. let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); @@ -1148,5 +1153,37 @@ fn initialize_chain_and_contract() -> (Chain, AccountKeys, ContractAddress, Modu }) .expect("Initialize contract"); + // Grant UPGRADER role + let grant_role_params = GrantRoleParams { + address: UPGRADER_ADDR, + role: Roles::UPGRADER, + }; + + let _update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.grantRole".to_string()), + address: init.contract_address, + message: OwnedParameter::from_serial(&grant_role_params) + .expect("GrantRole params"), + }) + .expect("UPGRADER should be granted role"); + + // Grant BLACKLISTER role + let grant_role_params = GrantRoleParams { + address: BLACKLISTER_ADDR, + role: Roles::BLACKLISTER, + }; + + let _update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.grantRole".to_string()), + address: init.contract_address, + message: OwnedParameter::from_serial(&grant_role_params) + .expect("GrantRole params"), + }) + .expect("BLACKLISTER should be granted role"); + (chain, keypairs, init.contract_address, deployment.module_reference) } From 2c069dde2ac030498ed9a3ac8b4431209835b4f8 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 11 Jan 2024 17:33:38 +0100 Subject: [PATCH 09/13] Add mint entrypoint to permit function --- examples/cis2-multi/src/lib.rs | 159 ++++++++++++++++++----------- examples/cis2-multi/tests/tests.rs | 113 +++++++++++++++++++- 2 files changed, 208 insertions(+), 64 deletions(-) diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index 600cb2e92..beb120002 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -59,7 +59,7 @@ //! (unprotected function). The `mint` function airdrops the `MINT_AIRDROP` //! amount of tokens to a specified `owner` address in the input parameter. //! ATTENTION: You most likley want to add your custom access control mechanism -//! to the `mint` function. +//! to the `mint` function and `permit` function. //! //! A token owner (or any of its operator addresses) can burn some of the token //! owner's tokens by invoking the `burn` function. @@ -121,6 +121,12 @@ pub const UPDATE_BLACKLIST_EVENT_TAG: u8 = u8::MAX - 6; pub const GRANT_ROLE_EVENT_TAG: u8 = u8::MAX - 7; pub const REVOKE_ROLE_EVENT_TAG: u8 = u8::MAX - 8; +const TRANSFER_ENTRYPOINT: EntrypointName<'_> = EntrypointName::new_unchecked("transfer"); +const UPDATE_OPERATOR_ENTRYPOINT: EntrypointName<'_> = + EntrypointName::new_unchecked("updateOperator"); +const MINT_ENTRYPOINT: EntrypointName<'_> = EntrypointName::new_unchecked("mint"); +const BURN_ENTRYPOINT: EntrypointName<'_> = EntrypointName::new_unchecked("burn"); + /// Tagged events to be serialized for the event log. #[derive(Debug, Serial, Deserial, PartialEq, Eq)] #[concordium(repr(u8))] @@ -867,9 +873,53 @@ fn contract_view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult, + logger: &mut impl HasLogger, +) -> ContractResult<()> { + let is_blacklisted = + host.state().blacklist.contains(&get_canonical_address(mint_params.owner)?); + + // Check token owner is not blacklisted. + ensure!(!is_blacklisted, CustomContractError::Blacklisted.into()); + + // Check that contract is not paused. + ensure!(!host.state().paused, CustomContractError::Paused.into()); + + let (state, builder) = host.state_and_builder(); + + // Mint the token in the state. + let token_metadata = state.mint( + &mint_params.token_id, + &mint_params.metadata_url, + &mint_params.owner, + state.mint_airdrop, + builder, + ); + + // Event for minted token. + logger.log(&Cis2Event::Mint(MintEvent { + token_id: mint_params.token_id, + amount: state.mint_airdrop, + owner: mint_params.owner, + }))?; + + // Metadata URL for the token. + logger.log(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>(TokenMetadataEvent { + token_id: mint_params.token_id, + metadata_url: token_metadata, + }))?; + + Ok(()) +} + /// Mint/Airdrops the fixed amount of `MINT_AIRDROP` of new tokens to the /// `owner` address. ATTENTION: Can be called by anyone. You should add your -/// custom access control to this function. +/// custom access control to this function and the permit function. /// /// Logs a `Mint` and a `TokenMetadata` event for each token. /// The metadata_url in the input parameter of the token is ignored except for @@ -895,36 +945,8 @@ fn contract_mint( // Parse the parameter. let params: MintParams = ctx.parameter_cursor().get()?; - let is_blacklisted = host.state().blacklist.contains(&get_canonical_address(params.owner)?); - - // Check token owner is not blacklisted. - ensure!(!is_blacklisted, CustomContractError::Blacklisted.into()); - - // Check that contract is not paused. - ensure!(!host.state().paused, CustomContractError::Paused.into()); - - let (state, builder) = host.state_and_builder(); - // Mint the token in the state. - let token_metadata = state.mint( - ¶ms.token_id, - ¶ms.metadata_url, - ¶ms.owner, - state.mint_airdrop, - builder, - ); - - // Event for minted token. - logger.log(&Cis2Event::Mint(MintEvent { - token_id: params.token_id, - amount: state.mint_airdrop, - owner: params.owner, - }))?; + mint(params, host, logger)?; - // Metadata URL for the token. - logger.log(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>(TokenMetadataEvent { - token_id: params.token_id, - metadata_url: token_metadata, - }))?; Ok(()) } @@ -1211,42 +1233,55 @@ fn contract_permit( host.check_account_signature(param.signer, ¶m.signature, &message_hash)?; ensure!(valid_signature, CustomContractError::WrongSignature.into()); - if message.entry_point.as_entrypoint_name() == EntrypointName::new_unchecked("transfer") { - // Transfer the tokens. + match message.entry_point.as_entrypoint_name() { + TRANSFER_ENTRYPOINT => { + // Transfer the tokens. - let TransferParams(transfers): TransferParameter = from_bytes(&message.payload)?; + let TransferParams(transfers): TransferParameter = from_bytes(&message.payload)?; - for transfer_entry in transfers { - // Authenticate the signer for this transfer - ensure!( - transfer_entry.from.matches_account(¶m.signer) - || host.state().is_operator(&Address::from(param.signer), &transfer_entry.from), - ContractError::Unauthorized - ); + for transfer_entry in transfers { + // Authenticate the signer for this transfer + ensure!( + transfer_entry.from.matches_account(¶m.signer) + || host + .state() + .is_operator(&Address::from(param.signer), &transfer_entry.from), + ContractError::Unauthorized + ); - transfer(transfer_entry, host, logger)? + transfer(transfer_entry, host, logger)? + } } - } else if message.entry_point.as_entrypoint_name() - == EntrypointName::new_unchecked("updateOperator") - { - // Update the operator. - let UpdateOperatorParams(updates): UpdateOperatorParams = from_bytes(&message.payload)?; - - let (state, builder) = host.state_and_builder(); - - for update in updates { - update_operator( - state.paused, - update.update, - concordium_std::Address::Account(param.signer), - update.operator, - state, - builder, - logger, - )?; + UPDATE_OPERATOR_ENTRYPOINT => { + // Update the operator. + let UpdateOperatorParams(updates): UpdateOperatorParams = from_bytes(&message.payload)?; + + let (state, builder) = host.state_and_builder(); + + for update in updates { + update_operator( + state.paused, + update.update, + concordium_std::Address::Account(param.signer), + update.operator, + state, + builder, + logger, + )?; + } + } + MINT_ENTRYPOINT => { + // ATTENTION: Can be called by anyone. You should add your + // custom access control to here. + + // Mint tokens + let mint_params: MintParams = from_bytes(&message.payload)?; + + mint(mint_params, host, logger)?; + } + _ => { + bail!(CustomContractError::WrongEntryPoint.into()) } - } else { - bail!(CustomContractError::WrongEntryPoint.into()) } // Log the nonce event. diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs index f14c10090..ea32ffa9b 100644 --- a/examples/cis2-multi/tests/tests.rs +++ b/examples/cis2-multi/tests/tests.rs @@ -296,10 +296,119 @@ fn test_operator_can_transfer() { ]); } +/// Test permit mint function. The signature is generated in the test +/// case. ALICE mints tokens to her account. +#[test] +fn test_permit_mint() { + let (mut chain, keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); + + // Check balances in state. + let balance_of_alice_and_bob = get_balances(&chain, contract_address); + + assert_eq!(balance_of_alice_and_bob.0, [TokenAmountU64(100), TokenAmountU64(0)]); + + // Create input parameters for the `mint` transfer function. + let payload = MintParams { + owner: ALICE_ADDR, + metadata_url: MetadataUrl { + url: "https://some.example/token/2A".to_string(), + hash: None, + }, + token_id: TOKEN_1, + }; + + // The `viewMessageHash` function uses the same input parameter `PermitParam` as + // the `permit` function. The `PermitParam` type includes a `signature` and + // a `signer`. Because these two values (`signature` and `signer`) are not + // read in the `viewMessageHash` function, any value can be used and we choose + // to use `DUMMY_SIGNATURE` and `ALICE` in the test case below. + let signature_map = BTreeMap::from([(0u8, CredentialSignatures { + sigs: BTreeMap::from([(0u8, concordium_std::Signature::Ed25519(DUMMY_SIGNATURE))]), + })]); + + let mut permit_mint_param = PermitParam { + signature: AccountSignatures { + sigs: signature_map, + }, + signer: ALICE, + message: PermitMessage { + timestamp: Timestamp::from_timestamp_millis(10_000_000_000), + contract_address: ContractAddress::new(0, 0), + entry_point: OwnedEntrypointName::new_unchecked("mint".into()), + nonce: 0, + payload: to_bytes(&payload), + }, + }; + + // Get the message hash to be signed. + let invoke = chain + .contract_invoke(BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.viewMessageHash".to_string()), + message: OwnedParameter::from_serial(&permit_mint_param) + .expect("Should be a valid inut parameter"), + }) + .expect("Should be able to query viewMessageHash"); + + let message_hash: HashSha2256 = + from_bytes(&invoke.return_value).expect("Should return a valid result"); + + permit_mint_param.signature = keypairs.sign_message(&to_bytes(&message_hash)); + + // Mint tokens with the permit function. + let update = chain + .contract_update( + Signer::with_one_key(), + BOB, + BOB_ADDR, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.permit".to_string()), + message: OwnedParameter::from_serial(&permit_mint_param) + .expect("Should be a valid inut parameter"), + }, + ) + .expect("Should be able to mint tokens with permit"); + + // Check that the correct events occurred. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>(); + + assert_eq!(events, [ + Event::Cis2Event(Cis2Event::Mint(MintEvent { + token_id: TOKEN_1, + amount: TokenAmountU64(100), + owner: ALICE_ADDR, + })), + Event::Cis2Event(Cis2Event::TokenMetadata(TokenMetadataEvent { + token_id: TOKEN_1, + metadata_url: MetadataUrl { + url: "https://some.example/token/2A".to_string(), + hash: None, + }, + })), + Event::Nonce(NonceEvent { + account: ALICE, + nonce: 0, + }) + ]); + + // Check balances in state. + let balance_of_alice_and_bob = get_balances(&chain, contract_address); + + assert_eq!(balance_of_alice_and_bob.0, [TokenAmountU64(200), TokenAmountU64(0)]); +} + /// Test permit update operator function. The signature is generated in the test /// case. ALICE adds BOB as an operator. #[test] -fn test_inside_signature_permit_update_operator() { +fn test_permit_update_operator() { let (mut chain, keypairs, contract_address, _update, _module_reference) = initialize_contract_with_alice_tokens(); @@ -398,7 +507,7 @@ fn test_inside_signature_permit_update_operator() { /// Test permit transfer function. The signature is generated in the test case. /// TOKEN_1 is transferred from Alice to Bob. #[test] -fn test_inside_signature_permit_transfer() { +fn test_permit_transfer() { let (mut chain, keypairs, contract_address, _update, _module_reference) = initialize_contract_with_alice_tokens(); From 69ce0370fec060f3d0fcd8848d84d86170327890 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 11 Jan 2024 18:02:11 +0100 Subject: [PATCH 10/13] Add burn function to permit entrypoint --- examples/cis2-multi/src/lib.rs | 91 +++++++++++++++++--------- examples/cis2-multi/tests/tests.rs | 101 ++++++++++++++++++++++++++++- 2 files changed, 160 insertions(+), 32 deletions(-) diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index beb120002..153c47e39 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -877,12 +877,11 @@ fn contract_view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult, logger: &mut impl HasLogger, ) -> ContractResult<()> { - let is_blacklisted = - host.state().blacklist.contains(&get_canonical_address(mint_params.owner)?); + let is_blacklisted = host.state().blacklist.contains(&get_canonical_address(params.owner)?); // Check token owner is not blacklisted. ensure!(!is_blacklisted, CustomContractError::Blacklisted.into()); @@ -894,23 +893,23 @@ fn mint( // Mint the token in the state. let token_metadata = state.mint( - &mint_params.token_id, - &mint_params.metadata_url, - &mint_params.owner, + ¶ms.token_id, + ¶ms.metadata_url, + ¶ms.owner, state.mint_airdrop, builder, ); // Event for minted token. logger.log(&Cis2Event::Mint(MintEvent { - token_id: mint_params.token_id, + token_id: params.token_id, amount: state.mint_airdrop, - owner: mint_params.owner, + owner: params.owner, }))?; // Metadata URL for the token. logger.log(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>(TokenMetadataEvent { - token_id: mint_params.token_id, + token_id: params.token_id, metadata_url: token_metadata, }))?; @@ -927,6 +926,8 @@ fn mint( /// /// It rejects if: /// - Fails to parse parameter. +/// - The token receiver is blacklisted. +/// - The contract is paused. /// - Fails to log Mint event. /// - Fails to log TokenMetadata event. #[receive( @@ -950,6 +951,35 @@ fn contract_mint( Ok(()) } +/// Internal `burn/permit` helper function. Invokes the `burn` +/// function of the state. Logs a `Burn` event. +/// The function assumes that the burn is authorized. +fn burn( + params: BurnParams, + host: &mut Host, + logger: &mut impl HasLogger, +) -> ContractResult<()> { + // Check that contract is not paused. + ensure!(!host.state().paused, CustomContractError::Paused.into()); + + let is_blacklisted = host.state().blacklist.contains(&get_canonical_address(params.owner)?); + + // Check token owner is not blacklisted. + ensure!(!is_blacklisted, CustomContractError::Blacklisted.into()); + + // Burn the token in the state. + host.state_mut().burn(¶ms.token_id, params.amount, ¶ms.owner)?; + + // Event for burned tokens. + logger.log(&Cis2Event::Burn(BurnEvent { + token_id: params.token_id, + amount: params.amount, + owner: params.owner, + }))?; + + Ok(()) +} + /// Burns an amount of tokens from the /// `owner` address. /// @@ -958,6 +988,8 @@ fn contract_mint( /// It rejects if: /// - Fails to parse parameter. /// - The sender is not the token owner or an operator of the token owner. +/// - The owner is blacklisted. +/// - The contract is paused. /// - The token owner owns an insufficient token amount to burn from. /// - Fails to log Burn event. #[receive( @@ -985,23 +1017,7 @@ fn contract_burn( ContractError::Unauthorized ); - // Check that contract is not paused. - ensure!(!host.state().paused, CustomContractError::Paused.into()); - - let is_blacklisted = host.state().blacklist.contains(&get_canonical_address(params.owner)?); - - // Check token owner is not blacklisted. - ensure!(!is_blacklisted, CustomContractError::Blacklisted.into()); - - // Burn the token in the state. - host.state_mut().burn(¶ms.token_id, params.amount, ¶ms.owner)?; - - // Event for burned tokens. - logger.log(&Cis2Event::Burn(BurnEvent { - token_id: params.token_id, - amount: params.amount, - owner: params.owner, - }))?; + burn(params, host, logger)?; Ok(()) } @@ -1067,6 +1083,8 @@ fn transfer( /// /// It rejects if: /// - It fails to parse the parameter. +/// - Either the from or to addresses are blacklisted. +/// - The contract is paused. /// - Any of the transfers fail to be executed, which could be if: /// - The `token_id` does not exist. /// - The sender is not the owner of the token, or an operator for this @@ -1236,7 +1254,6 @@ fn contract_permit( match message.entry_point.as_entrypoint_name() { TRANSFER_ENTRYPOINT => { // Transfer the tokens. - let TransferParams(transfers): TransferParameter = from_bytes(&message.payload)?; for transfer_entry in transfers { @@ -1271,13 +1288,25 @@ fn contract_permit( } } MINT_ENTRYPOINT => { + // Mint tokens. // ATTENTION: Can be called by anyone. You should add your // custom access control to here. + let params: MintParams = from_bytes(&message.payload)?; - // Mint tokens - let mint_params: MintParams = from_bytes(&message.payload)?; - - mint(mint_params, host, logger)?; + mint(params, host, logger)?; + } + BURN_ENTRYPOINT => { + // Burn tokens. + let params: BurnParams = from_bytes(&message.payload)?; + + // Authenticate the sender for the token burns. + ensure!( + params.owner.matches_account(¶m.signer) + || host.state().is_operator(&Address::from(param.signer), ¶ms.owner), + ContractError::Unauthorized + ); + + burn(params, host, logger)?; } _ => { bail!(CustomContractError::WrongEntryPoint.into()) diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs index ea32ffa9b..712cbb09a 100644 --- a/examples/cis2-multi/tests/tests.rs +++ b/examples/cis2-multi/tests/tests.rs @@ -308,7 +308,7 @@ fn test_permit_mint() { assert_eq!(balance_of_alice_and_bob.0, [TokenAmountU64(100), TokenAmountU64(0)]); - // Create input parameters for the `mint` transfer function. + // Create input parameters for the `mint` function. let payload = MintParams { owner: ALICE_ADDR, metadata_url: MetadataUrl { @@ -405,6 +405,105 @@ fn test_permit_mint() { assert_eq!(balance_of_alice_and_bob.0, [TokenAmountU64(200), TokenAmountU64(0)]); } +/// Test permit burn function. The signature is generated in the test +/// case. ALICE burns tokens from her account. +#[test] +fn test_permit_burn() { + let (mut chain, keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); + + // Check balances in state. + let balance_of_alice_and_bob = get_balances(&chain, contract_address); + + assert_eq!(balance_of_alice_and_bob.0, [TokenAmountU64(100), TokenAmountU64(0)]); + + // Create input parameters for the `burn` function. + let payload = BurnParams { + owner: ALICE_ADDR, + amount: TokenAmountU64(1), + token_id: TOKEN_1, + }; + + // The `viewMessageHash` function uses the same input parameter `PermitParam` as + // the `permit` function. The `PermitParam` type includes a `signature` and + // a `signer`. Because these two values (`signature` and `signer`) are not + // read in the `viewMessageHash` function, any value can be used and we choose + // to use `DUMMY_SIGNATURE` and `ALICE` in the test case below. + let signature_map = BTreeMap::from([(0u8, CredentialSignatures { + sigs: BTreeMap::from([(0u8, concordium_std::Signature::Ed25519(DUMMY_SIGNATURE))]), + })]); + + let mut permit_burn_param = PermitParam { + signature: AccountSignatures { + sigs: signature_map, + }, + signer: ALICE, + message: PermitMessage { + timestamp: Timestamp::from_timestamp_millis(10_000_000_000), + contract_address: ContractAddress::new(0, 0), + entry_point: OwnedEntrypointName::new_unchecked("burn".into()), + nonce: 0, + payload: to_bytes(&payload), + }, + }; + + // Get the message hash to be signed. + let invoke = chain + .contract_invoke(BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.viewMessageHash".to_string()), + message: OwnedParameter::from_serial(&permit_burn_param) + .expect("Should be a valid inut parameter"), + }) + .expect("Should be able to query viewMessageHash"); + + let message_hash: HashSha2256 = + from_bytes(&invoke.return_value).expect("Should return a valid result"); + + permit_burn_param.signature = keypairs.sign_message(&to_bytes(&message_hash)); + + // Burn tokens with the permit function. + let update = chain + .contract_update( + Signer::with_one_key(), + BOB, + BOB_ADDR, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.permit".to_string()), + message: OwnedParameter::from_serial(&permit_burn_param) + .expect("Should be a valid inut parameter"), + }, + ) + .expect("Should be able to burn tokens with permit"); + + // Check that the correct events occurred. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>(); + + assert_eq!(events, [ + Event::Cis2Event(Cis2Event::Burn(BurnEvent { + token_id: TOKEN_1, + amount: TokenAmountU64(1), + owner: ALICE_ADDR, + })), + Event::Nonce(NonceEvent { + account: ALICE, + nonce: 0, + }) + ]); + + // Check balances in state. + let balance_of_alice_and_bob = get_balances(&chain, contract_address); + + assert_eq!(balance_of_alice_and_bob.0, [TokenAmountU64(99), TokenAmountU64(0)]); +} + /// Test permit update operator function. The signature is generated in the test /// case. ALICE adds BOB as an operator. #[test] From 14320a2ea549a899e3acd7d247db75c708073388 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Fri, 12 Jan 2024 13:44:24 +0100 Subject: [PATCH 11/13] Simplified permit test cases --- concordium-cis2/src/lib.rs | 2 +- examples/cis2-multi/src/lib.rs | 58 +++++- examples/cis2-multi/tests/tests.rs | 298 ++++++++--------------------- 3 files changed, 133 insertions(+), 225 deletions(-) diff --git a/concordium-cis2/src/lib.rs b/concordium-cis2/src/lib.rs index 68188b543..2db2c00b9 100644 --- a/concordium-cis2/src/lib.rs +++ b/concordium-cis2/src/lib.rs @@ -1336,7 +1336,7 @@ impl<'a> StandardIdentifier<'a> { /// Consists of a string of ASCII characters up to a length of 255. /// /// See [StandardIdentifier] for the borrowed version. -#[derive(Debug, Serialize, PartialEq, Eq, SchemaType)] +#[derive(Debug, Serialize, PartialEq, Eq, SchemaType, Clone)] #[concordium(transparent)] pub struct StandardIdentifierOwned { #[concordium(size_length = 1)] diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index 153c47e39..4fa9df04d 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -1,3 +1,6 @@ +//! TODO explain pausing, grant roles, update roles, permit function has added +//! mint/burn TODO add more testcases (grant role and pausing tests) +//! //! A multi token example implementation of the Concordium Token Standard CIS2 //! and the Concordium Sponsored Transaction Standard CIS3. //! @@ -140,8 +143,10 @@ pub enum Event { #[concordium(tag = 249)] UpdateBlacklist(UpdateBlacklistEvent), /// The event tracks when a new role is granted to an address. + #[concordium(tag = 248)] GrantRole(GrantRoleEvent), /// The event tracks when a role is removed from an address. + #[concordium(tag = 247)] RevokeRole(RevokeRoleEvent), /// Cis2 token events. #[concordium(forward = cis2_events)] @@ -204,10 +209,30 @@ impl schema::SchemaType for Event { ]), ), ); + event_map.insert( + GRANT_ROLE_EVENT_TAG, + ( + "GrantRole".to_string(), + schema::Fields::Named(vec![ + (String::from("address"), Address::get_type()), + (String::from("role"), Roles::get_type()), + ]), + ), + ); + event_map.insert( + REVOKE_ROLE_EVENT_TAG, + ( + "RevokeRole".to_string(), + schema::Fields::Named(vec![ + (String::from("address"), Address::get_type()), + (String::from("role"), Roles::get_type()), + ]), + ), + ); event_map.insert( UPDATE_BLACKLIST_EVENT_TAG, ( - "UpdateBlacklistEvent".to_string(), + "UpdateBlacklist".to_string(), schema::Fields::Named(vec![ (String::from("update"), BlacklistUpdate::get_type()), (String::from("address"), Address::get_type()), @@ -393,8 +418,6 @@ struct State { blacklist: StateSet, /// The amount of tokens airdropped when the mint function is invoked. mint_airdrop: ContractTokenAmount, - // TODO: replace with role based access. - owner: Address, /// Specifies if the contract is paused. paused: bool, /// A map containing all roles granted to addresses. @@ -575,7 +598,6 @@ impl State { nonces_registry: state_builder.new_map(), mint_airdrop, blacklist: state_builder.new_set(), - owner: Address::Account(AccountAddress([0; 32])), paused: false, roles: state_builder.new_map(), } @@ -830,8 +852,10 @@ pub struct ViewState { pub tokens: Vec, pub nonces_registry: Vec<(AccountAddress, u64)>, pub blacklist: Vec
, + pub roles: Vec<(Address, Vec)>, pub mint_airdrop: ContractTokenAmount, pub paused: bool, + pub implementors: Vec<(StandardIdentifierOwned, Vec)>, } /// View function for testing. This reports on the entire state of the contract @@ -862,12 +886,38 @@ fn contract_view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult)> = state + .roles + .iter() + .map(|(key, value)| { + let mut roles_vec = Vec::new(); + for role in value.roles.iter() { + roles_vec.push(*role); + } + (*key, roles_vec) + }) + .collect(); + + let implementors: Vec<(StandardIdentifierOwned, Vec)> = state + .implementors + .iter() + .map(|(key, value)| { + let mut implementors = Vec::new(); + for test in value.iter() { + implementors.push(*test); + } + + ((*key).clone(), implementors) + }) + .collect(); Ok(ViewState { state: contract_state, tokens, nonces_registry, blacklist, + roles, + implementors, mint_airdrop: host.state().mint_airdrop, paused: host.state().paused, }) diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs index 712cbb09a..ca326ccbd 100644 --- a/examples/cis2-multi/tests/tests.rs +++ b/examples/cis2-multi/tests/tests.rs @@ -318,61 +318,8 @@ fn test_permit_mint() { token_id: TOKEN_1, }; - // The `viewMessageHash` function uses the same input parameter `PermitParam` as - // the `permit` function. The `PermitParam` type includes a `signature` and - // a `signer`. Because these two values (`signature` and `signer`) are not - // read in the `viewMessageHash` function, any value can be used and we choose - // to use `DUMMY_SIGNATURE` and `ALICE` in the test case below. - let signature_map = BTreeMap::from([(0u8, CredentialSignatures { - sigs: BTreeMap::from([(0u8, concordium_std::Signature::Ed25519(DUMMY_SIGNATURE))]), - })]); - - let mut permit_mint_param = PermitParam { - signature: AccountSignatures { - sigs: signature_map, - }, - signer: ALICE, - message: PermitMessage { - timestamp: Timestamp::from_timestamp_millis(10_000_000_000), - contract_address: ContractAddress::new(0, 0), - entry_point: OwnedEntrypointName::new_unchecked("mint".into()), - nonce: 0, - payload: to_bytes(&payload), - }, - }; - - // Get the message hash to be signed. - let invoke = chain - .contract_invoke(BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.viewMessageHash".to_string()), - message: OwnedParameter::from_serial(&permit_mint_param) - .expect("Should be a valid inut parameter"), - }) - .expect("Should be able to query viewMessageHash"); - - let message_hash: HashSha2256 = - from_bytes(&invoke.return_value).expect("Should return a valid result"); - - permit_mint_param.signature = keypairs.sign_message(&to_bytes(&message_hash)); - - // Mint tokens with the permit function. - let update = chain - .contract_update( - Signer::with_one_key(), - BOB, - BOB_ADDR, - Energy::from(10000), - UpdateContractPayload { - amount: Amount::zero(), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.permit".to_string()), - message: OwnedParameter::from_serial(&permit_mint_param) - .expect("Should be a valid inut parameter"), - }, - ) - .expect("Should be able to mint tokens with permit"); + let update = + permit(&mut chain, contract_address, to_bytes(&payload), "mint".to_string(), keypairs); // Check that the correct events occurred. let events = update @@ -424,61 +371,8 @@ fn test_permit_burn() { token_id: TOKEN_1, }; - // The `viewMessageHash` function uses the same input parameter `PermitParam` as - // the `permit` function. The `PermitParam` type includes a `signature` and - // a `signer`. Because these two values (`signature` and `signer`) are not - // read in the `viewMessageHash` function, any value can be used and we choose - // to use `DUMMY_SIGNATURE` and `ALICE` in the test case below. - let signature_map = BTreeMap::from([(0u8, CredentialSignatures { - sigs: BTreeMap::from([(0u8, concordium_std::Signature::Ed25519(DUMMY_SIGNATURE))]), - })]); - - let mut permit_burn_param = PermitParam { - signature: AccountSignatures { - sigs: signature_map, - }, - signer: ALICE, - message: PermitMessage { - timestamp: Timestamp::from_timestamp_millis(10_000_000_000), - contract_address: ContractAddress::new(0, 0), - entry_point: OwnedEntrypointName::new_unchecked("burn".into()), - nonce: 0, - payload: to_bytes(&payload), - }, - }; - - // Get the message hash to be signed. - let invoke = chain - .contract_invoke(BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.viewMessageHash".to_string()), - message: OwnedParameter::from_serial(&permit_burn_param) - .expect("Should be a valid inut parameter"), - }) - .expect("Should be able to query viewMessageHash"); - - let message_hash: HashSha2256 = - from_bytes(&invoke.return_value).expect("Should return a valid result"); - - permit_burn_param.signature = keypairs.sign_message(&to_bytes(&message_hash)); - - // Burn tokens with the permit function. - let update = chain - .contract_update( - Signer::with_one_key(), - BOB, - BOB_ADDR, - Energy::from(10000), - UpdateContractPayload { - amount: Amount::zero(), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.permit".to_string()), - message: OwnedParameter::from_serial(&permit_burn_param) - .expect("Should be a valid inut parameter"), - }, - ) - .expect("Should be able to burn tokens with permit"); + let update = + permit(&mut chain, contract_address, to_bytes(&payload), "burn".to_string(), keypairs); // Check that the correct events occurred. let events = update @@ -523,61 +417,13 @@ fn test_permit_update_operator() { }; let payload = UpdateOperatorParams(vec![update_operator]); - // The `viewMessageHash` function uses the same input parameter `PermitParam` as - // the `permit` function. The `PermitParam` type includes a `signature` and - // a `signer`. Because these two values (`signature` and `signer`) are not - // read in the `viewMessageHash` function, any value can be used and we choose - // to use `DUMMY_SIGNATURE` and `ALICE` in the test case below. - let signature_map = BTreeMap::from([(0u8, CredentialSignatures { - sigs: BTreeMap::from([(0u8, concordium_std::Signature::Ed25519(DUMMY_SIGNATURE))]), - })]); - - let mut permit_update_operator_param = PermitParam { - signature: AccountSignatures { - sigs: signature_map, - }, - signer: ALICE, - message: PermitMessage { - timestamp: Timestamp::from_timestamp_millis(10_000_000_000), - contract_address: ContractAddress::new(0, 0), - entry_point: OwnedEntrypointName::new_unchecked("updateOperator".into()), - nonce: 0, - payload: to_bytes(&payload), - }, - }; - - // Get the message hash to be signed. - let invoke = chain - .contract_invoke(BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.viewMessageHash".to_string()), - message: OwnedParameter::from_serial(&permit_update_operator_param) - .expect("Should be a valid inut parameter"), - }) - .expect("Should be able to query viewMessageHash"); - - let message_hash: HashSha2256 = - from_bytes(&invoke.return_value).expect("Should return a valid result"); - - permit_update_operator_param.signature = keypairs.sign_message(&to_bytes(&message_hash)); - - // Update operator with the permit function. - let update = chain - .contract_update( - Signer::with_one_key(), - BOB, - Address::Account(BOB), - Energy::from(10000), - UpdateContractPayload { - amount: Amount::zero(), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.permit".to_string()), - message: OwnedParameter::from_serial(&permit_update_operator_param) - .expect("Should be a valid inut parameter"), - }, - ) - .expect("Should be able to update operator with permit"); + let update = permit( + &mut chain, + contract_address, + to_bytes(&payload), + "updateOperator".to_string(), + keypairs, + ); // Check that the correct events occurred. let events = update @@ -625,61 +471,8 @@ fn test_permit_transfer() { }; let payload = TransferParams::from(vec![transfer]); - // The `viewMessageHash` function uses the same input parameter `PermitParam` as - // the `permit` function. The `PermitParam` type includes a `signature` and - // a `signer`. Because these two values (`signature` and `signer`) are not - // read in the `viewMessageHash` function, any value can be used and we choose - // to use `DUMMY_SIGNATURE` and `ALICE` in the test case below. - let signature_map = BTreeMap::from([(0u8, CredentialSignatures { - sigs: BTreeMap::from([(0u8, concordium_std::Signature::Ed25519(DUMMY_SIGNATURE))]), - })]); - - let mut permit_transfer_param = PermitParam { - signature: AccountSignatures { - sigs: signature_map, - }, - signer: ALICE, - message: PermitMessage { - timestamp: Timestamp::from_timestamp_millis(10_000_000_000), - contract_address: ContractAddress::new(0, 0), - entry_point: OwnedEntrypointName::new_unchecked("transfer".into()), - nonce: 0, - payload: to_bytes(&payload), - }, - }; - - // Get the message hash to be signed. - let invoke = chain - .contract_invoke(BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.viewMessageHash".to_string()), - message: OwnedParameter::from_serial(&permit_transfer_param) - .expect("Should be a valid inut parameter"), - }) - .expect("Should be able to query viewMessageHash"); - - let message_hash: HashSha2256 = - from_bytes(&invoke.return_value).expect("Should return a valid result"); - - permit_transfer_param.signature = keypairs.sign_message(&to_bytes(&message_hash)); - - // Transfer token with the permit function. - let update = chain - .contract_update( - Signer::with_one_key(), - BOB, - BOB_ADDR, - Energy::from(10000), - UpdateContractPayload { - amount: Amount::zero(), - address: contract_address, - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.permit".to_string()), - message: OwnedParameter::from_serial(&permit_transfer_param) - .expect("Should be a valid inut parameter"), - }, - ) - .expect("Should be able to transfer token with permit"); + let update = + permit(&mut chain, contract_address, to_bytes(&payload), "transfer".to_string(), keypairs); // Check that the correct events occurred. let events = update @@ -1199,6 +992,71 @@ fn test_upgrade_without_migration_function() { })]); } +/// Execute a permit function invoke +fn permit( + chain: &mut Chain, + contract_address: ContractAddress, + payload: Vec, + entrypoint_name: String, + keypairs: AccountKeys, +) -> ContractInvokeSuccess { + // The `viewMessageHash` function uses the same input parameter `PermitParam` as + // the `permit` function. The `PermitParam` type includes a `signature` and + // a `signer`. Because these two values (`signature` and `signer`) are not + // read in the `viewMessageHash` function, any value can be used and we choose + // to use `DUMMY_SIGNATURE` and `ALICE` in the test case below. + let signature_map = BTreeMap::from([(0u8, CredentialSignatures { + sigs: BTreeMap::from([(0u8, concordium_std::Signature::Ed25519(DUMMY_SIGNATURE))]), + })]); + + let mut param = PermitParam { + signature: AccountSignatures { + sigs: signature_map, + }, + signer: ALICE, + message: PermitMessage { + timestamp: Timestamp::from_timestamp_millis(10_000_000_000), + contract_address: ContractAddress::new(0, 0), + entry_point: OwnedEntrypointName::new_unchecked(entrypoint_name), + nonce: 0, + payload, + }, + }; + + // Get the message hash to be signed. + let invoke = chain + .contract_invoke(BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.viewMessageHash".to_string()), + message: OwnedParameter::from_serial(¶m) + .expect("Should be a valid inut parameter"), + }) + .expect("Should be able to query viewMessageHash"); + + let message_hash: HashSha2256 = + from_bytes(&invoke.return_value).expect("Should return a valid result"); + + param.signature = keypairs.sign_message(&to_bytes(&message_hash)); + + // Execute permit function. + chain + .contract_update( + Signer::with_one_key(), + BOB, + BOB_ADDR, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.permit".to_string()), + message: OwnedParameter::from_serial(¶m) + .expect("Should be a valid inut parameter"), + }, + ) + .expect("Should be able to exit permit token with permit") +} + /// Check if Bob is an operator of Alice. fn operator_of(chain: &Chain, contract_address: ContractAddress) -> OperatorOfQueryResponse { let operator_of_params = OperatorOfQueryParams { From 7483a337655dfcfd1894a8749a387417c48dc214 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Fri, 12 Jan 2024 14:23:27 +0100 Subject: [PATCH 12/13] Add pausing tests --- concordium-cis2/CHANGELOG.md | 1 + examples/cis2-multi/src/lib.rs | 215 +++++++++++++++-------------- examples/cis2-multi/tests/tests.rs | 182 +++++++++++++++++++++++- 3 files changed, 294 insertions(+), 104 deletions(-) diff --git a/concordium-cis2/CHANGELOG.md b/concordium-cis2/CHANGELOG.md index 7a873d30d..092f91703 100644 --- a/concordium-cis2/CHANGELOG.md +++ b/concordium-cis2/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased changes +- Add `Clone` trait for `StandardIdentifierOwned`. - Add specific parameter type `OnReceivingCis2DataParams` for a contract function which receives CIS2 tokens with a specific type D for the AdditionalData. - Add `Deserial` trait for `OnReceivingCis2DataParams`. - Add `Serial` trait for `OnReceivingCis2DataParams`. diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index 4fa9df04d..71484967f 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -1,6 +1,3 @@ -//! TODO explain pausing, grant roles, update roles, permit function has added -//! mint/burn TODO add more testcases (grant role and pausing tests) -//! //! A multi token example implementation of the Concordium Token Standard CIS2 //! and the Concordium Sponsored Transaction Standard CIS3. //! @@ -28,6 +25,11 @@ //! service provider submits the transaction on behalf of the user and pays the //! transaction fee to execute the transaction on-chain. //! +//! As follows from the CIS3 specification, the contract has a `permit` +//! function. It is the sponsored counterpart to the +//! `transfer/updateOperator/mint/burn` function and can be executed by anyone +//! on behalf of an account given a signed message. +//! //! Concordium supports natively multi-sig accounts. Each account address on //! Concordium is controlled by one or several credential(s) (real-world //! identities) and each credential has one or several public-private key @@ -51,17 +53,13 @@ //! address to another address. An address can enable and disable one or more //! addresses as operators with the `updateOperator` function. An operator of //! an address is allowed to transfer any tokens owned by this address. -//! As follows from the CIS3 specification, the contract has a `permit` -//! function. It is the sponsored counterpart to the `transfer/updateOperator` -//! function and can be executed by anyone on behalf of an account given a -//! signed message. //! -//! ## `Mint` und `burn` functions: +//! ## `Mint` and `burn` functions: //! In this example, the contract is initialized with no tokens, and tokens can //! be minted through a `mint` contract function, which can be called by anyone //! (unprotected function). The `mint` function airdrops the `MINT_AIRDROP` //! amount of tokens to a specified `owner` address in the input parameter. -//! ATTENTION: You most likley want to add your custom access control mechanism +//! ATTENTION: You most likely want to add your custom access control mechanism //! to the `mint` function and `permit` function. //! //! A token owner (or any of its operator addresses) can burn some of the token @@ -74,11 +72,22 @@ //! This function is not very useful and is only there to showcase a simple //! implementation of a token receive hook. //! +//! ## Grant and Revoke roles: +//! The contract has access control roles. The admin can grant or revoke all +//! other roles. The available roles are ADMIN (can grant/revoke roles), +//! UPGRADER (can upgrade the contract), BLACKLISTER (can blacklist addresses), +//! and PAUSER (can pause the contract). +//! //! ## Blacklist: //! This contract includes a blacklist. The account with `BLACKLISTER` role can //! add/remove addresses to/from that list. Blacklisted addresses can not //! transfer their tokens, receive new tokens, or burn their tokens. //! +//! ## Pausing: +//! This contract can be paused by an address with the PAUSER role. +//! `Mint,burn,updateOperator,transfer` entrypoints cannot be called when the +//! contract is paused. +//! //! ## Native upgradability: //! This contract can be upgraded. The contract //! has a function to upgrade the smart contract instance to a new module and @@ -145,7 +154,7 @@ pub enum Event { /// The event tracks when a new role is granted to an address. #[concordium(tag = 248)] GrantRole(GrantRoleEvent), - /// The event tracks when a role is removed from an address. + /// The event tracks when a role is revoked from an address. #[concordium(tag = 247)] RevokeRole(RevokeRoleEvent), /// Cis2 token events. @@ -182,7 +191,7 @@ pub struct GrantRoleEvent { role: Roles, } -/// The RevokeRoleEvent is logged when a role is removed from an address. +/// The RevokeRoleEvent is logged when a role is revoked from an address. #[derive(Serialize, SchemaType, Debug, PartialEq, Eq)] pub struct RevokeRoleEvent { /// Address that has been its role revoked. @@ -332,6 +341,84 @@ pub struct BurnParams { pub token_id: ContractTokenId, } +/// The parameter type for the contract function `supportsPermit`. +#[derive(Debug, Serialize, SchemaType)] +pub struct SupportsPermitQueryParams { + /// The list of supportPermit queries. + #[concordium(size_length = 2)] + pub queries: Vec, +} + +/// The parameter type for the contract function `setImplementors`. +/// Takes a standard identifier and list of contract addresses providing +/// implementations of this standard. +#[derive(Debug, Serialize, SchemaType)] +struct SetImplementorsParams { + /// The identifier for the standard. + id: StandardIdentifierOwned, + /// The addresses of the implementors of the standard. + implementors: Vec, +} + +/// Part of the parameter type for the contract function `permit`. +/// Specifies the message that is signed. +#[derive(SchemaType, Serialize)] +pub struct PermitMessage { + /// The contract_address that the signature is intended for. + pub contract_address: ContractAddress, + /// A nonce to prevent replay attacks. + pub nonce: u64, + /// A timestamp to make signatures expire. + pub timestamp: Timestamp, + /// The entry_point that the signature is intended for. + pub entry_point: OwnedEntrypointName, + /// The serialized payload that should be forwarded to either the `transfer` + /// or the `updateOperator` function. + #[concordium(size_length = 2)] + pub payload: Vec, +} + +/// The parameter type for the contract function `permit`. +/// Takes a signature, the signer, and the message that was signed. +#[derive(Serialize, SchemaType)] +pub struct PermitParam { + /// Signature/s. The CIS3 standard supports multi-sig accounts. + pub signature: AccountSignatures, + /// Account that created the above signature. + pub signer: AccountAddress, + /// Message that was signed. + pub message: PermitMessage, +} + +#[derive(Serialize)] +pub struct PermitParamPartial { + /// Signature/s. The CIS3 standard supports multi-sig accounts. + signature: AccountSignatures, + /// Account that created the above signature. + signer: AccountAddress, +} + +/// The parameter type for the contract function `setPaused`. +#[derive(Serialize, SchemaType)] +#[repr(transparent)] +struct SetPausedParams { + /// Specifies if contract is paused. + paused: bool, +} + +/// The parameter type for the contract function `upgrade`. +/// Takes the new module and optionally an entrypoint to call in the new module +/// after triggering the upgrade. The upgrade is reverted if the entrypoint +/// fails. This is useful for doing migration in the same transaction triggering +/// the upgrade. +#[derive(Serialize, SchemaType)] +pub struct UpgradeParams { + /// The new module reference. + pub module: ModuleReference, + /// Optional entrypoint to call in the new module after upgrade. + pub migrate: Option<(OwnedEntrypointName, OwnedParameter)>, +} + /// The parameter for the contract function `grantRole` which grants a role to /// an address. #[derive(Serialize, SchemaType)] @@ -342,10 +429,10 @@ pub struct GrantRoleParams { pub role: Roles, } -/// The parameter for the contract function `removeRole` which revokes a role +/// The parameter for the contract function `revokeRole` which revokes a role /// from an address. #[derive(Serialize, SchemaType)] -pub struct RemoveRoleParams { +pub struct RevokeRoleParams { /// The address that has been its role revoked. pub address: Address, /// The role that has been revoked from the above address. @@ -424,71 +511,6 @@ struct State { roles: StateMap, S>, } -/// The parameter type for the contract function `supportsPermit`. -#[derive(Debug, Serialize, SchemaType)] -pub struct SupportsPermitQueryParams { - /// The list of supportPermit queries. - #[concordium(size_length = 2)] - pub queries: Vec, -} - -/// The parameter type for the contract function `setImplementors`. -/// Takes a standard identifier and list of contract addresses providing -/// implementations of this standard. -#[derive(Debug, Serialize, SchemaType)] -struct SetImplementorsParams { - /// The identifier for the standard. - id: StandardIdentifierOwned, - /// The addresses of the implementors of the standard. - implementors: Vec, -} - -/// Part of the parameter type for the contract function `permit`. -/// Specifies the message that is signed. -#[derive(SchemaType, Serialize)] -pub struct PermitMessage { - /// The contract_address that the signature is intended for. - pub contract_address: ContractAddress, - /// A nonce to prevent replay attacks. - pub nonce: u64, - /// A timestamp to make signatures expire. - pub timestamp: Timestamp, - /// The entry_point that the signature is intended for. - pub entry_point: OwnedEntrypointName, - /// The serialized payload that should be forwarded to either the `transfer` - /// or the `updateOperator` function. - #[concordium(size_length = 2)] - pub payload: Vec, -} - -/// The parameter type for the contract function `permit`. -/// Takes a signature, the signer, and the message that was signed. -#[derive(Serialize, SchemaType)] -pub struct PermitParam { - /// Signature/s. The CIS3 standard supports multi-sig accounts. - pub signature: AccountSignatures, - /// Account that created the above signature. - pub signer: AccountAddress, - /// Message that was signed. - pub message: PermitMessage, -} - -#[derive(Serialize)] -pub struct PermitParamPartial { - /// Signature/s. The CIS3 standard supports multi-sig accounts. - signature: AccountSignatures, - /// Account that created the above signature. - signer: AccountAddress, -} - -/// The parameter type for the contract function `setPaused`. -#[derive(Serialize, SchemaType)] -#[repr(transparent)] -struct SetPausedParams { - /// Specifies if contract is paused. - paused: bool, -} - /// The different errors the contract can produce. #[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] pub enum CustomContractError { @@ -536,7 +558,7 @@ pub enum CustomContractError { FailedUpgradeUnsupportedModuleVersion, // -18 /// Contract is paused. Paused, // -19 - /// Failed to remove role because it was not granted in the first place. + /// Failed to revoke role because it was not granted in the first place. RoleWasNotGranted, // -20 /// Failed to grant role because it was granted already in the first place. RoleWasAlreadyGranted, // -21 @@ -777,8 +799,8 @@ impl State { }); } - /// Remove role from an address. - fn remove_role(&mut self, account: &Address, role: Roles) { + /// Revoke role from an address. + fn revoke_role(&mut self, account: &Address, role: Roles) { self.roles.entry(*account).and_modify(|entry| { entry.roles.remove(&role); }); @@ -1340,7 +1362,7 @@ fn contract_permit( MINT_ENTRYPOINT => { // Mint tokens. // ATTENTION: Can be called by anyone. You should add your - // custom access control to here. + // custom access control here. let params: MintParams = from_bytes(&message.payload)?; mint(params, host, logger)?; @@ -1881,19 +1903,6 @@ fn contract_update_blacklist( Ok(()) } -/// The parameter type for the contract function `upgrade`. -/// Takes the new module and optionally an entrypoint to call in the new module -/// after triggering the upgrade. The upgrade is reverted if the entrypoint -/// fails. This is useful for doing migration in the same transaction triggering -/// the upgrade. -#[derive(Serialize, SchemaType)] -pub struct UpgradeParams { - /// The new module reference. - pub module: ModuleReference, - /// Optional entrypoint to call in the new module after upgrade. - pub migrate: Option<(OwnedEntrypointName, OwnedParameter)>, -} - /// Upgrade this smart contract instance to a new module and call optionally a /// migration function after the upgrade. /// @@ -2014,26 +2023,26 @@ fn contract_grant_role( Ok(()) } -/// Remove role from an address. +/// Revoke role from an address. /// /// It rejects if: /// - It fails to parse the parameter. /// - Sender is not the ADMIN of the contract instance. -/// - The `address` is not holding the specified role to be removed. +/// - The `address` is not holding the specified role to be revoked. #[receive( contract = "cis2_multi", - name = "removeRole", - parameter = "RemoveRoleParams", + name = "revokeRole", + parameter = "RevokeRoleParams", enable_logger, mutable )] -fn contract_remove_role( +fn contract_revoke_role( ctx: &ReceiveContext, host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { // Parse the parameter. - let params: RemoveRoleParams = ctx.parameter_cursor().get()?; + let params: RevokeRoleParams = ctx.parameter_cursor().get()?; let (state, _) = host.state_and_builder(); @@ -2048,8 +2057,8 @@ fn contract_remove_role( CustomContractError::RoleWasNotGranted.into() ); - // Remove role. - state.remove_role(¶ms.address, params.role); + // Revoke role. + state.revoke_role(¶ms.address, params.role); logger.log(&Event::RevokeRole(RevokeRoleEvent { address: params.address, role: params.role, diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs index ca326ccbd..156fce48d 100644 --- a/examples/cis2-multi/tests/tests.rs +++ b/examples/cis2-multi/tests/tests.rs @@ -20,6 +20,8 @@ const UPGRADER: AccountAddress = AccountAddress([2; 32]); const UPGRADER_ADDR: Address = Address::Account(UPGRADER); const BLACKLISTER: AccountAddress = AccountAddress([3; 32]); const BLACKLISTER_ADDR: Address = Address::Account(BLACKLISTER); +const PAUSER: AccountAddress = AccountAddress([4; 32]); +const PAUSER_ADDR: Address = Address::Account(PAUSER); const NON_EXISTING_ACCOUNT: AccountAddress = AccountAddress([99u8; 32]); /// Token IDs. @@ -992,6 +994,167 @@ fn test_upgrade_without_migration_function() { })]); } +/// Test that the pause/unpause entrypoints correctly sets the pause value in +/// the state. +#[test] +fn test_pause_functionality() { + let (mut chain, _keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); + + // Pause the contract. + chain + .contract_update(SIGNER, PAUSER, PAUSER_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.setPaused".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&true).expect("Pause params"), + }) + .expect("Pause"); + + // Check that the contract is now paused. + assert_eq!(invoke_view(&mut chain, contract_address).paused, true); + + // Unpause the contract. + chain + .contract_update(SIGNER, PAUSER, PAUSER_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.setPaused".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&false).expect("Unpause params"), + }) + .expect("Unpause"); + // Check that the contract is now unpaused. + assert_eq!(invoke_view(&mut chain, contract_address).paused, false); +} + +/// Test that only the PAUSER can pause/unpause the contract. +#[test] +fn test_pause_unpause_unauthorized() { + let (mut chain, _keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); + + // Pause the contract as Bob, who is not the PAUSER. + let update = chain + .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.setPaused".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&true).expect("Pause params"), + }) + .expect_err("Pause"); + + // Check that the correct error is returned. + let rv: ContractError = update.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, ContractError::Unauthorized); +} + +/// Test that one can NOT call non-admin state-mutative functions (burn, +/// mint, transfer, updateOperator) when the contract is paused. +#[test] +fn test_no_execution_of_state_mutative_functions_when_paused() { + let (mut chain, _keypairs, contract_address, _update, _module_reference) = + initialize_contract_with_alice_tokens(); + + // Pause the contract. + chain + .contract_update(SIGNER, PAUSER, PAUSER_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.setPaused".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&true).expect("Pause params"), + }) + .expect("Pause"); + + // Try to transfer 1 token from Alice to Bob. + let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::Account(BOB), + token_id: TOKEN_0, + amount: TokenAmountU64(1), + data: AdditionalData::empty(), + }]); + let update_transfer = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.transfer".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), + }) + .expect_err("Transfer tokens"); + assert_contract_paused_error(&update_transfer); + + // Try to add Bob as an operator for Alice. + let params = UpdateOperatorParams(vec![UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }]); + let update_operator = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.updateOperator".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), + }) + .expect_err("Update operator"); + assert_contract_paused_error(&update_operator); + + // Try to mint tokens. + let params = MintParams { + owner: ALICE_ADDR, + metadata_url: MetadataUrl { + url: "https://some.example/token/02".to_string(), + hash: None, + }, + token_id: TOKEN_0, + }; + + let update_operator = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.mint".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("Mint params"), + }) + .expect_err("Update operator"); + assert_contract_paused_error(&update_operator); + + // Try to burn tokens. + let params = BurnParams { + owner: ALICE_ADDR, + amount: TokenAmountU64(1), + token_id: TOKEN_0, + }; + + let update_operator = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.burn".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(¶ms).expect("Burn params"), + }) + .expect_err("Update operator"); + assert_contract_paused_error(&update_operator); +} + +/// Check that the returned error is `ContractPaused`. +fn assert_contract_paused_error(update: &ContractInvokeError) { + let rv: ContractError = update.parse_return_value().expect("ContractError return value"); + assert_eq!(rv, ContractError::Custom(CustomContractError::Paused)); +} + +/// Get the result of the view entrypoint. +fn invoke_view(chain: &mut Chain, contract_address: ContractAddress) -> ViewState { + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.view".to_string()), + address: contract_address, + message: OwnedParameter::empty(), + }) + .expect("Invoke view"); + invoke.parse_return_value().expect("Return value") +} + /// Execute a permit function invoke fn permit( chain: &mut Chain, @@ -1183,7 +1346,7 @@ fn initialize_contract_with_alice_tokens( } /// Setup chain and contract. -/// The function creates the four accounts: ALICE, BOB, UPGRADER, and +/// The function creates the five accounts: ALICE, BOB, UPGRADER, PAUSER, and /// BLACKLISTER. The function grants ALICE the ADMIN role, the UPGRADER the /// UPGRADE role, and the BLACKLISTER the BLACKLISTE role. fn initialize_chain_and_contract() -> (Chain, AccountKeys, ContractAddress, ModuleReference) { @@ -1204,6 +1367,7 @@ fn initialize_chain_and_contract() -> (Chain, AccountKeys, ContractAddress, Modu chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); chain.create_account(Account::new(UPGRADER, ACC_INITIAL_BALANCE)); chain.create_account(Account::new(BLACKLISTER, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(PAUSER, ACC_INITIAL_BALANCE)); // Load and deploy the module. let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); @@ -1235,6 +1399,22 @@ fn initialize_chain_and_contract() -> (Chain, AccountKeys, ContractAddress, Modu }) .expect("UPGRADER should be granted role"); + // Grant PAUSER role + let grant_role_params = GrantRoleParams { + address: PAUSER_ADDR, + role: Roles::PAUSER, + }; + + let _update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.grantRole".to_string()), + address: init.contract_address, + message: OwnedParameter::from_serial(&grant_role_params) + .expect("GrantRole params"), + }) + .expect("PAUSER should be granted role"); + // Grant BLACKLISTER role let grant_role_params = GrantRoleParams { address: BLACKLISTER_ADDR, From d707448730276d1ba6a0231d3782ad61fabcc8bc Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 16 Jan 2024 11:41:59 +0100 Subject: [PATCH 13/13] Address comments --- examples/cis2-multi/src/lib.rs | 71 +++++++++++++++++------------- examples/cis2-multi/tests/tests.rs | 2 +- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index 71484967f..62ac6b663 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -128,10 +128,10 @@ const SUPPORTS_PERMIT_ENTRYPOINTS: [EntrypointName; 2] = [EntrypointName::new_unchecked("updateOperator"), EntrypointName::new_unchecked("transfer")]; /// Event tags. -pub const NONCE_EVENT_TAG: u8 = u8::MAX - 5; -pub const UPDATE_BLACKLIST_EVENT_TAG: u8 = u8::MAX - 6; -pub const GRANT_ROLE_EVENT_TAG: u8 = u8::MAX - 7; -pub const REVOKE_ROLE_EVENT_TAG: u8 = u8::MAX - 8; +pub const UPDATE_BLACKLIST_EVENT_TAG: u8 = 0; +pub const GRANT_ROLE_EVENT_TAG: u8 = 1; +pub const REVOKE_ROLE_EVENT_TAG: u8 = 2; +pub const NONCE_EVENT_TAG: u8 = 250; const TRANSFER_ENTRYPOINT: EntrypointName<'_> = EntrypointName::new_unchecked("transfer"); const UPDATE_OPERATOR_ENTRYPOINT: EntrypointName<'_> = @@ -143,20 +143,21 @@ const BURN_ENTRYPOINT: EntrypointName<'_> = EntrypointName::new_unchecked("burn" #[derive(Debug, Serial, Deserial, PartialEq, Eq)] #[concordium(repr(u8))] pub enum Event { - /// The event tracks the nonce used by the signer of the `PermitMessage` - /// whenever the `permit` function is invoked. - #[concordium(tag = 250)] - Nonce(NonceEvent), /// The event is logged whenever an address is added or removed from the /// blacklist. - #[concordium(tag = 249)] + #[concordium(tag = 0)] UpdateBlacklist(UpdateBlacklistEvent), /// The event tracks when a new role is granted to an address. - #[concordium(tag = 248)] + #[concordium(tag = 1)] GrantRole(GrantRoleEvent), /// The event tracks when a role is revoked from an address. - #[concordium(tag = 247)] + #[concordium(tag = 2)] RevokeRole(RevokeRoleEvent), + /// Cis3 event. + /// The event tracks the nonce used by the signer of the `PermitMessage` + /// whenever the `permit` function is invoked. + #[concordium(tag = 250)] + Nonce(NonceEvent), /// Cis2 token events. #[concordium(forward = cis2_events)] Cis2Event(Cis2Event), @@ -186,18 +187,18 @@ pub struct UpdateBlacklistEvent { #[derive(Serialize, SchemaType, Debug, PartialEq, Eq)] pub struct GrantRoleEvent { /// The address that has been its role granted. - address: Address, + pub address: Address, /// The role that was granted to the above address. - role: Roles, + pub role: Roles, } /// The RevokeRoleEvent is logged when a role is revoked from an address. #[derive(Serialize, SchemaType, Debug, PartialEq, Eq)] pub struct RevokeRoleEvent { /// Address that has been its role revoked. - address: Address, + pub address: Address, /// The role that was revoked from the above address. - role: Roles, + pub role: Roles, } // Implementing a custom schemaType for the `Event` struct containing all @@ -393,17 +394,17 @@ pub struct PermitParam { #[derive(Serialize)] pub struct PermitParamPartial { /// Signature/s. The CIS3 standard supports multi-sig accounts. - signature: AccountSignatures, + pub signature: AccountSignatures, /// Account that created the above signature. - signer: AccountAddress, + pub signer: AccountAddress, } /// The parameter type for the contract function `setPaused`. #[derive(Serialize, SchemaType)] #[repr(transparent)] -struct SetPausedParams { +pub struct SetPausedParams { /// Specifies if contract is paused. - paused: bool, + pub paused: bool, } /// The parameter type for the contract function `upgrade`. @@ -458,6 +459,8 @@ pub enum Roles { BLACKLISTER, /// Pauser role. PAUSER, + // /// Minter role. (comment in if you want a MINTER role) + // MINTER, } /// The state for each address. @@ -1018,6 +1021,15 @@ fn contract_mint( // Parse the parameter. let params: MintParams = ctx.parameter_cursor().get()?; + // // Get the sender who invoked this contract function. (comment in if you want + // // only the MINTER role to mint tokens) + // let sender = ctx.sender(); + + // // Check that only the MINTER is authorized to mint an address. (comment in + // // if you want only the MINTER role to mint tokens) + // ensure!(host.state().has_role(&sender, Roles::MINTER), + // ContractError::Unauthorized); + mint(params, host, logger)?; Ok(()) @@ -1349,7 +1361,6 @@ fn contract_permit( for update in updates { update_operator( - state.paused, update.update, concordium_std::Address::Account(param.signer), update.operator, @@ -1365,6 +1376,13 @@ fn contract_permit( // custom access control here. let params: MintParams = from_bytes(&message.payload)?; + // // Check that only the MINTER can authorize to mint. (comment in if you want + // // only the MINTER role having the authorization to mint) + // ensure!( + // host.state().has_role(&Address::from(param.signer), Roles::MINTER), + // ContractError::Unauthorized + // ); + mint(params, host, logger)?; } BURN_ENTRYPOINT => { @@ -1399,7 +1417,6 @@ fn contract_permit( /// Logs a `UpdateOperator` event. The function assumes that the sender is /// authorized to do the `updateOperator` action. fn update_operator( - paused: bool, update: OperatorUpdate, sender: Address, operator: Address, @@ -1408,7 +1425,7 @@ fn update_operator( logger: &mut impl HasLogger, ) -> ContractResult<()> { // Check that contract is not paused. - ensure!(!paused, CustomContractError::Paused.into()); + ensure!(!state.paused, CustomContractError::Paused.into()); // Update the operator in the state. match update { @@ -1453,15 +1470,7 @@ fn contract_update_operator( let sender = ctx.sender(); let (state, builder) = host.state_and_builder(); for param in params { - update_operator( - state.paused, - param.update, - sender, - param.operator, - state, - builder, - logger, - )?; + update_operator(param.update, sender, param.operator, state, builder, logger)?; } Ok(()) } diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs index 156fce48d..eb54eb31e 100644 --- a/examples/cis2-multi/tests/tests.rs +++ b/examples/cis2-multi/tests/tests.rs @@ -198,7 +198,7 @@ fn test_add_operator() { address: contract_address, message: OwnedParameter::from_serial(&query_params).expect("OperatorOf params"), }) - .expect("Invoke opeatorOF"); + .expect("Invoke opeatorOf"); let rv: OperatorOfQueryResponse = invoke.parse_return_value().expect("OperatorOf return value"); assert_eq!(rv, OperatorOfQueryResponse(vec![true]));