From bf8429f5d94d0db71f92fd0e95e24f44174a88ea Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 14 Mar 2024 15:31:39 +0200 Subject: [PATCH 01/28] Add account abstracted smart contract wallet --- examples/README.md | 1 + .../Cargo.toml | 27 +++ .../src/lib.rs | 203 ++++++++++++++++++ .../tests/tests.rs | 149 +++++++++++++ 4 files changed, 380 insertions(+) create mode 100644 examples/account-abstracted-smart-contract-wallet/Cargo.toml create mode 100644 examples/account-abstracted-smart-contract-wallet/src/lib.rs create mode 100644 examples/account-abstracted-smart-contract-wallet/tests/tests.rs diff --git a/examples/README.md b/examples/README.md index 6ce83d63a..a6a6b9cd6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,6 +13,7 @@ the logic of the contract is reasonable, or safe. The list of contracts is as follows: +- [account-abstracted-smart-contract-wallet](./account-abstracted-smart-contract-wallet) An example of how to implement a smart contract wallet. - [account-signature-checks](./account-signature-checks) A simple contract that demonstrates how account signature checks can be performed in smart contracts. - [two-step-transfer](./two-step-transfer) A contract that acts like an account (can send, store and accept CCD), diff --git a/examples/account-abstracted-smart-contract-wallet/Cargo.toml b/examples/account-abstracted-smart-contract-wallet/Cargo.toml new file mode 100644 index 000000000..e4d6d0c44 --- /dev/null +++ b/examples/account-abstracted-smart-contract-wallet/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "account-abstracted-smart-contract-wallet" +version = "0.1.0" +authors = ["Concordium "] +edition = "2021" +license = "MPL-2.0" + +[features] +default = ["std", "bump_alloc"] +std = ["concordium-std/std", "concordium-cis2/std"] +bump_alloc = ["concordium-std/bump_alloc"] + +[dependencies] +concordium-std = {path = "../../concordium-std", default-features = false} +concordium-cis2 = {path = "../../concordium-cis2", default-features = false, features=[ + "u256_amount"]} + +[dev-dependencies] +concordium-smart-contract-testing = {path = "../../contract-testing"} +rand = "0.8" + +[lib] +crate-type=["cdylib", "rlib"] + +[profile.release] +codegen-units = 1 +opt-level = "s" diff --git a/examples/account-abstracted-smart-contract-wallet/src/lib.rs b/examples/account-abstracted-smart-contract-wallet/src/lib.rs new file mode 100644 index 000000000..5f89c7adb --- /dev/null +++ b/examples/account-abstracted-smart-contract-wallet/src/lib.rs @@ -0,0 +1,203 @@ +//! # +use concordium_cis2::*; +use concordium_std::*; + +#[derive(SchemaType, Serialize)] +pub struct VerificationParameter { + pub public_key: PublicKeyEd25519, + pub signature: SignatureEd25519, + pub message: Vec, +} + +// specification, the order of the fields cannot be changed. +#[derive(Debug, Serialize, SchemaType)] +pub struct OnReceivingCis2Params { + /// The ID of the token received. + pub token_id: ContractTokenId, + /// The amount of tokens received. + pub amount: A, + /// The previous owner of the tokens. + pub from: Address, + /// Some extra information which was sent as part of the transfer. + pub data: AdditionalData, +} + +/// 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. + pub signature: AccountSignatures, + /// Account that created the above signature. + pub signer: AccountAddress, +} + +/// Contract token ID type. +pub type ContractTokenId = TokenIdVec; + +/// Contract token amount. +/// Since the tokens are non-fungible the total supply of any token will be at +/// most 1 and it is fine to use a small type for representing token amounts. +pub type ContractTokenAmount = TokenAmountU256; + +/// The state for each address. +#[derive(Serial, DeserialWithState, Deletable)] +#[concordium(state_parameter = "S")] +struct PublicKeyState { + /// The amount of tokens owned by this address. + token_balances: StateMap, S>, + /// + native_balance: Amount, +} + +impl PublicKeyState { + fn empty(state_builder: &mut StateBuilder) -> Self { + PublicKeyState { + token_balances: state_builder.new_map(), + native_balance: Amount::zero(), + } + } +} + +/// The contract state, +/// +/// Note: The specification does not specify how to structure the contract state +/// and this could be structured in a more space-efficient way. +#[derive(Serial, DeserialWithState)] +#[concordium(state_parameter = "S")] +struct State { + /// The state of addresses. + balances: StateMap, S>, + /// 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 + /// prevent replay attacks of the signed message. The nonce is increased + /// sequentially every time a signed message (corresponding to the + /// account) is successfully executed in the `permit` function. This + /// mapping keeps track of the next nonce that needs to be used by the + /// account to generate a signature. + nonces_registry: StateMap, +} + +/// The different errors the contract can produce. +#[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] +pub enum CustomContractError { + /// Failed parsing the parameter. + #[from(ParseError)] + ParseParams, // -1 + /// Failed logging: Log is full. + LogFull, // -2 + /// Failed logging: Log is malformed. + LogMalformed, // -3 + /// Invalid contract name. + OnlyContract, +} + +pub type ContractError = Cis2Error; + +pub type ContractResult = Result; + +#[init(contract = "account-abstracted-smart-contract-wallet")] +fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { + let state = State { + balances: state_builder.new_map(), + implementors: state_builder.new_map(), + nonces_registry: state_builder.new_map(), + }; + + Ok(state) +} + +/// +#[receive( + contract = "account-abstracted-smart-contract-wallet", + name = "depositNativeCurrency", + parameter = "PublicKeyEd25519", + payable, + mutable +)] +fn deposit_native_currency( + ctx: &ReceiveContext, + host: &mut Host, + amount: Amount, +) -> ReceiveResult<()> { + let beneficiary: PublicKeyEd25519 = ctx.parameter_cursor().get()?; + + let (state, builder) = host.state_and_builder(); + + let mut public_key_balances = + state.balances.entry(beneficiary).or_insert_with(|| PublicKeyState::empty(builder)); + + public_key_balances.native_balance = amount; + + Ok(()) +} + +/// +#[receive( + contract = "account-abstracted-smart-contract-wallet", + name = "depositCis2Tokens", + parameter = "PublicKeyEd25519", + mutable +)] +fn deposit_cis2_tokens(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { + let cis2_hook_param: OnReceivingCis2DataParams< + ContractTokenId, + ContractTokenAmount, + PublicKeyEd25519, + > = ctx.parameter_cursor().get()?; + + // Ensure that only contracts can call this hook function + let contract_sender_address = match ctx.sender() { + Address::Contract(contract_sender_address) => contract_sender_address, + Address::Account(_) => bail!(CustomContractError::OnlyContract.into()), + }; + + let (state, builder) = host.state_and_builder(); + + let mut public_key_balances = state + .balances + .entry(cis2_hook_param.data) + .or_insert_with(|| PublicKeyState::empty(builder)); + + let mut contract_token_balances = public_key_balances + .token_balances + .entry(contract_sender_address) + .or_insert_with(|| builder.new_map()); + + let mut cis2_token_balance = contract_token_balances.entry(cis2_hook_param.token_id).or_insert_with(|| TokenAmountU256 { + 0: 0u8.into(), + }); + + *cis2_token_balance += cis2_hook_param.amount; + Ok(()) +} diff --git a/examples/account-abstracted-smart-contract-wallet/tests/tests.rs b/examples/account-abstracted-smart-contract-wallet/tests/tests.rs new file mode 100644 index 000000000..60404ecc9 --- /dev/null +++ b/examples/account-abstracted-smart-contract-wallet/tests/tests.rs @@ -0,0 +1,149 @@ +//! Tests for the signature-verifier contract. +use concordium_smart_contract_testing::*; +use concordium_std::*; + +const ALICE: AccountAddress = AccountAddress([0u8; 32]); +const ALICE_ADDR: Address = Address::Account(ALICE); +const SIGNER: Signer = Signer::with_one_key(); + +// #[test] +// fn test_outside_signature_check() { +// let mut chain = Chain::new(); + +// // Create an account. +// chain.create_account(Account::new(ALICE, Amount::from_ccd(1000))); + +// // Load and deploy the module. +// let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists."); +// let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Module deploys."); + +// // Initialize the signature verifier contract. +// let init = chain +// .contract_init(SIGNER, ALICE, Energy::from(10_000), InitContractPayload { +// amount: Amount::zero(), +// mod_ref: deployment.module_reference, +// init_name: OwnedContractName::new_unchecked("init_signature-verifier".to_string()), +// param: OwnedParameter::empty(), +// }) +// .expect("Initialize signature verifier contract"); + +// // Construct a parameter with an invalid signature. +// let parameter_invalid = VerificationParameter { +// public_key: PublicKeyEd25519([0; 32]), +// signature: SignatureEd25519([1; 64]), +// message: vec![2; 100], +// }; + +// // Call the contract with the invalid signature. +// let update_invalid = chain +// .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { +// amount: Amount::zero(), +// address: init.contract_address, +// receive_name: OwnedReceiveName::new_unchecked("signature-verifier.verify".to_string()), +// message: OwnedParameter::from_serial(¶meter_invalid) +// .expect("Parameter has valid size."), +// }) +// .expect("Call signature verifier contract with an invalid signature."); +// // Check that the signature does NOT verify. +// let rv: bool = update_invalid.parse_return_value().expect("Deserializing bool"); +// assert!(!rv, "Signature verification should fail."); + +// // Construct a parameter with a valid signature. +// let parameter_valid = VerificationParameter { +// public_key: PublicKeyEd25519::from_str("35a2a8e52efad975dbf6580e7734e4f249eaa5ea8a763e934a8671cd7e446499").expect("Valid public key"), +// signature: SignatureEd25519::from_str("aaf2bfe0f7f74631850370422118f30e8787c5717a4a15527a5e1d0ffc791b663b1509b121022ef26086b37859001d09642674fa3be201f7d9dc2708f5e6ec02").expect("Valid signature"), +// message: b"Concordium".to_vec(), +// }; + +// // Call the contract with the valid signature. +// let update = chain +// .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { +// amount: Amount::zero(), +// address: init.contract_address, +// receive_name: OwnedReceiveName::new_unchecked("signature-verifier.verify".to_string()), +// message: OwnedParameter::from_serial(¶meter_valid) +// .expect("Parameter has valid size."), +// }) +// .expect("Call signature verifier contract with a valid signature."); +// // Check that the signature verifies. +// let rv: bool = update.parse_return_value().expect("Deserializing bool"); +// assert!(rv, "Signature checking failed."); +// } + +// /// Tests that the signature verifier contract returns true when the signature +// /// is valid. The signature is generated in the test case. +// #[test] +// fn test_inside_signature_check() { +// let mut chain = Chain::new(); + +// // Create an account. +// chain.create_account(Account::new(ALICE, Amount::from_ccd(1000))); + +// // Load and deploy the module. +// let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists."); +// let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Module deploys."); + +// // Initialize the signature verifier contract. +// let init = chain +// .contract_init(SIGNER, ALICE, Energy::from(10_000), InitContractPayload { +// amount: Amount::zero(), +// mod_ref: deployment.module_reference, +// init_name: OwnedContractName::new_unchecked("init_signature-verifier".to_string()), +// param: OwnedParameter::empty(), +// }) +// .expect("Initialize signature verifier contract"); + +// use ed25519_dalek::{Signer, SigningKey}; + +// let rng = &mut rand::thread_rng(); + +// // Construct message, verifying_key, and signature. +// let message: &[u8] = b"Concordium"; +// let signing_key = SigningKey::generate(rng); +// let verifying_key = signing_key.verifying_key(); +// let signature = signing_key.sign(&message); + +// // Construct a parameter with an invalid signature. +// let parameter_invalid = VerificationParameter { +// public_key: PublicKeyEd25519(verifying_key.to_bytes()), +// signature: SignatureEd25519(signature.to_bytes()), +// message: b"wrong_message".to_vec(), +// }; + +// // Call the contract with the invalid signature. +// let update_invalid = chain +// .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { +// amount: Amount::zero(), +// address: init.contract_address, +// receive_name: OwnedReceiveName::new_unchecked("signature-verifier.verify".to_string()), +// message: OwnedParameter::from_serial(¶meter_invalid) +// .expect("Parameter has valid size."), +// }) +// .expect("Call signature verifier contract with an invalid signature."); + +// // Check that the signature does NOT verify. +// let rv: bool = update_invalid.parse_return_value().expect("Deserializing bool"); +// assert!(!rv, "Signature verification should fail."); + +// // Construct a parameter with a valid signature. +// let parameter_valid = VerificationParameter { +// public_key: PublicKeyEd25519(verifying_key.to_bytes()), +// signature: SignatureEd25519(signature.to_bytes()), +// message: message.to_vec(), +// }; + +// // Call the contract with the valid signature. +// let update = chain +// .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { +// amount: Amount::zero(), +// address: init.contract_address, +// receive_name: OwnedReceiveName::new_unchecked("signature-verifier.verify".to_string()), +// message: OwnedParameter::from_serial(¶meter_valid) +// .expect("Parameter has valid size."), +// }) +// .expect("Call signature verifier contract with a valid signature."); + +// // Check that the signature verifies. +// let rv: bool = update.parse_return_value().expect("Deserializing bool"); +// assert!(rv, "Signature checking failed."); +// } From 881152ec858b97d8bc50b3847882edb2d39c5307 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 14 Mar 2024 16:16:49 +0200 Subject: [PATCH 02/28] Add internal transfer functions --- .../src/lib.rs | 235 +++++++++++++++++- 1 file changed, 225 insertions(+), 10 deletions(-) diff --git a/examples/account-abstracted-smart-contract-wallet/src/lib.rs b/examples/account-abstracted-smart-contract-wallet/src/lib.rs index 5f89c7adb..7af8f8277 100644 --- a/examples/account-abstracted-smart-contract-wallet/src/lib.rs +++ b/examples/account-abstracted-smart-contract-wallet/src/lib.rs @@ -108,6 +108,119 @@ struct State { nonces_registry: StateMap, } +// Functions for creating, updating and querying the contract state. +impl State { + /// Creates a new state with no tokens. + fn empty(state_builder: &mut StateBuilder) -> Self { + State { + balances: state_builder.new_map(), + implementors: state_builder.new_map(), + nonces_registry: state_builder.new_map(), + } + } + + /// Get the current balance of a given token ID for a given address. + /// Results in an error if the token ID does not exist in the state. + /// Since this contract only contains NFTs, the balance will always be + /// either 1 or 0. + fn balance( + &mut self, + public_key: PublicKeyEd25519, + contract_address: ContractAddress, + token_id: ContractTokenId, + ) -> ContractResult { + let mut public_key_balances = + self.balances.entry(public_key).occupied_or(CustomContractError::InvalidPublicKey)?; + + let mut contract_token_balances = public_key_balances + .token_balances + .entry(contract_address) + .occupied_or(CustomContractError::InvalidContractAddress)?; + + let cis2_token_balance = contract_token_balances + .entry(token_id) + .occupied_or(CustomContractError::InvalidTokenId)?; + + Ok(cis2_token_balance.0.into()) + } + + /// Update the state with a transfer of some token. + /// Results in an error if the token ID does not exist in the state or if + /// the from address have insufficient tokens to do the transfer. + fn transfer( + &mut self, + from_public_key: PublicKeyEd25519, + to_public_key: PublicKeyEd25519, + contract_address: ContractAddress, + token_id: ContractTokenId, + amount: ContractTokenAmount, + ) -> ContractResult<()> { + let zero_token_amount = TokenAmountU256 { + 0: 0u8.into(), + }; + // A zero transfer does not modify the state. + if amount == zero_token_amount { + return Ok(()); + } + + { + let mut from_public_key_balances = self + .balances + .entry(from_public_key) + .occupied_or(CustomContractError::InvalidPublicKey)?; + + let mut from_contract_token_balances = from_public_key_balances + .token_balances + .entry(contract_address) + .occupied_or(CustomContractError::InvalidContractAddress)?; + + let mut from_cis2_token_balance = from_contract_token_balances + .entry(token_id.clone()) + .occupied_or(CustomContractError::InsufficientFunds)?; + + ensure!(*from_cis2_token_balance >= amount, ContractError::InsufficientFunds); + *from_cis2_token_balance -= amount; + } + + let mut to_public_key_balances = self + .balances + .entry(to_public_key) + .occupied_or(CustomContractError::InvalidPublicKey)?; + + let mut to_contract_token_balances = to_public_key_balances + .token_balances + .entry(contract_address) + .occupied_or(CustomContractError::InvalidContractAddress)?; + + let mut to_cis2_token_balance = + to_contract_token_balances.entry(token_id).or_insert(TokenAmountU256 { + 0: 0u8.into(), + }); + + *to_cis2_token_balance += amount; + + Ok(()) + } + + // /// 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) { + // SupportResult::SupportBy(addresses.to_vec()) + // } else { + // SupportResult::NoSupport + // } + // } + + // /// Set implementors for a given standard. + // fn set_implementors( + // &mut self, + // std_id: StandardIdentifierOwned, + // implementors: Vec, + // ) { + // self.implementors.insert(std_id, implementors); + // } +} + /// The different errors the contract can produce. #[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] pub enum CustomContractError { @@ -120,21 +233,26 @@ pub enum CustomContractError { LogMalformed, // -3 /// Invalid contract name. OnlyContract, + InvalidPublicKey, + InvalidContractAddress, + InvalidTokenId, + InsufficientFunds, } pub type ContractError = Cis2Error; pub type ContractResult = Result; +/// Mapping CustomContractError to ContractError +impl From for ContractError { + fn from(c: CustomContractError) -> Self { + Cis2Error::Custom(c) + } +} + #[init(contract = "account-abstracted-smart-contract-wallet")] fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { - let state = State { - balances: state_builder.new_map(), - implementors: state_builder.new_map(), - nonces_registry: state_builder.new_map(), - }; - - Ok(state) + Ok(State::empty(state_builder)) } /// @@ -194,10 +312,107 @@ fn deposit_cis2_tokens(ctx: &ReceiveContext, host: &mut Host) -> ReceiveR .entry(contract_sender_address) .or_insert_with(|| builder.new_map()); - let mut cis2_token_balance = contract_token_balances.entry(cis2_hook_param.token_id).or_insert_with(|| TokenAmountU256 { - 0: 0u8.into(), - }); + let mut cis2_token_balance = contract_token_balances + .entry(cis2_hook_param.token_id) + .or_insert_with(|| TokenAmountU256 { + 0: 0u8.into(), + }); *cis2_token_balance += cis2_hook_param.amount; + + Ok(()) +} + +/// +#[receive( + contract = "account-abstracted-smart-contract-wallet", + name = "internalTransferNativeCurrency", + parameter = "PublicKeyEd25519", + payable, + mutable +)] +fn internal_transfer_native_currency( + ctx: &ReceiveContext, + host: &mut Host, + amount: Amount, +) -> ReceiveResult<()> { + let beneficiary: PublicKeyEd25519 = ctx.parameter_cursor().get()?; + + let (state, builder) = host.state_and_builder(); + + let mut public_key_balances = + state.balances.entry(beneficiary).or_insert_with(|| PublicKeyState::empty(builder)); + + public_key_balances.native_balance = amount; + + Ok(()) +} + +/// The parameter type for the contract function `transfer`. +#[derive(Debug, Serialize, Clone, SchemaType)] +#[concordium(transparent)] +pub struct TransferParameter(#[concordium(size_length = 2)] pub Vec); + +/// A single transfer of some amount of a token. +// Note: For the serialization to be derived according to the CIS2 +// specification, the order of the fields cannot be changed. +#[derive(Debug, Serialize, Clone, SchemaType)] +pub struct Transfer { + /// The address owning the tokens being transferred. + pub from_public_key: PublicKeyEd25519, + /// The address receiving the tokens being transferred. + pub to_public_key: PublicKeyEd25519, + /// + pub contract_address: ContractAddress, + /// The ID of the token being transferred. + pub token_id: ContractTokenId, + /// The amount of tokens being transferred. + pub amount: ContractTokenAmount, +} + +/// +#[receive( + contract = "account-abstracted-smart-contract-wallet", + name = "internalTransferCis2Token", + parameter = "PublicKeyEd25519", + mutable +)] +fn internal_transfer_cis2_token(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { + // Parse the parameter. + let TransferParameter(transfers): TransferParameter = ctx.parameter_cursor().get()?; + + for Transfer { + from_public_key, + to_public_key, + contract_address, + token_id, + amount, + } in transfers + { + // TODO: this needs to be authorized by the singer instead. + // Authenticate the sender for this transfer + // Get the sender who invoked this contract function. + // let sender = ctx.sender(); + // ensure!(from == sender || state.is_operator(&sender, &from), ContractError::Unauthorized); + + // Update the contract state + host.state_mut().transfer( + from_public_key, + to_public_key, + contract_address, + token_id, + amount, + )?; + + // TODO: add events + // // Log transfer event + // logger.log(&WccdEvent::Cis2Event(Cis2Event::Transfer(TransferEvent { + // token_id, + // amount, + // from, + // to: to_address, + // })))?; + } + Ok(()) } From a5e78dd324ce8110fe84ddcadae982cc6cc93ae4 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Fri, 22 Mar 2024 11:00:56 +0200 Subject: [PATCH 03/28] Add query for native currency balance --- .../src/lib.rs | 260 ++++++++++++------ 1 file changed, 183 insertions(+), 77 deletions(-) diff --git a/examples/account-abstracted-smart-contract-wallet/src/lib.rs b/examples/account-abstracted-smart-contract-wallet/src/lib.rs index 7af8f8277..ad8cdcb46 100644 --- a/examples/account-abstracted-smart-contract-wallet/src/lib.rs +++ b/examples/account-abstracted-smart-contract-wallet/src/lib.rs @@ -5,39 +5,26 @@ use concordium_std::*; #[derive(SchemaType, Serialize)] pub struct VerificationParameter { pub public_key: PublicKeyEd25519, - pub signature: SignatureEd25519, - pub message: Vec, -} - -// specification, the order of the fields cannot be changed. -#[derive(Debug, Serialize, SchemaType)] -pub struct OnReceivingCis2Params { - /// The ID of the token received. - pub token_id: ContractTokenId, - /// The amount of tokens received. - pub amount: A, - /// The previous owner of the tokens. - pub from: Address, - /// Some extra information which was sent as part of the transfer. - pub data: AdditionalData, + pub signature: SignatureEd25519, + pub message: Vec, } /// Part of the parameter type for the contract function `permit`. /// Specifies the message that is signed. #[derive(SchemaType, Serialize)] -pub struct PermitMessage { +pub struct SigningMessage { /// The contract_address that the signature is intended for. pub contract_address: ContractAddress, /// A nonce to prevent replay attacks. - pub nonce: u64, + pub nonce: u64, /// A timestamp to make signatures expire. - pub timestamp: Timestamp, + pub timestamp: Timestamp, /// The entry_point that the signature is intended for. - pub entry_point: OwnedEntrypointName, + 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, + pub payload: Vec, } /// The parameter type for the contract function `permit`. @@ -47,9 +34,9 @@ pub struct PermitParam { /// Signature/s. The CIS3 standard supports multi-sig accounts. pub signature: AccountSignatures, /// Account that created the above signature. - pub signer: AccountAddress, + pub signer: AccountAddress, /// Message that was signed. - pub message: PermitMessage, + pub message: SigningMessage, } #[derive(Serialize)] @@ -57,7 +44,7 @@ pub struct PermitParamPartial { /// Signature/s. The CIS3 standard supports multi-sig accounts. pub signature: AccountSignatures, /// Account that created the above signature. - pub signer: AccountAddress, + pub signer: AccountAddress, } /// Contract token ID type. @@ -95,10 +82,10 @@ impl PublicKeyState { #[concordium(state_parameter = "S")] struct State { /// The state of addresses. - balances: StateMap, S>, + balances: StateMap, S>, /// A map with contract addresses providing implementations of additional /// standards. - implementors: StateMap, S>, + implementors: StateMap, S>, /// A registry to link an account to its next nonce. The nonce is used to /// prevent replay attacks of the signed message. The nonce is increased /// sequentially every time a signed message (corresponding to the @@ -113,8 +100,8 @@ impl State { /// Creates a new state with no tokens. fn empty(state_builder: &mut StateBuilder) -> Self { State { - balances: state_builder.new_map(), - implementors: state_builder.new_map(), + balances: state_builder.new_map(), + implementors: state_builder.new_map(), nonces_registry: state_builder.new_map(), } } @@ -123,31 +110,77 @@ impl State { /// Results in an error if the token ID does not exist in the state. /// Since this contract only contains NFTs, the balance will always be /// either 1 or 0. - fn balance( - &mut self, - public_key: PublicKeyEd25519, - contract_address: ContractAddress, - token_id: ContractTokenId, + fn balance_tokens( + &self, + public_key: &PublicKeyEd25519, + contract_address: &ContractAddress, + token_id: &ContractTokenId, ) -> ContractResult { - let mut public_key_balances = - self.balances.entry(public_key).occupied_or(CustomContractError::InvalidPublicKey)?; + let public_key_balances = + self.balances.get(public_key).ok_or(CustomContractError::InvalidPublicKey)?; - let mut contract_token_balances = public_key_balances + let contract_token_balances = public_key_balances .token_balances - .entry(contract_address) - .occupied_or(CustomContractError::InvalidContractAddress)?; + .get(contract_address) + .ok_or(CustomContractError::InvalidContractAddress)?; - let cis2_token_balance = contract_token_balances - .entry(token_id) - .occupied_or(CustomContractError::InvalidTokenId)?; + let cis2_token_balance = + contract_token_balances.get(token_id).ok_or(CustomContractError::InvalidTokenId)?; Ok(cis2_token_balance.0.into()) } + /// Get the current balance of a given token ID for a given address. + /// Results in an error if the token ID does not exist in the state. + /// Since this contract only contains NFTs, the balance will always be + /// either 1 or 0. + fn balance_native_currency(&self, public_key: &PublicKeyEd25519) -> ContractResult { + let public_key_balances = + self.balances.get(public_key).ok_or(CustomContractError::InvalidPublicKey)?; + + Ok(public_key_balances.native_balance) + } + /// Update the state with a transfer of some token. /// Results in an error if the token ID does not exist in the state or if /// the from address have insufficient tokens to do the transfer. - fn transfer( + fn transfer_native_currency( + &mut self, + from_public_key: PublicKeyEd25519, + to_public_key: PublicKeyEd25519, + amount: Amount, + ) -> ContractResult<()> { + // A zero transfer does not modify the state. + if amount == Amount::zero() { + return Ok(()); + } + + { + let mut from_public_key_native_balance = self + .balances + .entry(from_public_key) + .occupied_or(CustomContractError::InvalidPublicKey)? + .native_balance; + + ensure!(from_public_key_native_balance >= amount, ContractError::InsufficientFunds); + from_public_key_native_balance -= amount; + } + + let mut to_public_key_native_balance = self + .balances + .entry(to_public_key) + .occupied_or(CustomContractError::InvalidPublicKey)? + .native_balance; + + to_public_key_native_balance += amount; + + Ok(()) + } + + /// Update the state with a transfer of some token. + /// Results in an error if the token ID does not exist in the state or if + /// the from address have insufficient tokens to do the transfer. + fn transfer_cis2_tokens( &mut self, from_public_key: PublicKeyEd25519, to_public_key: PublicKeyEd25519, @@ -155,9 +188,7 @@ impl State { token_id: ContractTokenId, amount: ContractTokenAmount, ) -> ContractResult<()> { - let zero_token_amount = TokenAmountU256 { - 0: 0u8.into(), - }; + let zero_token_amount = TokenAmountU256(0u8.into()); // A zero transfer does not modify the state. if amount == zero_token_amount { return Ok(()); @@ -193,9 +224,7 @@ impl State { .occupied_or(CustomContractError::InvalidContractAddress)?; let mut to_cis2_token_balance = - to_contract_token_balances.entry(token_id).or_insert(TokenAmountU256 { - 0: 0u8.into(), - }); + to_contract_token_balances.entry(token_id).or_insert(TokenAmountU256(0u8.into())); *to_cis2_token_balance += amount; @@ -203,8 +232,9 @@ impl State { } // /// 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) { + // fn have_implementors(&self, std_id: &StandardIdentifierOwned) -> + // SupportResult { if let Some(addresses) = + // self.implementors.get(std_id) { // SupportResult::SupportBy(addresses.to_vec()) // } else { // SupportResult::NoSupport @@ -245,21 +275,20 @@ pub type ContractResult = Result; /// Mapping CustomContractError to ContractError impl From for ContractError { - fn from(c: CustomContractError) -> Self { - Cis2Error::Custom(c) - } + fn from(c: CustomContractError) -> Self { Cis2Error::Custom(c) } } -#[init(contract = "account-abstracted-smart-contract-wallet")] +#[init(contract = "smart-contract-wallet", error = "CustomContractError")] fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { Ok(State::empty(state_builder)) } /// #[receive( - contract = "account-abstracted-smart-contract-wallet", + contract = "smart-contract-wallet", name = "depositNativeCurrency", parameter = "PublicKeyEd25519", + error = "CustomContractError", payable, mutable )] @@ -277,14 +306,17 @@ fn deposit_native_currency( public_key_balances.native_balance = amount; + // TODO: emit event + Ok(()) } /// #[receive( - contract = "account-abstracted-smart-contract-wallet", + contract = "smart-contract-wallet", name = "depositCis2Tokens", parameter = "PublicKeyEd25519", + error = "CustomContractError", mutable )] fn deposit_cis2_tokens(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { @@ -314,36 +346,45 @@ fn deposit_cis2_tokens(ctx: &ReceiveContext, host: &mut Host) -> ReceiveR let mut cis2_token_balance = contract_token_balances .entry(cis2_hook_param.token_id) - .or_insert_with(|| TokenAmountU256 { - 0: 0u8.into(), - }); + .or_insert_with(|| TokenAmountU256(0u8.into())); *cis2_token_balance += cis2_hook_param.amount; - + + // TODO: emit event + Ok(()) } /// #[receive( - contract = "account-abstracted-smart-contract-wallet", + contract = "smart-contract-wallet", name = "internalTransferNativeCurrency", parameter = "PublicKeyEd25519", + error = "CustomContractError", payable, mutable )] fn internal_transfer_native_currency( - ctx: &ReceiveContext, - host: &mut Host, - amount: Amount, + _ctx: &ReceiveContext, + _host: &mut Host, + _amount: Amount, ) -> ReceiveResult<()> { - let beneficiary: PublicKeyEd25519 = ctx.parameter_cursor().get()?; + // TODO: this needs to be authorized by the singer instead. + // Authenticate the sender for this transfer + // Get the sender who invoked this contract function. + // let sender = ctx.sender(); + // ensure!(from == sender || state.is_operator(&sender, &from), + // ContractError::Unauthorized); - let (state, builder) = host.state_and_builder(); + // let beneficiary: PublicKeyEd25519 = ctx.parameter_cursor().get()?; - let mut public_key_balances = - state.balances.entry(beneficiary).or_insert_with(|| PublicKeyState::empty(builder)); + // let (state, builder) = host.state_and_builder(); - public_key_balances.native_balance = amount; + // let mut public_key_balances = + // state.balances.entry(beneficiary).or_insert_with(|| + // PublicKeyState::empty(builder)); + + // public_key_balances.native_balance = amount; Ok(()) } @@ -359,25 +400,29 @@ pub struct TransferParameter(#[concordium(size_length = 2)] pub Vec); #[derive(Debug, Serialize, Clone, SchemaType)] pub struct Transfer { /// The address owning the tokens being transferred. - pub from_public_key: PublicKeyEd25519, + pub from_public_key: PublicKeyEd25519, /// The address receiving the tokens being transferred. - pub to_public_key: PublicKeyEd25519, + pub to_public_key: PublicKeyEd25519, /// pub contract_address: ContractAddress, /// The ID of the token being transferred. - pub token_id: ContractTokenId, + pub token_id: ContractTokenId, /// The amount of tokens being transferred. - pub amount: ContractTokenAmount, + pub amount: ContractTokenAmount, } /// #[receive( - contract = "account-abstracted-smart-contract-wallet", - name = "internalTransferCis2Token", + contract = "smart-contract-wallet", + name = "internalTransferCis2Tokens", parameter = "PublicKeyEd25519", + error = "CustomContractError", mutable )] -fn internal_transfer_cis2_token(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { +fn internal_transfer_cis2_tokens( + ctx: &ReceiveContext, + host: &mut Host, +) -> ReceiveResult<()> { // Parse the parameter. let TransferParameter(transfers): TransferParameter = ctx.parameter_cursor().get()?; @@ -393,10 +438,11 @@ fn internal_transfer_cis2_token(ctx: &ReceiveContext, host: &mut Host) -> // Authenticate the sender for this transfer // Get the sender who invoked this contract function. // let sender = ctx.sender(); - // ensure!(from == sender || state.is_operator(&sender, &from), ContractError::Unauthorized); + // ensure!(from == sender || state.is_operator(&sender, &from), + // ContractError::Unauthorized); // Update the contract state - host.state_mut().transfer( + host.state_mut().transfer_cis2_tokens( from_public_key, to_public_key, contract_address, @@ -416,3 +462,63 @@ fn internal_transfer_cis2_token(ctx: &ReceiveContext, host: &mut Host) -> Ok(()) } + +/// A query for the balance of a given address for a given token. +// Note: For the serialization to be derived according to the CIS2 +// specification, the order of the fields cannot be changed. +#[derive(Debug, Serialize, SchemaType)] +pub struct NativeCurrencyBalanceOfQuery { + /// The public key for which to query the balance of. + pub public_key: PublicKeyEd25519, +} + +/// The parameter type for the contract function `balanceOfNativeCurrency`. +// Note: For the serialization to be derived according to the CIS2 +// specification, the order of the fields cannot be changed. +#[derive(Debug, Serialize, SchemaType)] +#[concordium(transparent)] +pub struct NativeCurrencyBalanceOfParameter { + /// List of balance queries. + #[concordium(size_length = 2)] + pub queries: Vec, +} + +/// The response which is sent back when calling the contract function +/// `balanceOfNativeCurrency`. +/// It consists of the list of results corresponding to the list of queries. +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] +#[concordium(transparent)] +pub struct NativeCurrencyBalanceOfResponse(#[concordium(size_length = 2)] pub Vec); + +impl From> for NativeCurrencyBalanceOfResponse { + fn from(results: Vec) -> Self { NativeCurrencyBalanceOfResponse(results) } +} + +/// Get the balance of given token IDs and addresses. +/// +/// It rejects if: +/// - It fails to parse the parameter. +/// - Any of the queried `token_id` does not exist. +#[receive( + contract = "smart-contract-wallet", + name = "balanceOfNativeCurrency", + parameter = "NativeCurrencyBalanceOfParameter", + return_value = "NativeCurrencyBalanceOfResponse", + error = "CustomContractError" +)] +fn contract_balance_of( + ctx: &ReceiveContext, + host: &Host, +) -> ContractResult { + // Parse the parameter. + let params: NativeCurrencyBalanceOfParameter = ctx.parameter_cursor().get()?; + // Build the response. + let mut response = Vec::with_capacity(params.queries.len()); + for query in params.queries { + // Query the state for balance. + let amount = host.state().balance_native_currency(&query.public_key)?; + response.push(amount); + } + let result = NativeCurrencyBalanceOfResponse::from(response); + Ok(result) +} From 0fdfabf11846b2d0b24bf8bbce8b04a1d430e69e Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Fri, 22 Mar 2024 15:44:14 +0200 Subject: [PATCH 04/28] Add balance tests --- .../Cargo.toml | 1 + .../src/lib.rs | 129 ++++-- .../tests/tests.rs | 370 +++++++++++------- examples/cis2-multi/src/lib.rs | 34 +- examples/cis2-multi/tests/tests.rs | 15 +- 5 files changed, 363 insertions(+), 186 deletions(-) diff --git a/examples/account-abstracted-smart-contract-wallet/Cargo.toml b/examples/account-abstracted-smart-contract-wallet/Cargo.toml index e4d6d0c44..e3557bebb 100644 --- a/examples/account-abstracted-smart-contract-wallet/Cargo.toml +++ b/examples/account-abstracted-smart-contract-wallet/Cargo.toml @@ -17,6 +17,7 @@ concordium-cis2 = {path = "../../concordium-cis2", default-features = false, fea [dev-dependencies] concordium-smart-contract-testing = {path = "../../contract-testing"} +cis2-multi = {path = "../cis2-multi"} rand = "0.8" [lib] diff --git a/examples/account-abstracted-smart-contract-wallet/src/lib.rs b/examples/account-abstracted-smart-contract-wallet/src/lib.rs index ad8cdcb46..2ecb2a5c6 100644 --- a/examples/account-abstracted-smart-contract-wallet/src/lib.rs +++ b/examples/account-abstracted-smart-contract-wallet/src/lib.rs @@ -112,22 +112,22 @@ impl State { /// either 1 or 0. fn balance_tokens( &self, - public_key: &PublicKeyEd25519, - contract_address: &ContractAddress, token_id: &ContractTokenId, + cis2_token_contract_address: &ContractAddress, + public_key: &PublicKeyEd25519, ) -> ContractResult { - let public_key_balances = - self.balances.get(public_key).ok_or(CustomContractError::InvalidPublicKey)?; - - let contract_token_balances = public_key_balances - .token_balances - .get(contract_address) - .ok_or(CustomContractError::InvalidContractAddress)?; - - let cis2_token_balance = - contract_token_balances.get(token_id).ok_or(CustomContractError::InvalidTokenId)?; + let zero_token_amount = TokenAmountU256(0u8.into()); - Ok(cis2_token_balance.0.into()) + Ok(self + .balances + .get(public_key) + .map(|a| { + a.token_balances + .get(cis2_token_contract_address) + .map(|s| s.get(token_id).map(|s| *s).unwrap_or_else(|| zero_token_amount)) + .unwrap_or_else(|| zero_token_amount) + }) + .unwrap_or_else(|| zero_token_amount)) } /// Get the current balance of a given token ID for a given address. @@ -135,10 +135,7 @@ impl State { /// Since this contract only contains NFTs, the balance will always be /// either 1 or 0. fn balance_native_currency(&self, public_key: &PublicKeyEd25519) -> ContractResult { - let public_key_balances = - self.balances.get(public_key).ok_or(CustomContractError::InvalidPublicKey)?; - - Ok(public_key_balances.native_balance) + Ok(self.balances.get(public_key).map(|s| s.native_balance).unwrap_or_else(Amount::zero)) } /// Update the state with a transfer of some token. @@ -262,11 +259,11 @@ pub enum CustomContractError { /// Failed logging: Log is malformed. LogMalformed, // -3 /// Invalid contract name. - OnlyContract, - InvalidPublicKey, - InvalidContractAddress, - InvalidTokenId, - InsufficientFunds, + OnlyContract, // -4 + InvalidPublicKey, // -5 + InvalidContractAddress, // -6 + InvalidTokenId, // -7 + InsufficientFunds, // -8 } pub type ContractError = Cis2Error; @@ -278,14 +275,14 @@ impl From for ContractError { fn from(c: CustomContractError) -> Self { Cis2Error::Custom(c) } } -#[init(contract = "smart-contract-wallet", error = "CustomContractError")] +#[init(contract = "smart_contract_wallet", error = "CustomContractError")] fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { Ok(State::empty(state_builder)) } /// #[receive( - contract = "smart-contract-wallet", + contract = "smart_contract_wallet", name = "depositNativeCurrency", parameter = "PublicKeyEd25519", error = "CustomContractError", @@ -313,9 +310,13 @@ fn deposit_native_currency( /// #[receive( - contract = "smart-contract-wallet", + contract = "smart_contract_wallet", name = "depositCis2Tokens", - parameter = "PublicKeyEd25519", + parameter = "OnReceivingCis2DataParams< + ContractTokenId, + ContractTokenAmount, + PublicKeyEd25519, +>", error = "CustomContractError", mutable )] @@ -357,7 +358,7 @@ fn deposit_cis2_tokens(ctx: &ReceiveContext, host: &mut Host) -> ReceiveR /// #[receive( - contract = "smart-contract-wallet", + contract = "smart_contract_wallet", name = "internalTransferNativeCurrency", parameter = "PublicKeyEd25519", error = "CustomContractError", @@ -413,7 +414,7 @@ pub struct Transfer { /// #[receive( - contract = "smart-contract-wallet", + contract = "smart_contract_wallet", name = "internalTransferCis2Tokens", parameter = "PublicKeyEd25519", error = "CustomContractError", @@ -500,13 +501,13 @@ impl From> for NativeCurrencyBalanceOfResponse { /// - It fails to parse the parameter. /// - Any of the queried `token_id` does not exist. #[receive( - contract = "smart-contract-wallet", + contract = "smart_contract_wallet", name = "balanceOfNativeCurrency", parameter = "NativeCurrencyBalanceOfParameter", return_value = "NativeCurrencyBalanceOfResponse", error = "CustomContractError" )] -fn contract_balance_of( +fn contract_balance_of_native_currency( ctx: &ReceiveContext, host: &Host, ) -> ContractResult { @@ -522,3 +523,71 @@ fn contract_balance_of( let result = NativeCurrencyBalanceOfResponse::from(response); Ok(result) } + +/// A query for the balance of a given address for a given token. +// Note: For the serialization to be derived according to the CIS2 +// specification, the order of the fields cannot be changed. +#[derive(Debug, Serialize, SchemaType)] +pub struct Cis2TokensBalanceOfQuery { + /// The ID of the token for which to query the balance of. + pub token_id: ContractTokenId, + /// + pub cis2_token_contract_address: ContractAddress, + /// The public key for which to query the balance of. + pub public_key: PublicKeyEd25519, +} + +/// The parameter type for the contract function `balanceOf`. +// Note: For the serialization to be derived according to the CIS2 +// specification, the order of the fields cannot be changed. +#[derive(Debug, Serialize, SchemaType)] +#[concordium(transparent)] +pub struct Cis2TokensBalanceOfParameter { + /// List of balance queries. + #[concordium(size_length = 2)] + pub queries: Vec, +} + +/// The response which is sent back when calling the contract function +/// `balanceOf`. +/// It consists of the list of results corresponding to the list of queries. +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] +#[concordium(transparent)] +pub struct Cis2TokensBalanceOfResponse(#[concordium(size_length = 2)] pub Vec); + +impl From> for Cis2TokensBalanceOfResponse { + fn from(results: Vec) -> Self { Cis2TokensBalanceOfResponse(results) } +} + +/// Get the balance of given token IDs and addresses. +/// +/// It rejects if: +/// - It fails to parse the parameter. +/// - Any of the queried `token_id` does not exist. +#[receive( + contract = "smart_contract_wallet", + name = "balanceOfCis2Tokens", + parameter = "Cis2TokensBalanceOfParameter", + return_value = "Cis2TokensBalanceOfResponse", + error = "CustomContractError" +)] +fn contract_balance_of_cis2_tokens( + ctx: &ReceiveContext, + host: &Host, +) -> ContractResult { + // Parse the parameter. + let params: Cis2TokensBalanceOfParameter = ctx.parameter_cursor().get()?; + // Build the response. + let mut response = Vec::with_capacity(params.queries.len()); + for query in params.queries { + // Query the state for balance. + let amount = host.state().balance_tokens( + &query.token_id, + &query.cis2_token_contract_address, + &query.public_key, + )?; + response.push(amount); + } + let result = Cis2TokensBalanceOfResponse::from(response); + Ok(result) +} diff --git a/examples/account-abstracted-smart-contract-wallet/tests/tests.rs b/examples/account-abstracted-smart-contract-wallet/tests/tests.rs index 60404ecc9..74a0acf85 100644 --- a/examples/account-abstracted-smart-contract-wallet/tests/tests.rs +++ b/examples/account-abstracted-smart-contract-wallet/tests/tests.rs @@ -1,149 +1,233 @@ -//! Tests for the signature-verifier contract. +//! Tests for the `cis2_wCCD` contract. +use account_abstracted_smart_contract_wallet::*; +use cis2_multi::MintParams; +use concordium_cis2::*; use concordium_smart_contract_testing::*; -use concordium_std::*; +use concordium_std::PublicKeyEd25519; -const ALICE: AccountAddress = AccountAddress([0u8; 32]); +/// The tests accounts. +const ALICE: AccountAddress = AccountAddress([0; 32]); const ALICE_ADDR: Address = Address::Account(ALICE); -const SIGNER: Signer = Signer::with_one_key(); +const BOB: AccountAddress = AccountAddress([1; 32]); -// #[test] -// fn test_outside_signature_check() { -// let mut chain = Chain::new(); - -// // Create an account. -// chain.create_account(Account::new(ALICE, Amount::from_ccd(1000))); - -// // Load and deploy the module. -// let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists."); -// let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Module deploys."); - -// // Initialize the signature verifier contract. -// let init = chain -// .contract_init(SIGNER, ALICE, Energy::from(10_000), InitContractPayload { -// amount: Amount::zero(), -// mod_ref: deployment.module_reference, -// init_name: OwnedContractName::new_unchecked("init_signature-verifier".to_string()), -// param: OwnedParameter::empty(), -// }) -// .expect("Initialize signature verifier contract"); - -// // Construct a parameter with an invalid signature. -// let parameter_invalid = VerificationParameter { -// public_key: PublicKeyEd25519([0; 32]), -// signature: SignatureEd25519([1; 64]), -// message: vec![2; 100], -// }; - -// // Call the contract with the invalid signature. -// let update_invalid = chain -// .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { -// amount: Amount::zero(), -// address: init.contract_address, -// receive_name: OwnedReceiveName::new_unchecked("signature-verifier.verify".to_string()), -// message: OwnedParameter::from_serial(¶meter_invalid) -// .expect("Parameter has valid size."), -// }) -// .expect("Call signature verifier contract with an invalid signature."); -// // Check that the signature does NOT verify. -// let rv: bool = update_invalid.parse_return_value().expect("Deserializing bool"); -// assert!(!rv, "Signature verification should fail."); - -// // Construct a parameter with a valid signature. -// let parameter_valid = VerificationParameter { -// public_key: PublicKeyEd25519::from_str("35a2a8e52efad975dbf6580e7734e4f249eaa5ea8a763e934a8671cd7e446499").expect("Valid public key"), -// signature: SignatureEd25519::from_str("aaf2bfe0f7f74631850370422118f30e8787c5717a4a15527a5e1d0ffc791b663b1509b121022ef26086b37859001d09642674fa3be201f7d9dc2708f5e6ec02").expect("Valid signature"), -// message: b"Concordium".to_vec(), -// }; - -// // Call the contract with the valid signature. -// let update = chain -// .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { -// amount: Amount::zero(), -// address: init.contract_address, -// receive_name: OwnedReceiveName::new_unchecked("signature-verifier.verify".to_string()), -// message: OwnedParameter::from_serial(¶meter_valid) -// .expect("Parameter has valid size."), -// }) -// .expect("Call signature verifier contract with a valid signature."); -// // Check that the signature verifies. -// let rv: bool = update.parse_return_value().expect("Deserializing bool"); -// assert!(rv, "Signature checking failed."); -// } +const ALICE_PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([8; 32]); +const BOB_PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([9; 32]); + +const TOKEN_ID: TokenIdU8 = TokenIdU8(4); + +/// Initial balance of the accounts. +const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); + +const AIRDROP_TOKEN_AMOUNT: TokenAmountU64 = TokenAmountU64(100); + +/// A signer for all the transactions. +const SIGNER: Signer = Signer::with_one_key(); -// /// Tests that the signature verifier contract returns true when the signature -// /// is valid. The signature is generated in the test case. -// #[test] -// fn test_inside_signature_check() { -// let mut chain = Chain::new(); - -// // Create an account. -// chain.create_account(Account::new(ALICE, Amount::from_ccd(1000))); - -// // Load and deploy the module. -// let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists."); -// let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Module deploys."); - -// // Initialize the signature verifier contract. -// let init = chain -// .contract_init(SIGNER, ALICE, Energy::from(10_000), InitContractPayload { -// amount: Amount::zero(), -// mod_ref: deployment.module_reference, -// init_name: OwnedContractName::new_unchecked("init_signature-verifier".to_string()), -// param: OwnedParameter::empty(), -// }) -// .expect("Initialize signature verifier contract"); - -// use ed25519_dalek::{Signer, SigningKey}; - -// let rng = &mut rand::thread_rng(); - -// // Construct message, verifying_key, and signature. -// let message: &[u8] = b"Concordium"; -// let signing_key = SigningKey::generate(rng); -// let verifying_key = signing_key.verifying_key(); -// let signature = signing_key.sign(&message); - -// // Construct a parameter with an invalid signature. -// let parameter_invalid = VerificationParameter { -// public_key: PublicKeyEd25519(verifying_key.to_bytes()), -// signature: SignatureEd25519(signature.to_bytes()), -// message: b"wrong_message".to_vec(), -// }; - -// // Call the contract with the invalid signature. -// let update_invalid = chain -// .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { -// amount: Amount::zero(), -// address: init.contract_address, -// receive_name: OwnedReceiveName::new_unchecked("signature-verifier.verify".to_string()), -// message: OwnedParameter::from_serial(¶meter_invalid) -// .expect("Parameter has valid size."), -// }) -// .expect("Call signature verifier contract with an invalid signature."); - -// // Check that the signature does NOT verify. -// let rv: bool = update_invalid.parse_return_value().expect("Deserializing bool"); -// assert!(!rv, "Signature verification should fail."); - -// // Construct a parameter with a valid signature. -// let parameter_valid = VerificationParameter { -// public_key: PublicKeyEd25519(verifying_key.to_bytes()), -// signature: SignatureEd25519(signature.to_bytes()), -// message: message.to_vec(), -// }; - -// // Call the contract with the valid signature. -// let update = chain -// .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10_000), UpdateContractPayload { -// amount: Amount::zero(), -// address: init.contract_address, -// receive_name: OwnedReceiveName::new_unchecked("signature-verifier.verify".to_string()), -// message: OwnedParameter::from_serial(¶meter_valid) -// .expect("Parameter has valid size."), -// }) -// .expect("Call signature verifier contract with a valid signature."); - -// // Check that the signature verifies. -// let rv: bool = update.parse_return_value().expect("Deserializing bool"); -// assert!(rv, "Signature checking failed."); +/// Test that init produces the correct logs. +#[test] +fn test_init() { + let (_chain, _smart_contract_wallet, _cis2_token_contract_address) = + initialize_chain_and_contract(); +} + +/// Test depositing of native currency. +#[test] +fn test_deposit_native_currency() { + let (mut chain, smart_contract_wallet, _cis2_token_contract_address) = + initialize_chain_and_contract(); + + // Check that Alice has 0 CCD and Bob has 0 CCD on their public keys. + let balances = get_native_currency_balance_from_alice_and_bob(&chain, smart_contract_wallet); + assert_eq!(balances.0, [Amount::zero(), Amount::zero()]); + + let send_amount = Amount::from_micro_ccd(100); + chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: send_amount, + receive_name: OwnedReceiveName::new_unchecked( + "smart_contract_wallet.depositNativeCurrency".to_string(), + ), + address: smart_contract_wallet, + message: OwnedParameter::from_serial(&ALICE_PUBLIC_KEY) + .expect("Deposit native currency params"), + }) + .expect("Should be able to deposit CCD"); + + // Check that Alice now has 100 CCD and Bob has 0 CCD on their public keys. + let balances = get_native_currency_balance_from_alice_and_bob(&chain, smart_contract_wallet); + assert_eq!(balances.0, [send_amount, Amount::zero()]); +} + +/// Test depositing of cis2 tokens. +#[test] +fn test_deposit_cis2_tokens() { + let (mut chain, smart_contract_wallet, cis2_token_contract_address) = + initialize_chain_and_contract(); + + let new_metadata_url = "https://new-url.com".to_string(); + + let mint_param: MintParams = MintParams { + to: Receiver::Contract( + smart_contract_wallet, + OwnedEntrypointName::new_unchecked("depositCis2Tokens".to_string()), + ), + metadata_url: MetadataUrl { + url: new_metadata_url.clone(), + hash: None, + }, + token_id: TOKEN_ID, + data: AdditionalData::from(to_bytes(&ALICE_PUBLIC_KEY)), + }; + + // Check that Alice has 0 tokens and Bob has 0 tokens on their public keys. + let balances = get_cis2_tokens_balances_from_alice_and_bob( + &chain, + smart_contract_wallet, + cis2_token_contract_address, + ); + assert_eq!(balances.0, [TokenAmountU256(0u8.into()), TokenAmountU256(0u8.into())]); + + 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: cis2_token_contract_address, + message: OwnedParameter::from_serial(&mint_param) + .expect("Mint cis2 tokens params"), + }) + .expect("Should be able to deposit CCD"); + + // Check that Alice now has 100 tokens and Bob has 0 tokens on their public + // keys. + let balances = get_cis2_tokens_balances_from_alice_and_bob( + &chain, + smart_contract_wallet, + cis2_token_contract_address, + ); + assert_eq!(balances.0, [ + TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()), + TokenAmountU256(0u8.into()) + ]); +} + +// Helpers: + +/// Setup chain and contract. +/// +/// Also creates the two accounts, Alice and Bob. +/// +/// Alice is the owner of the contract. +fn initialize_chain_and_contract() -> (Chain, ContractAddress, ContractAddress) { + let mut chain = Chain::new(); + + // Create some accounts on the chain. + chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); + + // Load and deploy cis2 token module. + let module = + module_load_v1("../cis2-multi/concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); + + // Initialize the auction contract. + let cis2_token_contract_init = chain + .contract_init(SIGNER, ALICE, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_cis2_multi".to_string()), + param: OwnedParameter::from_serial(&AIRDROP_TOKEN_AMOUNT) + .expect("Token amount params"), + }) + .expect("Initialize contract"); + + // Load and deploy the module. + let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); + let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); + + // Initialize the auction contract. + let smart_contract_wallet_init = chain + .contract_init(SIGNER, ALICE, Energy::from(10000), InitContractPayload { + amount: Amount::zero(), + mod_ref: deployment.module_reference, + init_name: OwnedContractName::new_unchecked("init_smart_contract_wallet".to_string()), + param: OwnedParameter::empty(), + }) + .expect("Initialize contract"); + + (chain, smart_contract_wallet_init.contract_address, cis2_token_contract_init.contract_address) +} + +/// Get the token balances for Alice and Bob. +fn get_cis2_tokens_balances_from_alice_and_bob( + chain: &Chain, + contract_address: ContractAddress, + cis2_token_contract_address: ContractAddress, +) -> Cis2TokensBalanceOfResponse { + let balance_of_params = Cis2TokensBalanceOfParameter { + queries: vec![ + Cis2TokensBalanceOfQuery { + token_id: TokenIdVec(vec![4]), + cis2_token_contract_address, + public_key: ALICE_PUBLIC_KEY, + }, + Cis2TokensBalanceOfQuery { + token_id: TokenIdVec(vec![4]), + cis2_token_contract_address, + public_key: BOB_PUBLIC_KEY, + }, + ], + }; + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "smart_contract_wallet.balanceOfCis2Tokens".to_string(), + ), + address: contract_address, + message: OwnedParameter::from_serial(&balance_of_params) + .expect("BalanceOf params"), + }) + .expect("Invoke balanceOf"); + let rv: Cis2TokensBalanceOfResponse = + invoke.parse_return_value().expect("BalanceOf return value"); + rv +} + +/// Get the native currency balances for Alice and Bob. +fn get_native_currency_balance_from_alice_and_bob( + chain: &Chain, + contract_address: ContractAddress, +) -> NativeCurrencyBalanceOfResponse { + let balance_of_params = NativeCurrencyBalanceOfParameter { + queries: vec![ + NativeCurrencyBalanceOfQuery { + public_key: ALICE_PUBLIC_KEY, + }, + NativeCurrencyBalanceOfQuery { + public_key: BOB_PUBLIC_KEY, + }, + ], + }; + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "smart_contract_wallet.balanceOfNativeCurrency".to_string(), + ), + address: contract_address, + message: OwnedParameter::from_serial(&balance_of_params) + .expect("BalanceOf native currency params"), + }) + .expect("Invoke balanceOf native currency"); + let rv: NativeCurrencyBalanceOfResponse = + invoke.parse_return_value().expect("BalanceOf native currency return value"); + rv +} + +// /// Deserialize the events from an update. +// fn deserialize_update_events(update: &ContractInvokeSuccess) -> +// Vec { update +// .events() +// .flat_map(|(_addr, events)| events.iter().map(|e| +// e.parse().expect("Deserialize event"))) .collect() // } diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index 1dc49ce7a..6cdd53e69 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -320,14 +320,16 @@ pub type ContractTokenAmount = TokenAmountU64; /// The parameter for the contract function `mint` which mints/airdrops a number /// of tokens to the owner's address. -#[derive(Serialize, SchemaType)] +#[derive(Serialize, SchemaType, Clone)] pub struct MintParams { /// Owner of the newly minted tokens. - pub owner: Address, + pub to: Receiver, /// The metadata_url of the token. pub metadata_url: MetadataUrl, /// The token_id to mint/create additional tokens. pub token_id: ContractTokenId, + /// Additional data that can be sent to the receiving contract. + pub data: AdditionalData, } /// The parameter for the contract function `burn` which burns a number @@ -952,11 +954,16 @@ fn contract_view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult, logger: &mut impl HasLogger, ) -> ContractResult<()> { - let is_blacklisted = host.state().blacklist.contains(&get_canonical_address(params.owner)?); + let to_address = match params.to { + Receiver::Account(address) => Address::Account(address), + Receiver::Contract(address, _) => Address::Contract(address), + }; + + let is_blacklisted = host.state().blacklist.contains(&get_canonical_address(to_address)?); // Check token owner is not blacklisted. ensure!(!is_blacklisted, CustomContractError::Blacklisted.into()); @@ -970,7 +977,7 @@ fn mint( let token_metadata = state.mint( ¶ms.token_id, ¶ms.metadata_url, - ¶ms.owner, + &to_address, state.mint_airdrop, builder, ); @@ -979,7 +986,7 @@ fn mint( logger.log(&Cis2Event::Mint(MintEvent { token_id: params.token_id, amount: state.mint_airdrop, - owner: params.owner, + owner: to_address, }))?; // Metadata URL for the token. @@ -1030,7 +1037,18 @@ fn contract_mint( // ensure!(host.state().has_role(&sender, Roles::MINTER), // ContractError::Unauthorized); - mint(params, host, logger)?; + mint(¶ms, host, logger)?; + + // If the receiver is a contract: invoke the receive hook function. + if let Receiver::Contract(address, function) = params.to { + let parameter = OnReceivingCis2Params { + token_id: params.token_id, + amount: host.state.mint_airdrop, + from: Address::Contract(ctx.self_address()), + data: params.data, + }; + host.invoke_contract(&address, ¶meter, function.as_entrypoint_name(), Amount::zero())?; + } Ok(()) } @@ -1383,7 +1401,7 @@ fn contract_permit( // ContractError::Unauthorized // ); - mint(params, host, logger)?; + mint(¶ms, host, logger)?; } BURN_ENTRYPOINT => { // Burn tokens. diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs index 626c10f34..90d1c43d0 100644 --- a/examples/cis2-multi/tests/tests.rs +++ b/examples/cis2-multi/tests/tests.rs @@ -313,12 +313,13 @@ fn test_permit_mint() { // Create input parameters for the `mint` function. let payload = MintParams { - owner: ALICE_ADDR, + to: Receiver::from_account(ALICE), metadata_url: MetadataUrl { url: "https://some.example/token/2A".to_string(), hash: None, }, token_id: TOKEN_1, + data: AdditionalData::empty(), }; let update = @@ -893,12 +894,13 @@ fn test_token_balance_of_blacklisted_address_can_not_change() { // Bob cannot mint tokens to its address. let mint_params = MintParams { - owner: BOB_ADDR, + to: Receiver::from_account(BOB), token_id: TOKEN_0, metadata_url: MetadataUrl { url: "https://some.example/token/02".to_string(), hash: None, }, + data: AdditionalData::empty(), }; let update = chain @@ -1101,12 +1103,13 @@ fn test_no_execution_of_state_mutative_functions_when_paused() { // Try to mint tokens. let params = MintParams { - owner: ALICE_ADDR, + to: Receiver::from_account(ALICE), metadata_url: MetadataUrl { url: "https://some.example/token/02".to_string(), hash: None, }, token_id: TOKEN_0, + data: AdditionalData::empty(), }; let update_operator = chain @@ -1306,12 +1309,13 @@ fn initialize_contract_with_alice_tokens( let (mut chain, keypairs, contract_address, module_reference) = initialize_chain_and_contract(); let mint_params = MintParams { - owner: ALICE_ADDR, + to: Receiver::from_account(ALICE), token_id: TOKEN_0, metadata_url: MetadataUrl { url: "https://some.example/token/02".to_string(), hash: None, }, + data: AdditionalData::empty(), }; // Mint/airdrop TOKEN_0 to Alice as the owner. @@ -1325,12 +1329,13 @@ fn initialize_contract_with_alice_tokens( .expect("Mint tokens"); let mint_params = MintParams { - owner: ALICE_ADDR, + to: Receiver::from_account(ALICE), token_id: TOKEN_1, metadata_url: MetadataUrl { url: "https://some.example/token/2A".to_string(), hash: None, }, + data: AdditionalData::empty(), }; // Mint/airdrop TOKEN_1 to Alice as the owner. From 3a9aa0daa691938a94e6a1042982cb02e5a49bab Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Fri, 22 Mar 2024 16:43:56 +0200 Subject: [PATCH 05/28] Add events --- .../src/lib.rs | 161 ++++++++++++++++-- .../tests/tests.rs | 50 ++++-- 2 files changed, 185 insertions(+), 26 deletions(-) diff --git a/examples/account-abstracted-smart-contract-wallet/src/lib.rs b/examples/account-abstracted-smart-contract-wallet/src/lib.rs index 2ecb2a5c6..581cb24c2 100644 --- a/examples/account-abstracted-smart-contract-wallet/src/lib.rs +++ b/examples/account-abstracted-smart-contract-wallet/src/lib.rs @@ -55,6 +55,117 @@ pub type ContractTokenId = TokenIdVec; /// most 1 and it is fine to use a small type for representing token amounts. pub type ContractTokenAmount = TokenAmountU256; +/// Tagged events to be serialized for the event log. +#[derive(Debug, Serial, Deserial, PartialEq, Eq, SchemaType)] +#[concordium(repr(u8))] +pub enum Event { + /// The event tracks when a role is revoked from an address. + #[concordium(tag = 244)] + InternalCis2TokensTransfer(InternalCis2TokensTransferEvent), + /// The event tracks when a role is revoked from an address. + #[concordium(tag = 245)] + InternalNativeCurrencyTransferEvent(InternalNativeCurrencyTransferEvent), + /// The event tracks when a role is revoked from an address. + #[concordium(tag = 246)] + WithdrawCis2Tokens(WithdrawCis2TokensEvent), + /// The event tracks when a role is revoked from an address. + #[concordium(tag = 247)] + WithdrawNativeCurrency(WithdrawNativeCurrencyEvent), + /// The event tracks when a role is revoked from an address. + #[concordium(tag = 248)] + DepositCis2Tokens(DepositCis2TokensEvent), + /// The event tracks when a role is revoked from an address. + #[concordium(tag = 249)] + DepositNativeCurrency(DepositNativeCurrencyEvent), + /// 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), +} + +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] +pub struct InternalCis2TokensTransferEvent { + /// Account that signed the `PermitMessage`. + pub token_amount: ContractTokenAmount, + /// The nonce that was used in the `PermitMessage`. + pub token_id: ContractTokenId, + /// The nonce that was used in the `PermitMessage`. + pub cis2_token_contract_address: ContractAddress, + /// The nonce that was used in the `PermitMessage`. + pub from: PublicKeyEd25519, + /// The nonce that was used in the `PermitMessage`. + pub to: PublicKeyEd25519, +} + +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] +pub struct InternalNativeCurrencyTransferEvent { + /// Account that signed the `PermitMessage`. + pub ccd_amount: Amount, + /// The nonce that was used in the `PermitMessage`. + pub from: PublicKeyEd25519, + /// The nonce that was used in the `PermitMessage`. + pub to: PublicKeyEd25519, +} + +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] +pub struct WithdrawCis2TokensEvent { + /// Account that signed the `PermitMessage`. + pub token_amount: ContractTokenAmount, + /// The nonce that was used in the `PermitMessage`. + pub token_id: ContractTokenId, + /// The nonce that was used in the `PermitMessage`. + pub cis2_token_contract_address: ContractAddress, + /// The nonce that was used in the `PermitMessage`. + pub from: PublicKeyEd25519, + /// The nonce that was used in the `PermitMessage`. + pub to: Address, +} + +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] +pub struct WithdrawNativeCurrencyEvent { + /// Account that signed the `PermitMessage`. + pub ccd_amount: Amount, + /// The nonce that was used in the `PermitMessage`. + pub from: PublicKeyEd25519, + /// The nonce that was used in the `PermitMessage`. + pub to: Address, +} + +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] +pub struct DepositCis2TokensEvent { + /// Account that signed the `PermitMessage`. + pub token_amount: ContractTokenAmount, + /// The nonce that was used in the `PermitMessage`. + pub token_id: ContractTokenId, + /// The nonce that was used in the `PermitMessage`. + pub cis2_token_contract_address: ContractAddress, + /// The nonce that was used in the `PermitMessage`. + pub from: Address, + /// The nonce that was used in the `PermitMessage`. + pub to: PublicKeyEd25519, +} + +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] +pub struct DepositNativeCurrencyEvent { + /// Account that signed the `PermitMessage`. + pub ccd_amount: Amount, + /// The nonce that was used in the `PermitMessage`. + pub from: Address, + /// The nonce that was used in the `PermitMessage`. + pub to: PublicKeyEd25519, +} + +/// The NonceEvent is logged when the `permit` function is invoked. The event +/// tracks the nonce used by the signer of the `PermitMessage`. +#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] +pub struct NonceEvent { + /// Account that signed the `PermitMessage`. + pub account: AccountAddress, + /// The nonce that was used in the `PermitMessage`. + pub nonce: u64, +} + /// The state for each address. #[derive(Serial, DeserialWithState, Deletable)] #[concordium(state_parameter = "S")] @@ -275,7 +386,7 @@ impl From for ContractError { fn from(c: CustomContractError) -> Self { Cis2Error::Custom(c) } } -#[init(contract = "smart_contract_wallet", error = "CustomContractError")] +#[init(contract = "smart_contract_wallet", event = "Event", error = "CustomContractError")] fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { Ok(State::empty(state_builder)) } @@ -286,6 +397,7 @@ fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitRe name = "depositNativeCurrency", parameter = "PublicKeyEd25519", error = "CustomContractError", + enable_logger, payable, mutable )] @@ -293,17 +405,22 @@ fn deposit_native_currency( ctx: &ReceiveContext, host: &mut Host, amount: Amount, + logger: &mut impl HasLogger, ) -> ReceiveResult<()> { - let beneficiary: PublicKeyEd25519 = ctx.parameter_cursor().get()?; + let to: PublicKeyEd25519 = ctx.parameter_cursor().get()?; let (state, builder) = host.state_and_builder(); let mut public_key_balances = - state.balances.entry(beneficiary).or_insert_with(|| PublicKeyState::empty(builder)); + state.balances.entry(to).or_insert_with(|| PublicKeyState::empty(builder)); public_key_balances.native_balance = amount; - // TODO: emit event + logger.log(&Event::DepositNativeCurrency(DepositNativeCurrencyEvent { + ccd_amount: amount, + from: ctx.sender(), + to, + }))?; Ok(()) } @@ -318,9 +435,14 @@ fn deposit_native_currency( PublicKeyEd25519, >", error = "CustomContractError", + enable_logger, mutable )] -fn deposit_cis2_tokens(ctx: &ReceiveContext, host: &mut Host) -> ReceiveResult<()> { +fn deposit_cis2_tokens( + ctx: &ReceiveContext, + host: &mut Host, + logger: &mut impl HasLogger, +) -> ReceiveResult<()> { let cis2_hook_param: OnReceivingCis2DataParams< ContractTokenId, ContractTokenAmount, @@ -346,12 +468,18 @@ fn deposit_cis2_tokens(ctx: &ReceiveContext, host: &mut Host) -> ReceiveR .or_insert_with(|| builder.new_map()); let mut cis2_token_balance = contract_token_balances - .entry(cis2_hook_param.token_id) + .entry(cis2_hook_param.token_id.clone()) .or_insert_with(|| TokenAmountU256(0u8.into())); *cis2_token_balance += cis2_hook_param.amount; - // TODO: emit event + logger.log(&Event::DepositCis2Tokens(DepositCis2TokensEvent { + token_amount: cis2_hook_param.amount, + token_id: cis2_hook_param.token_id, + cis2_token_contract_address: contract_sender_address, + from: cis2_hook_param.from, + to: cis2_hook_param.data, + }))?; Ok(()) } @@ -418,11 +546,13 @@ pub struct Transfer { name = "internalTransferCis2Tokens", parameter = "PublicKeyEd25519", error = "CustomContractError", + enable_logger, mutable )] fn internal_transfer_cis2_tokens( ctx: &ReceiveContext, host: &mut Host, + logger: &mut impl HasLogger, ) -> ReceiveResult<()> { // Parse the parameter. let TransferParameter(transfers): TransferParameter = ctx.parameter_cursor().get()?; @@ -447,18 +577,17 @@ fn internal_transfer_cis2_tokens( from_public_key, to_public_key, contract_address, - token_id, + token_id.clone(), amount, )?; - // TODO: add events - // // Log transfer event - // logger.log(&WccdEvent::Cis2Event(Cis2Event::Transfer(TransferEvent { - // token_id, - // amount, - // from, - // to: to_address, - // })))?; + logger.log(&Event::InternalCis2TokensTransfer(InternalCis2TokensTransferEvent { + token_amount: amount, + token_id, + cis2_token_contract_address: contract_address, + from: from_public_key, + to: to_public_key, + }))?; } Ok(()) diff --git a/examples/account-abstracted-smart-contract-wallet/tests/tests.rs b/examples/account-abstracted-smart-contract-wallet/tests/tests.rs index 74a0acf85..09a22e473 100644 --- a/examples/account-abstracted-smart-contract-wallet/tests/tests.rs +++ b/examples/account-abstracted-smart-contract-wallet/tests/tests.rs @@ -41,7 +41,7 @@ fn test_deposit_native_currency() { assert_eq!(balances.0, [Amount::zero(), Amount::zero()]); let send_amount = Amount::from_micro_ccd(100); - chain + let update = chain .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { amount: send_amount, receive_name: OwnedReceiveName::new_unchecked( @@ -56,6 +56,15 @@ fn test_deposit_native_currency() { // Check that Alice now has 100 CCD and Bob has 0 CCD on their public keys. let balances = get_native_currency_balance_from_alice_and_bob(&chain, smart_contract_wallet); assert_eq!(balances.0, [send_amount, Amount::zero()]); + + // Check that the logs are correct. + let events = deserialize_update_events_of_specified_contract(&update, smart_contract_wallet); + + assert_eq!(events, [Event::DepositNativeCurrency(DepositNativeCurrencyEvent { + ccd_amount: send_amount, + from: ALICE_ADDR, + to: ALICE_PUBLIC_KEY, + })]); } /// Test depositing of cis2 tokens. @@ -87,7 +96,7 @@ fn test_deposit_cis2_tokens() { ); assert_eq!(balances.0, [TokenAmountU256(0u8.into()), TokenAmountU256(0u8.into())]); - chain + 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()), @@ -108,6 +117,17 @@ fn test_deposit_cis2_tokens() { TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()), TokenAmountU256(0u8.into()) ]); + + // Check that the logs are correct. + let events = deserialize_update_events_of_specified_contract(&update, smart_contract_wallet); + + assert_eq!(events, [Event::DepositCis2Tokens(DepositCis2TokensEvent { + token_amount: TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()), + token_id: TokenIdVec(vec![TOKEN_ID.0]), + cis2_token_contract_address, + from: Address::Contract(cis2_token_contract_address), + to: ALICE_PUBLIC_KEY + })]); } // Helpers: @@ -166,12 +186,12 @@ fn get_cis2_tokens_balances_from_alice_and_bob( let balance_of_params = Cis2TokensBalanceOfParameter { queries: vec![ Cis2TokensBalanceOfQuery { - token_id: TokenIdVec(vec![4]), + token_id: TokenIdVec(vec![TOKEN_ID.0]), cis2_token_contract_address, public_key: ALICE_PUBLIC_KEY, }, Cis2TokensBalanceOfQuery { - token_id: TokenIdVec(vec![4]), + token_id: TokenIdVec(vec![TOKEN_ID.0]), cis2_token_contract_address, public_key: BOB_PUBLIC_KEY, }, @@ -225,9 +245,19 @@ fn get_native_currency_balance_from_alice_and_bob( } // /// Deserialize the events from an update. -// fn deserialize_update_events(update: &ContractInvokeSuccess) -> -// Vec { update -// .events() -// .flat_map(|(_addr, events)| events.iter().map(|e| -// e.parse().expect("Deserialize event"))) .collect() -// } +fn deserialize_update_events_of_specified_contract( + update: &ContractInvokeSuccess, + smart_contract_wallet: ContractAddress, +) -> Vec { + update + .events() + .flat_map(|(addr, events)| { + if addr == smart_contract_wallet { + Some(events.iter().map(|e| e.parse().expect("Deserialize event"))) + } else { + None + } + }) + .flatten() + .collect() +} From 937699ca318cea954d61eeb2681c9172c6f83407 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Fri, 22 Mar 2024 16:46:44 +0200 Subject: [PATCH 06/28] Rename --- examples/README.md | 2 +- .../Cargo.toml | 2 +- .../src/lib.rs | 0 .../tests/tests.rs | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename examples/{account-abstracted-smart-contract-wallet => smart-contract-wallet}/Cargo.toml (93%) rename examples/{account-abstracted-smart-contract-wallet => smart-contract-wallet}/src/lib.rs (100%) rename examples/{account-abstracted-smart-contract-wallet => smart-contract-wallet}/tests/tests.rs (100%) diff --git a/examples/README.md b/examples/README.md index a6a6b9cd6..caf4e7dee 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,7 +13,6 @@ the logic of the contract is reasonable, or safe. The list of contracts is as follows: -- [account-abstracted-smart-contract-wallet](./account-abstracted-smart-contract-wallet) An example of how to implement a smart contract wallet. - [account-signature-checks](./account-signature-checks) A simple contract that demonstrates how account signature checks can be performed in smart contracts. - [two-step-transfer](./two-step-transfer) A contract that acts like an account (can send, store and accept CCD), @@ -40,6 +39,7 @@ The list of contracts is as follows: - [proxy](./proxy) A proxy contract that can be put in front of another contract. It works with V0 as well as V1 smart contracts. - [recorder](./recorder) A contract that records account addresses, and has an entry point to invoke transfers to all those addresses. - [signature-verifier](./signature-verifier) An example of how to use `crypto_primitives`. The contract verifies an Ed25519 signature. +- [smart-contract-wallet](./smart-contract-wallet) An example of how to implement a smart contract wallet. - [nametoken](./nametoken) An example of how to register and manage names as tokens in a smart contract. - [voting](./voting) An example of how to conduct an election using a smart contract. - [transfer-policy-check](./transfer-policy-check) A contract that showcases how to use policies. diff --git a/examples/account-abstracted-smart-contract-wallet/Cargo.toml b/examples/smart-contract-wallet/Cargo.toml similarity index 93% rename from examples/account-abstracted-smart-contract-wallet/Cargo.toml rename to examples/smart-contract-wallet/Cargo.toml index e3557bebb..bd65c677b 100644 --- a/examples/account-abstracted-smart-contract-wallet/Cargo.toml +++ b/examples/smart-contract-wallet/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "account-abstracted-smart-contract-wallet" +name = "smart-contract-wallet" version = "0.1.0" authors = ["Concordium "] edition = "2021" diff --git a/examples/account-abstracted-smart-contract-wallet/src/lib.rs b/examples/smart-contract-wallet/src/lib.rs similarity index 100% rename from examples/account-abstracted-smart-contract-wallet/src/lib.rs rename to examples/smart-contract-wallet/src/lib.rs diff --git a/examples/account-abstracted-smart-contract-wallet/tests/tests.rs b/examples/smart-contract-wallet/tests/tests.rs similarity index 100% rename from examples/account-abstracted-smart-contract-wallet/tests/tests.rs rename to examples/smart-contract-wallet/tests/tests.rs From da7e4cc130cf64915eb0cbb7d7a71810c8c5216f Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Fri, 22 Mar 2024 16:57:28 +0200 Subject: [PATCH 07/28] Fix test cases --- examples/sponsored-tx-enabled-auction/tests/tests.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/sponsored-tx-enabled-auction/tests/tests.rs b/examples/sponsored-tx-enabled-auction/tests/tests.rs index 5bbc7494b..c7ef13ebb 100644 --- a/examples/sponsored-tx-enabled-auction/tests/tests.rs +++ b/examples/sponsored-tx-enabled-auction/tests/tests.rs @@ -149,12 +149,13 @@ fn full_auction_flow_with_cis3_permit_function() { // Airdrop tokens to ALICE. let parameter = cis2_multi::MintParams { - owner: concordium_smart_contract_testing::Address::Account(ALICE), + to: Receiver::from_account(ALICE), metadata_url: MetadataUrl { url: "https://some.example/token/0".to_string(), hash: None, }, token_id: concordium_cis2::TokenIdU8(1u8), + data: AdditionalData::empty(), }; let _update = chain @@ -320,12 +321,13 @@ fn full_auction_flow_with_several_bids() { // Airdrop tokens to ALICE. let parameter = cis2_multi::MintParams { - owner: concordium_smart_contract_testing::Address::Account(ALICE), + to: Receiver::from_account(ALICE), metadata_url: MetadataUrl { url: "https://some.example/token/0".to_string(), hash: None, }, token_id: concordium_cis2::TokenIdU8(1u8), + data: AdditionalData::empty(), }; let _update = chain @@ -429,12 +431,13 @@ fn full_auction_flow_with_several_bids() { // Airdrop tokens to BOB. let parameter = cis2_multi::MintParams { - owner: concordium_smart_contract_testing::Address::Account(BOB), + to: Receiver::from_account(BOB), metadata_url: MetadataUrl { url: "https://some.example/token/0".to_string(), hash: None, }, token_id: concordium_cis2::TokenIdU8(1u8), + data: AdditionalData::empty(), }; let _update = chain @@ -548,12 +551,13 @@ fn full_auction_flow_with_cis3_transfer_function() { // Airdrop tokens to ALICE. let parameter = cis2_multi::MintParams { - owner: concordium_smart_contract_testing::Address::Account(ALICE), + to: Receiver::from_account(ALICE), metadata_url: MetadataUrl { url: "https://some.example/token/0".to_string(), hash: None, }, token_id: concordium_cis2::TokenIdU8(1u8), + data: AdditionalData::empty(), }; let _update = chain From c47ce66a24863887a96c03f52ade0a93989adef5 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Fri, 22 Mar 2024 17:18:41 +0200 Subject: [PATCH 08/28] Optimized state --- examples/smart-contract-wallet/src/lib.rs | 108 ++++++++---------- examples/smart-contract-wallet/tests/tests.rs | 2 +- 2 files changed, 50 insertions(+), 60 deletions(-) diff --git a/examples/smart-contract-wallet/src/lib.rs b/examples/smart-contract-wallet/src/lib.rs index 581cb24c2..d534e6755 100644 --- a/examples/smart-contract-wallet/src/lib.rs +++ b/examples/smart-contract-wallet/src/lib.rs @@ -169,18 +169,16 @@ pub struct NonceEvent { /// The state for each address. #[derive(Serial, DeserialWithState, Deletable)] #[concordium(state_parameter = "S")] -struct PublicKeyState { +#[concordium(transparent)] +struct ContractAddressState { /// The amount of tokens owned by this address. - token_balances: StateMap, S>, - /// - native_balance: Amount, + balances: StateMap, S>, } -impl PublicKeyState { +impl ContractAddressState { fn empty(state_builder: &mut StateBuilder) -> Self { - PublicKeyState { - token_balances: state_builder.new_map(), - native_balance: Amount::zero(), + ContractAddressState { + balances: state_builder.new_map(), } } } @@ -193,7 +191,9 @@ impl PublicKeyState { #[concordium(state_parameter = "S")] struct State { /// The state of addresses. - balances: StateMap, S>, + token_balances: StateMap, S>, + /// + native_balances: StateMap, /// A map with contract addresses providing implementations of additional /// standards. implementors: StateMap, S>, @@ -211,7 +211,8 @@ impl State { /// Creates a new state with no tokens. fn empty(state_builder: &mut StateBuilder) -> Self { State { - balances: state_builder.new_map(), + native_balances: state_builder.new_map(), + token_balances: state_builder.new_map(), implementors: state_builder.new_map(), nonces_registry: state_builder.new_map(), } @@ -230,12 +231,12 @@ impl State { let zero_token_amount = TokenAmountU256(0u8.into()); Ok(self - .balances - .get(public_key) + .token_balances + .get(cis2_token_contract_address) .map(|a| { - a.token_balances - .get(cis2_token_contract_address) - .map(|s| s.get(token_id).map(|s| *s).unwrap_or_else(|| zero_token_amount)) + a.balances + .get(token_id) + .map(|s| s.get(public_key).map(|s| *s).unwrap_or_else(|| zero_token_amount)) .unwrap_or_else(|| zero_token_amount) }) .unwrap_or_else(|| zero_token_amount)) @@ -246,7 +247,7 @@ impl State { /// Since this contract only contains NFTs, the balance will always be /// either 1 or 0. fn balance_native_currency(&self, public_key: &PublicKeyEd25519) -> ContractResult { - Ok(self.balances.get(public_key).map(|s| s.native_balance).unwrap_or_else(Amount::zero)) + Ok(self.native_balances.get(public_key).map(|s| *s).unwrap_or_else(Amount::zero)) } /// Update the state with a transfer of some token. @@ -265,20 +266,20 @@ impl State { { let mut from_public_key_native_balance = self - .balances + .native_balances .entry(from_public_key) - .occupied_or(CustomContractError::InvalidPublicKey)? - .native_balance; + .occupied_or(CustomContractError::InvalidPublicKey) + .map(|x| *x)?; ensure!(from_public_key_native_balance >= amount, ContractError::InsufficientFunds); from_public_key_native_balance -= amount; } let mut to_public_key_native_balance = self - .balances + .native_balances .entry(to_public_key) - .occupied_or(CustomContractError::InvalidPublicKey)? - .native_balance; + .occupied_or(CustomContractError::InvalidPublicKey) + .map(|x| *x)?; to_public_key_native_balance += amount; @@ -302,37 +303,28 @@ impl State { return Ok(()); } - { - let mut from_public_key_balances = self - .balances - .entry(from_public_key) - .occupied_or(CustomContractError::InvalidPublicKey)?; - - let mut from_contract_token_balances = from_public_key_balances - .token_balances - .entry(contract_address) - .occupied_or(CustomContractError::InvalidContractAddress)?; + let mut contract_balances = self + .token_balances + .entry(contract_address) + .occupied_or(CustomContractError::InvalidPublicKey)?; - let mut from_cis2_token_balance = from_contract_token_balances - .entry(token_id.clone()) - .occupied_or(CustomContractError::InsufficientFunds)?; + let mut token_balances = contract_balances + .balances + .entry(token_id) + .occupied_or(CustomContractError::InvalidContractAddress)?; - ensure!(*from_cis2_token_balance >= amount, ContractError::InsufficientFunds); - *from_cis2_token_balance -= amount; - } + let mut from_cis2_token_balance = token_balances + .entry(from_public_key) + .occupied_or(CustomContractError::InsufficientFunds)?; - let mut to_public_key_balances = self - .balances - .entry(to_public_key) - .occupied_or(CustomContractError::InvalidPublicKey)?; + ensure!(*from_cis2_token_balance >= amount, ContractError::InsufficientFunds); + *from_cis2_token_balance -= amount; - let mut to_contract_token_balances = to_public_key_balances - .token_balances - .entry(contract_address) - .occupied_or(CustomContractError::InvalidContractAddress)?; + drop(from_cis2_token_balance); - let mut to_cis2_token_balance = - to_contract_token_balances.entry(token_id).or_insert(TokenAmountU256(0u8.into())); + let mut to_cis2_token_balance = token_balances + .entry(to_public_key) + .occupied_or(CustomContractError::InsufficientFunds)?; *to_cis2_token_balance += amount; @@ -409,12 +401,10 @@ fn deposit_native_currency( ) -> ReceiveResult<()> { let to: PublicKeyEd25519 = ctx.parameter_cursor().get()?; - let (state, builder) = host.state_and_builder(); - let mut public_key_balances = - state.balances.entry(to).or_insert_with(|| PublicKeyState::empty(builder)); + host.state_mut().native_balances.entry(to).or_insert_with(Amount::zero); - public_key_balances.native_balance = amount; + *public_key_balances = amount; logger.log(&Event::DepositNativeCurrency(DepositNativeCurrencyEvent { ccd_amount: amount, @@ -457,18 +447,18 @@ fn deposit_cis2_tokens( let (state, builder) = host.state_and_builder(); - let mut public_key_balances = state - .balances - .entry(cis2_hook_param.data) - .or_insert_with(|| PublicKeyState::empty(builder)); - - let mut contract_token_balances = public_key_balances + let mut contract_balances = state .token_balances .entry(contract_sender_address) + .or_insert_with(|| ContractAddressState::empty(builder)); + + let mut contract_token_balances = contract_balances + .balances + .entry(cis2_hook_param.token_id.clone()) .or_insert_with(|| builder.new_map()); let mut cis2_token_balance = contract_token_balances - .entry(cis2_hook_param.token_id.clone()) + .entry(cis2_hook_param.data) .or_insert_with(|| TokenAmountU256(0u8.into())); *cis2_token_balance += cis2_hook_param.amount; diff --git a/examples/smart-contract-wallet/tests/tests.rs b/examples/smart-contract-wallet/tests/tests.rs index 09a22e473..c08c1f661 100644 --- a/examples/smart-contract-wallet/tests/tests.rs +++ b/examples/smart-contract-wallet/tests/tests.rs @@ -1,9 +1,9 @@ //! Tests for the `cis2_wCCD` contract. -use account_abstracted_smart_contract_wallet::*; use cis2_multi::MintParams; use concordium_cis2::*; use concordium_smart_contract_testing::*; use concordium_std::PublicKeyEd25519; +use smart_contract_wallet::*; /// The tests accounts. const ALICE: AccountAddress = AccountAddress([0; 32]); From 0f04dada64e36b63c9e64507aa713f60ec110022 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Mon, 25 Mar 2024 09:45:32 +0200 Subject: [PATCH 09/28] Rename contract --- examples/README.md | 2 +- .../Cargo.toml | 0 .../src/lib.rs | 0 .../tests/tests.rs | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename examples/{smart-contract-wallet => cis5-smart-contract-wallet}/Cargo.toml (100%) rename examples/{smart-contract-wallet => cis5-smart-contract-wallet}/src/lib.rs (100%) rename examples/{smart-contract-wallet => cis5-smart-contract-wallet}/tests/tests.rs (100%) diff --git a/examples/README.md b/examples/README.md index caf4e7dee..584a6c7e2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -39,7 +39,7 @@ The list of contracts is as follows: - [proxy](./proxy) A proxy contract that can be put in front of another contract. It works with V0 as well as V1 smart contracts. - [recorder](./recorder) A contract that records account addresses, and has an entry point to invoke transfers to all those addresses. - [signature-verifier](./signature-verifier) An example of how to use `crypto_primitives`. The contract verifies an Ed25519 signature. -- [smart-contract-wallet](./smart-contract-wallet) An example of how to implement a smart contract wallet. +- [cis5-smart-contract-wallet](./cis5-smart-contract-wallet) An example of how to implement a CIS5 compatible smart contract wallet. - [nametoken](./nametoken) An example of how to register and manage names as tokens in a smart contract. - [voting](./voting) An example of how to conduct an election using a smart contract. - [transfer-policy-check](./transfer-policy-check) A contract that showcases how to use policies. diff --git a/examples/smart-contract-wallet/Cargo.toml b/examples/cis5-smart-contract-wallet/Cargo.toml similarity index 100% rename from examples/smart-contract-wallet/Cargo.toml rename to examples/cis5-smart-contract-wallet/Cargo.toml diff --git a/examples/smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs similarity index 100% rename from examples/smart-contract-wallet/src/lib.rs rename to examples/cis5-smart-contract-wallet/src/lib.rs diff --git a/examples/smart-contract-wallet/tests/tests.rs b/examples/cis5-smart-contract-wallet/tests/tests.rs similarity index 100% rename from examples/smart-contract-wallet/tests/tests.rs rename to examples/cis5-smart-contract-wallet/tests/tests.rs From 12d68969ac54f04003f8f2d4aebc687e1ae21759 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Mon, 25 Mar 2024 11:28:52 +0200 Subject: [PATCH 10/28] Add internal transfer cis2 tokens function --- .../cis5-smart-contract-wallet/src/lib.rs | 119 ++++++++----- .../cis5-smart-contract-wallet/tests/tests.rs | 162 ++++++++++++++---- 2 files changed, 203 insertions(+), 78 deletions(-) diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index d534e6755..59fd63fb3 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -322,9 +322,8 @@ impl State { drop(from_cis2_token_balance); - let mut to_cis2_token_balance = token_balances - .entry(to_public_key) - .occupied_or(CustomContractError::InsufficientFunds)?; + let mut to_cis2_token_balance = + token_balances.entry(to_public_key).or_insert_with(|| TokenAmountU256(0u8.into())); *to_cis2_token_balance += amount; @@ -508,33 +507,58 @@ fn internal_transfer_native_currency( Ok(()) } -/// The parameter type for the contract function `transfer`. -#[derive(Debug, Serialize, Clone, SchemaType)] -#[concordium(transparent)] -pub struct TransferParameter(#[concordium(size_length = 2)] pub Vec); - /// A single transfer of some amount of a token. // Note: For the serialization to be derived according to the CIS2 // specification, the order of the fields cannot be changed. #[derive(Debug, Serialize, Clone, SchemaType)] -pub struct Transfer { +pub struct Cis2TokensInternalTransferBatch { /// The address owning the tokens being transferred. - pub from_public_key: PublicKeyEd25519, + pub from: PublicKeyEd25519, /// The address receiving the tokens being transferred. - pub to_public_key: PublicKeyEd25519, - /// - pub contract_address: ContractAddress, - /// The ID of the token being transferred. - pub token_id: ContractTokenId, + pub to: PublicKeyEd25519, /// The amount of tokens being transferred. - pub amount: ContractTokenAmount, + pub token_amount: ContractTokenAmount, + /// The ID of the token being transferred. + pub token_id: ContractTokenId, + /// + pub cis2_token_contract_address: ContractAddress, +} + +#[derive(Debug, Serialize, Clone, SchemaType)] +pub struct Cis2TokensInternalTransfer { + /// The address owning the tokens being transferred. + pub signer: PublicKeyEd25519, + /// The address owning the tokens being transferred. + pub signature: SignatureEd25519, + /// The address owning the tokens being transferred. + pub expiry_time: Timestamp, + /// The address owning the tokens being transferred. + pub nonce: u64, + /// The address owning the tokens being transferred. + pub service_fee: ContractTokenAmount, + /// The address owning the tokens being transferred. + pub service_fee_recipient: Address, + /// List of balance queries. + #[concordium(size_length = 2)] + pub simple_transfers: Vec, +} + +/// The parameter type for the contract function `balanceOfNativeCurrency`. +// Note: For the serialization to be derived according to the CIS2 +// specification, the order of the fields cannot be changed. +#[derive(Debug, Serialize, SchemaType)] +#[concordium(transparent)] +pub struct Cis2TokensInternalTransferParameter { + /// List of balance queries. + #[concordium(size_length = 2)] + pub transfers: Vec, } /// #[receive( contract = "smart_contract_wallet", name = "internalTransferCis2Tokens", - parameter = "PublicKeyEd25519", + parameter = "Cis2TokensInternalTransferParameter", error = "CustomContractError", enable_logger, mutable @@ -545,15 +569,17 @@ fn internal_transfer_cis2_tokens( logger: &mut impl HasLogger, ) -> ReceiveResult<()> { // Parse the parameter. - let TransferParameter(transfers): TransferParameter = ctx.parameter_cursor().get()?; - - for Transfer { - from_public_key, - to_public_key, - contract_address, - token_id, - amount, - } in transfers + let param: Cis2TokensInternalTransferParameter = ctx.parameter_cursor().get()?; + + for Cis2TokensInternalTransfer { + signer: _signer, + signature: _signature, + expiry_time: _expiry_time, + nonce: _nonce, + service_fee: _service_fee, + service_fee_recipient: _service_fee_recipient, + simple_transfers, + } in param.transfers { // TODO: this needs to be authorized by the singer instead. // Authenticate the sender for this transfer @@ -562,22 +588,35 @@ fn internal_transfer_cis2_tokens( // ensure!(from == sender || state.is_operator(&sender, &from), // ContractError::Unauthorized); - // Update the contract state - host.state_mut().transfer_cis2_tokens( - from_public_key, - to_public_key, - contract_address, - token_id.clone(), - amount, - )?; + // TODO: Check signature and other parameters + + // TODO: transfer service fee - logger.log(&Event::InternalCis2TokensTransfer(InternalCis2TokensTransferEvent { - token_amount: amount, + for Cis2TokensInternalTransferBatch { + from, + to, + token_amount, token_id, - cis2_token_contract_address: contract_address, - from: from_public_key, - to: to_public_key, - }))?; + cis2_token_contract_address, + } in simple_transfers + { + // Update the contract state + host.state_mut().transfer_cis2_tokens( + from, + to, + cis2_token_contract_address, + token_id.clone(), + token_amount, + )?; + + logger.log(&Event::InternalCis2TokensTransfer(InternalCis2TokensTransferEvent { + token_amount, + token_id, + cis2_token_contract_address, + from, + to, + }))?; + } } Ok(()) diff --git a/examples/cis5-smart-contract-wallet/tests/tests.rs b/examples/cis5-smart-contract-wallet/tests/tests.rs index c08c1f661..a53240ce4 100644 --- a/examples/cis5-smart-contract-wallet/tests/tests.rs +++ b/examples/cis5-smart-contract-wallet/tests/tests.rs @@ -2,17 +2,25 @@ use cis2_multi::MintParams; use concordium_cis2::*; use concordium_smart_contract_testing::*; -use concordium_std::PublicKeyEd25519; +use concordium_std::{PublicKeyEd25519, SignatureEd25519}; use smart_contract_wallet::*; /// The tests accounts. const ALICE: AccountAddress = AccountAddress([0; 32]); const ALICE_ADDR: Address = Address::Account(ALICE); const BOB: AccountAddress = AccountAddress([1; 32]); +const SERVICE_FEE_RECIPIENT: AccountAddress = AccountAddress([2; 32]); +const SERVICE_FEE_RECIPIENT_ADDR: Address = Address::Account(SERVICE_FEE_RECIPIENT); const ALICE_PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([8; 32]); const BOB_PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([9; 32]); +const SIGNATURE: SignatureEd25519 = SignatureEd25519([ + 68, 134, 96, 171, 184, 199, 1, 93, 76, 87, 144, 68, 55, 180, 93, 56, 107, 95, 127, 112, 24, 55, + 162, 131, 165, 91, 133, 104, 2, 5, 78, 224, 214, 21, 66, 0, 44, 108, 52, 4, 108, 10, 123, 75, + 21, 68, 42, 79, 106, 106, 87, 125, 122, 77, 154, 114, 208, 145, 171, 47, 108, 96, 221, 13, +]); + const TOKEN_ID: TokenIdU8 = TokenIdU8(4); /// Initial balance of the accounts. @@ -73,60 +81,74 @@ fn test_deposit_cis2_tokens() { let (mut chain, smart_contract_wallet, cis2_token_contract_address) = initialize_chain_and_contract(); - let new_metadata_url = "https://new-url.com".to_string(); + alice_deposits_cis2_tokens(&mut chain, smart_contract_wallet, cis2_token_contract_address); +} - let mint_param: MintParams = MintParams { - to: Receiver::Contract( - smart_contract_wallet, - OwnedEntrypointName::new_unchecked("depositCis2Tokens".to_string()), - ), - metadata_url: MetadataUrl { - url: new_metadata_url.clone(), - hash: None, - }, - token_id: TOKEN_ID, - data: AdditionalData::from(to_bytes(&ALICE_PUBLIC_KEY)), - }; +/// Test internal transfer of cis2 tokens. +#[test] +fn test_internal_transfer_cis2_tokens() { + let (mut chain, smart_contract_wallet, cis2_token_contract_address) = + initialize_chain_and_contract(); - // Check that Alice has 0 tokens and Bob has 0 tokens on their public keys. - let balances = get_cis2_tokens_balances_from_alice_and_bob( - &chain, - smart_contract_wallet, - cis2_token_contract_address, - ); - assert_eq!(balances.0, [TokenAmountU256(0u8.into()), TokenAmountU256(0u8.into())]); + alice_deposits_cis2_tokens(&mut chain, smart_contract_wallet, cis2_token_contract_address); + + let service_fee_amount: TokenAmountU256 = TokenAmountU256(1.into()); + let transfer_amount: TokenAmountU256 = TokenAmountU256(5.into()); + let contract_token_id: TokenIdVec = TokenIdVec(vec![TOKEN_ID.0]); + + let internal_transfer_param = Cis2TokensInternalTransferParameter { + transfers: vec![Cis2TokensInternalTransfer { + signer: ALICE_PUBLIC_KEY, + signature: SIGNATURE, + expiry_time: Timestamp::now(), + nonce: 0u64, + service_fee: service_fee_amount, + service_fee_recipient: SERVICE_FEE_RECIPIENT_ADDR, + simple_transfers: vec![Cis2TokensInternalTransferBatch { + from: ALICE_PUBLIC_KEY, + to: BOB_PUBLIC_KEY, + token_amount: transfer_amount, + token_id: contract_token_id.clone(), + cis2_token_contract_address, + }], + }], + }; 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: cis2_token_contract_address, - message: OwnedParameter::from_serial(&mint_param) - .expect("Mint cis2 tokens params"), + receive_name: OwnedReceiveName::new_unchecked( + "smart_contract_wallet.internalTransferCis2Tokens".to_string(), + ), + address: smart_contract_wallet, + message: OwnedParameter::from_serial(&internal_transfer_param) + .expect("Internal transfer cis2 tokens params"), }) - .expect("Should be able to deposit CCD"); + .expect("Should be able to internally transfer cis2 tokens"); // Check that Alice now has 100 tokens and Bob has 0 tokens on their public // keys. + // TODO: check balance of service fee as well. let balances = get_cis2_tokens_balances_from_alice_and_bob( &chain, smart_contract_wallet, cis2_token_contract_address, ); assert_eq!(balances.0, [ - TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()), - TokenAmountU256(0u8.into()) + TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()) - transfer_amount, + TokenAmountU256(transfer_amount.into()) ]); - // Check that the logs are correct. + // TODO: there should be two events; check that serviceFee was transferred + // correctly to service_fee_recipient. Check that the logs are correct. let events = deserialize_update_events_of_specified_contract(&update, smart_contract_wallet); - assert_eq!(events, [Event::DepositCis2Tokens(DepositCis2TokensEvent { - token_amount: TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()), - token_id: TokenIdVec(vec![TOKEN_ID.0]), + assert_eq!(events, [Event::InternalCis2TokensTransfer(InternalCis2TokensTransferEvent { + token_amount: transfer_amount, + token_id: contract_token_id, cis2_token_contract_address, - from: Address::Contract(cis2_token_contract_address), - to: ALICE_PUBLIC_KEY + from: ALICE_PUBLIC_KEY, + to: BOB_PUBLIC_KEY })]); } @@ -180,7 +202,7 @@ fn initialize_chain_and_contract() -> (Chain, ContractAddress, ContractAddress) /// Get the token balances for Alice and Bob. fn get_cis2_tokens_balances_from_alice_and_bob( chain: &Chain, - contract_address: ContractAddress, + smart_contract_wallet: ContractAddress, cis2_token_contract_address: ContractAddress, ) -> Cis2TokensBalanceOfResponse { let balance_of_params = Cis2TokensBalanceOfParameter { @@ -203,7 +225,7 @@ fn get_cis2_tokens_balances_from_alice_and_bob( receive_name: OwnedReceiveName::new_unchecked( "smart_contract_wallet.balanceOfCis2Tokens".to_string(), ), - address: contract_address, + address: smart_contract_wallet, message: OwnedParameter::from_serial(&balance_of_params) .expect("BalanceOf params"), }) @@ -216,7 +238,7 @@ fn get_cis2_tokens_balances_from_alice_and_bob( /// Get the native currency balances for Alice and Bob. fn get_native_currency_balance_from_alice_and_bob( chain: &Chain, - contract_address: ContractAddress, + smart_contract_wallet: ContractAddress, ) -> NativeCurrencyBalanceOfResponse { let balance_of_params = NativeCurrencyBalanceOfParameter { queries: vec![ @@ -234,7 +256,7 @@ fn get_native_currency_balance_from_alice_and_bob( receive_name: OwnedReceiveName::new_unchecked( "smart_contract_wallet.balanceOfNativeCurrency".to_string(), ), - address: contract_address, + address: smart_contract_wallet, message: OwnedParameter::from_serial(&balance_of_params) .expect("BalanceOf native currency params"), }) @@ -244,6 +266,70 @@ fn get_native_currency_balance_from_alice_and_bob( rv } +/// Get the token balances for Alice and Bob. +fn alice_deposits_cis2_tokens( + chain: &mut Chain, + smart_contract_wallet: ContractAddress, + cis2_token_contract_address: ContractAddress, +) { + let new_metadata_url = "https://new-url.com".to_string(); + + let mint_param: MintParams = MintParams { + to: Receiver::Contract( + smart_contract_wallet, + OwnedEntrypointName::new_unchecked("depositCis2Tokens".to_string()), + ), + metadata_url: MetadataUrl { + url: new_metadata_url.clone(), + hash: None, + }, + token_id: TOKEN_ID, + data: AdditionalData::from(to_bytes(&ALICE_PUBLIC_KEY)), + }; + + // Check that Alice has 0 tokens and Bob has 0 tokens on their public keys. + let balances = get_cis2_tokens_balances_from_alice_and_bob( + &chain, + smart_contract_wallet, + cis2_token_contract_address, + ); + assert_eq!(balances.0, [TokenAmountU256(0u8.into()), TokenAmountU256(0u8.into())]); + + 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: cis2_token_contract_address, + message: OwnedParameter::from_serial(&mint_param) + .expect("Mint cis2 tokens params"), + }) + .expect("Should be able to deposit cis2 tokens"); + + // Check that Alice now has 100 tokens and Bob has 0 tokens on their public + // keys. + // TODO: check service_fee + let balances = get_cis2_tokens_balances_from_alice_and_bob( + &chain, + smart_contract_wallet, + cis2_token_contract_address, + ); + assert_eq!(balances.0, [ + TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()), + TokenAmountU256(0u8.into()) + ]); + + // Check that the logs are correct. + let events = deserialize_update_events_of_specified_contract(&update, smart_contract_wallet); + + assert_eq!(events, [Event::DepositCis2Tokens(DepositCis2TokensEvent { + token_amount: TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()), + token_id: TokenIdVec(vec![TOKEN_ID.0]), + cis2_token_contract_address, + from: Address::Contract(cis2_token_contract_address), + to: ALICE_PUBLIC_KEY + })]); +} + // /// Deserialize the events from an update. fn deserialize_update_events_of_specified_contract( update: &ContractInvokeSuccess, From e3e283c59eabc2dd0e41e288b4448b7cbefa18b4 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Mon, 25 Mar 2024 12:54:55 +0200 Subject: [PATCH 11/28] Added service fee --- .../cis5-smart-contract-wallet/src/lib.rs | 74 ++++++++++++------- .../cis5-smart-contract-wallet/tests/tests.rs | 73 +++++++++++------- 2 files changed, 93 insertions(+), 54 deletions(-) diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index 59fd63fb3..b151c1625 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -293,39 +293,49 @@ impl State { &mut self, from_public_key: PublicKeyEd25519, to_public_key: PublicKeyEd25519, - contract_address: ContractAddress, + cis2_token_contract_address: ContractAddress, token_id: ContractTokenId, - amount: ContractTokenAmount, - ) -> ContractResult<()> { + token_amount: ContractTokenAmount, + logger: &mut impl HasLogger, + ) -> ReceiveResult<()> { let zero_token_amount = TokenAmountU256(0u8.into()); // A zero transfer does not modify the state. - if amount == zero_token_amount { + if token_amount == zero_token_amount { return Ok(()); } let mut contract_balances = self .token_balances - .entry(contract_address) + .entry(cis2_token_contract_address) .occupied_or(CustomContractError::InvalidPublicKey)?; let mut token_balances = contract_balances .balances - .entry(token_id) + .entry(token_id.clone()) .occupied_or(CustomContractError::InvalidContractAddress)?; let mut from_cis2_token_balance = token_balances .entry(from_public_key) .occupied_or(CustomContractError::InsufficientFunds)?; - ensure!(*from_cis2_token_balance >= amount, ContractError::InsufficientFunds); - *from_cis2_token_balance -= amount; + ensure!(*from_cis2_token_balance >= token_amount, ContractError::InsufficientFunds.into()); + *from_cis2_token_balance -= token_amount; drop(from_cis2_token_balance); let mut to_cis2_token_balance = token_balances.entry(to_public_key).or_insert_with(|| TokenAmountU256(0u8.into())); - *to_cis2_token_balance += amount; + // CHECK: can overflow happen + *to_cis2_token_balance += token_amount; + + logger.log(&Event::InternalCis2TokensTransfer(InternalCis2TokensTransferEvent { + token_amount, + token_id, + cis2_token_contract_address, + from: from_public_key, + to: to_public_key, + }))?; Ok(()) } @@ -527,20 +537,26 @@ pub struct Cis2TokensInternalTransferBatch { #[derive(Debug, Serialize, Clone, SchemaType)] pub struct Cis2TokensInternalTransfer { /// The address owning the tokens being transferred. - pub signer: PublicKeyEd25519, + pub signer: PublicKeyEd25519, /// The address owning the tokens being transferred. - pub signature: SignatureEd25519, + pub signature: SignatureEd25519, /// The address owning the tokens being transferred. - pub expiry_time: Timestamp, + pub expiry_time: Timestamp, /// The address owning the tokens being transferred. - pub nonce: u64, + pub nonce: u64, /// The address owning the tokens being transferred. - pub service_fee: ContractTokenAmount, + pub service_fee: ContractTokenAmount, /// The address owning the tokens being transferred. - pub service_fee_recipient: Address, + pub service_fee_recipient: PublicKeyEd25519, + /// The amount of tokens being transferred. + pub service_fee_token_amount: ContractTokenAmount, + /// The ID of the token being transferred. + pub service_fee_token_id: ContractTokenId, + /// + pub service_fee_cis2_token_contract_address: ContractAddress, /// List of balance queries. #[concordium(size_length = 2)] - pub simple_transfers: Vec, + pub simple_transfers: Vec, } /// The parameter type for the contract function `balanceOfNativeCurrency`. @@ -572,12 +588,15 @@ fn internal_transfer_cis2_tokens( let param: Cis2TokensInternalTransferParameter = ctx.parameter_cursor().get()?; for Cis2TokensInternalTransfer { - signer: _signer, + signer, signature: _signature, expiry_time: _expiry_time, nonce: _nonce, service_fee: _service_fee, - service_fee_recipient: _service_fee_recipient, + service_fee_recipient, + service_fee_token_amount, + service_fee_token_id, + service_fee_cis2_token_contract_address, simple_transfers, } in param.transfers { @@ -590,7 +609,15 @@ fn internal_transfer_cis2_tokens( // TODO: Check signature and other parameters - // TODO: transfer service fee + // Transfer service fee + host.state_mut().transfer_cis2_tokens( + signer, + service_fee_recipient, + service_fee_cis2_token_contract_address, + service_fee_token_id.clone(), + service_fee_token_amount, + logger, + )?; for Cis2TokensInternalTransferBatch { from, @@ -607,15 +634,8 @@ fn internal_transfer_cis2_tokens( cis2_token_contract_address, token_id.clone(), token_amount, + logger, )?; - - logger.log(&Event::InternalCis2TokensTransfer(InternalCis2TokensTransferEvent { - token_amount, - token_id, - cis2_token_contract_address, - from, - to, - }))?; } } diff --git a/examples/cis5-smart-contract-wallet/tests/tests.rs b/examples/cis5-smart-contract-wallet/tests/tests.rs index a53240ce4..0ce65aa77 100644 --- a/examples/cis5-smart-contract-wallet/tests/tests.rs +++ b/examples/cis5-smart-contract-wallet/tests/tests.rs @@ -9,11 +9,10 @@ use smart_contract_wallet::*; const ALICE: AccountAddress = AccountAddress([0; 32]); const ALICE_ADDR: Address = Address::Account(ALICE); const BOB: AccountAddress = AccountAddress([1; 32]); -const SERVICE_FEE_RECIPIENT: AccountAddress = AccountAddress([2; 32]); -const SERVICE_FEE_RECIPIENT_ADDR: Address = Address::Account(SERVICE_FEE_RECIPIENT); const ALICE_PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([8; 32]); const BOB_PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([9; 32]); +const SERVICE_FEE_RECIPIENT_KEY: PublicKeyEd25519 = PublicKeyEd25519([4; 32]); const SIGNATURE: SignatureEd25519 = SignatureEd25519([ 68, 134, 96, 171, 184, 199, 1, 93, 76, 87, 144, 68, 55, 180, 93, 56, 107, 95, 127, 112, 24, 55, @@ -98,19 +97,22 @@ fn test_internal_transfer_cis2_tokens() { let internal_transfer_param = Cis2TokensInternalTransferParameter { transfers: vec![Cis2TokensInternalTransfer { - signer: ALICE_PUBLIC_KEY, - signature: SIGNATURE, - expiry_time: Timestamp::now(), - nonce: 0u64, - service_fee: service_fee_amount, - service_fee_recipient: SERVICE_FEE_RECIPIENT_ADDR, - simple_transfers: vec![Cis2TokensInternalTransferBatch { + signer: ALICE_PUBLIC_KEY, + signature: SIGNATURE, + expiry_time: Timestamp::now(), + nonce: 0u64, + service_fee: service_fee_amount, + service_fee_recipient: SERVICE_FEE_RECIPIENT_KEY, + simple_transfers: vec![Cis2TokensInternalTransferBatch { from: ALICE_PUBLIC_KEY, to: BOB_PUBLIC_KEY, token_amount: transfer_amount, token_id: contract_token_id.clone(), cis2_token_contract_address, }], + service_fee_token_amount: service_fee_amount, + service_fee_token_id: contract_token_id.clone(), + service_fee_cis2_token_contract_address: cis2_token_contract_address, }], }; @@ -128,28 +130,36 @@ fn test_internal_transfer_cis2_tokens() { // Check that Alice now has 100 tokens and Bob has 0 tokens on their public // keys. - // TODO: check balance of service fee as well. - let balances = get_cis2_tokens_balances_from_alice_and_bob( + let balances = get_cis2_tokens_balances_from_alice_and_bob_and_service_fee_recipient( &chain, smart_contract_wallet, cis2_token_contract_address, ); assert_eq!(balances.0, [ - TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()) - transfer_amount, - TokenAmountU256(transfer_amount.into()) + TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()) - transfer_amount - service_fee_amount, + TokenAmountU256(transfer_amount.into()), + TokenAmountU256(service_fee_amount.into()) ]); - // TODO: there should be two events; check that serviceFee was transferred - // correctly to service_fee_recipient. Check that the logs are correct. + // Check that the logs are correct. let events = deserialize_update_events_of_specified_contract(&update, smart_contract_wallet); - assert_eq!(events, [Event::InternalCis2TokensTransfer(InternalCis2TokensTransferEvent { - token_amount: transfer_amount, - token_id: contract_token_id, - cis2_token_contract_address, - from: ALICE_PUBLIC_KEY, - to: BOB_PUBLIC_KEY - })]); + assert_eq!(events, [ + Event::InternalCis2TokensTransfer(InternalCis2TokensTransferEvent { + token_amount: service_fee_amount, + token_id: contract_token_id.clone(), + cis2_token_contract_address, + from: ALICE_PUBLIC_KEY, + to: SERVICE_FEE_RECIPIENT_KEY + }), + Event::InternalCis2TokensTransfer(InternalCis2TokensTransferEvent { + token_amount: transfer_amount, + token_id: contract_token_id, + cis2_token_contract_address, + from: ALICE_PUBLIC_KEY, + to: BOB_PUBLIC_KEY + }) + ]); } // Helpers: @@ -200,7 +210,7 @@ fn initialize_chain_and_contract() -> (Chain, ContractAddress, ContractAddress) } /// Get the token balances for Alice and Bob. -fn get_cis2_tokens_balances_from_alice_and_bob( +fn get_cis2_tokens_balances_from_alice_and_bob_and_service_fee_recipient( chain: &Chain, smart_contract_wallet: ContractAddress, cis2_token_contract_address: ContractAddress, @@ -217,6 +227,11 @@ fn get_cis2_tokens_balances_from_alice_and_bob( cis2_token_contract_address, public_key: BOB_PUBLIC_KEY, }, + Cis2TokensBalanceOfQuery { + token_id: TokenIdVec(vec![TOKEN_ID.0]), + cis2_token_contract_address, + public_key: SERVICE_FEE_RECIPIENT_KEY, + }, ], }; let invoke = chain @@ -288,12 +303,16 @@ fn alice_deposits_cis2_tokens( }; // Check that Alice has 0 tokens and Bob has 0 tokens on their public keys. - let balances = get_cis2_tokens_balances_from_alice_and_bob( + let balances = get_cis2_tokens_balances_from_alice_and_bob_and_service_fee_recipient( &chain, smart_contract_wallet, cis2_token_contract_address, ); - assert_eq!(balances.0, [TokenAmountU256(0u8.into()), TokenAmountU256(0u8.into())]); + assert_eq!(balances.0, [ + TokenAmountU256(0u8.into()), + TokenAmountU256(0u8.into()), + TokenAmountU256(0u8.into()) + ]); let update = chain .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { @@ -307,14 +326,14 @@ fn alice_deposits_cis2_tokens( // Check that Alice now has 100 tokens and Bob has 0 tokens on their public // keys. - // TODO: check service_fee - let balances = get_cis2_tokens_balances_from_alice_and_bob( + let balances = get_cis2_tokens_balances_from_alice_and_bob_and_service_fee_recipient( &chain, smart_contract_wallet, cis2_token_contract_address, ); assert_eq!(balances.0, [ TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()), + TokenAmountU256(0u8.into()), TokenAmountU256(0u8.into()) ]); From f3c65052aab224162129da7d21f1e407f615146e Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 26 Mar 2024 09:44:15 +0200 Subject: [PATCH 12/28] Add signature checking --- .../cis5-smart-contract-wallet/Cargo.toml | 1 + .../cis5-smart-contract-wallet/src/lib.rs | 144 ++++++++++++++---- .../cis5-smart-contract-wallet/tests/tests.rs | 117 ++++++++++---- 3 files changed, 203 insertions(+), 59 deletions(-) diff --git a/examples/cis5-smart-contract-wallet/Cargo.toml b/examples/cis5-smart-contract-wallet/Cargo.toml index bd65c677b..135d3ea0b 100644 --- a/examples/cis5-smart-contract-wallet/Cargo.toml +++ b/examples/cis5-smart-contract-wallet/Cargo.toml @@ -18,6 +18,7 @@ concordium-cis2 = {path = "../../concordium-cis2", default-features = false, fea [dev-dependencies] concordium-smart-contract-testing = {path = "../../contract-testing"} cis2-multi = {path = "../cis2-multi"} +ed25519-dalek = { version = "2.0", features = ["rand_core"] } rand = "0.8" [lib] diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index b151c1625..44029dee6 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -2,6 +2,9 @@ use concordium_cis2::*; use concordium_std::*; +// TODO: look up genesis hash +const GENESIS_HASH: [u8; 32] = [1u8; 32]; + #[derive(SchemaType, Serialize)] pub struct VerificationParameter { pub public_key: PublicKeyEd25519, @@ -376,6 +379,12 @@ pub enum CustomContractError { InvalidContractAddress, // -6 InvalidTokenId, // -7 InsufficientFunds, // -8 + WrongSignature, // -9 + NonceMismatch, // -10 + WrongContract, // -11 + Expired, // -12 + WrongEntryPoint, // -13 + UnAuthorized, // -14 } pub type ContractError = Cis2Error; @@ -522,8 +531,6 @@ fn internal_transfer_native_currency( // specification, the order of the fields cannot be changed. #[derive(Debug, Serialize, Clone, SchemaType)] pub struct Cis2TokensInternalTransferBatch { - /// The address owning the tokens being transferred. - pub from: PublicKeyEd25519, /// The address receiving the tokens being transferred. pub to: PublicKeyEd25519, /// The amount of tokens being transferred. @@ -541,12 +548,12 @@ pub struct Cis2TokensInternalTransfer { /// The address owning the tokens being transferred. pub signature: SignatureEd25519, /// The address owning the tokens being transferred. + pub entry_point: OwnedEntrypointName, + /// The address owning the tokens being transferred. pub expiry_time: Timestamp, /// The address owning the tokens being transferred. pub nonce: u64, /// The address owning the tokens being transferred. - pub service_fee: ContractTokenAmount, - /// The address owning the tokens being transferred. pub service_fee_recipient: PublicKeyEd25519, /// The amount of tokens being transferred. pub service_fee_token_amount: ContractTokenAmount, @@ -570,12 +577,70 @@ pub struct Cis2TokensInternalTransferParameter { pub transfers: Vec, } +#[derive(Serialize)] +pub struct Cis2TokensInternalTransferPartial { + /// The address owning the tokens being transferred. + pub signer: PublicKeyEd25519, + /// The address owning the tokens being transferred. + pub signature: SignatureEd25519, +} + +fn calculate_message_hash_from_bytes( + message_bytes: &[u8], + crypto_primitives: &impl HasCryptoPrimitives, + ctx: &ReceiveContext, +) -> ContractResult<[u8; 32]> { + // We prepend the message with a context string consistent of the genesis_hash + // and this contract address. + let mut msg_prepend = [0; 32 + 16]; + msg_prepend[0..32].copy_from_slice(GENESIS_HASH.as_ref()); + msg_prepend[32..40].copy_from_slice(&ctx.self_address().index.to_le_bytes()); + msg_prepend[40..48].copy_from_slice(&ctx.self_address().subindex.to_le_bytes()); + + // Calculate the message hash. + Ok(crypto_primitives.hash_sha2_256(&[&msg_prepend[0..48], &message_bytes].concat()).0) +} + +/// Helper function to calculate the `message_hash`. +#[receive( + contract = "smart_contract_wallet", + name = "viewMessageHash", + parameter = "Cis2TokensInternalTransfer", + return_value = "[u8;32]", + error = "ContractError", + crypto_primitives, + mutable +)] +fn contract_view_message_hash( + ctx: &ReceiveContext, + _host: &mut Host, + crypto_primitives: &impl HasCryptoPrimitives, +) -> ContractResult<[u8; 32]> { + // Parse the parameter. + let mut cursor = ctx.parameter_cursor(); + // The input parameter is `PermitParam` but we only read the initial part of it + // with `PermitParamPartial`. I.e. we read the `signature` and the + // `signer`, but not the `message` here. + let _param: Cis2TokensInternalTransferPartial = cursor.get()?; + + // The input parameter is `PermitParam` but we have only read the initial part + // of it with `PermitParamPartial` so far. We read in the `message` now. + // `(cursor.size() - cursor.cursor_position()` is the length of the message in + // bytes. + let mut message_bytes = vec![0; (cursor.size() - cursor.cursor_position()) as usize]; + + cursor.read_exact(&mut message_bytes)?; + + calculate_message_hash_from_bytes(&message_bytes, crypto_primitives, ctx) +} + /// #[receive( contract = "smart_contract_wallet", name = "internalTransferCis2Tokens", parameter = "Cis2TokensInternalTransferParameter", error = "CustomContractError", + crypto_primitives, enable_logger, mutable )] @@ -583,31 +648,57 @@ fn internal_transfer_cis2_tokens( ctx: &ReceiveContext, host: &mut Host, logger: &mut impl HasLogger, + crypto_primitives: &impl HasCryptoPrimitives, ) -> ReceiveResult<()> { // Parse the parameter. let param: Cis2TokensInternalTransferParameter = ctx.parameter_cursor().get()?; - for Cis2TokensInternalTransfer { - signer, - signature: _signature, - expiry_time: _expiry_time, - nonce: _nonce, - service_fee: _service_fee, - service_fee_recipient, - service_fee_token_amount, - service_fee_token_id, - service_fee_cis2_token_contract_address, - simple_transfers, - } in param.transfers - { - // TODO: this needs to be authorized by the singer instead. - // Authenticate the sender for this transfer - // Get the sender who invoked this contract function. - // let sender = ctx.sender(); - // ensure!(from == sender || state.is_operator(&sender, &from), - // ContractError::Unauthorized); - - // TODO: Check signature and other parameters + for cis2_tokens_internal_transfer in param.transfers { + let Cis2TokensInternalTransfer { + signer, + signature, + entry_point, + expiry_time, + nonce, + service_fee_recipient, + service_fee_token_amount, + service_fee_token_id, + service_fee_cis2_token_contract_address, + simple_transfers, + } = cis2_tokens_internal_transfer.clone(); + + // Update the nonce. + let mut entry = host.state_mut().nonces_registry.entry(signer).or_insert_with(|| 0); + + // Get the current nonce. + let nonce_state = *entry; + // Bump nonce. + *entry += 1; + drop(entry); + + // Check the nonce to prevent replay attacks. + ensure_eq!(nonce, nonce_state, CustomContractError::NonceMismatch.into()); + + ensure_eq!( + entry_point, + "internalTransferCis2Tokens", + CustomContractError::WrongEntryPoint.into() + ); + + // Check signature is not expired. + ensure!(expiry_time > ctx.metadata().slot_time(), CustomContractError::Expired.into()); + + // We start message_bytes at 96 to remove signature and signer + let message_bytes = &to_bytes(&cis2_tokens_internal_transfer)[96..]; + + // Calculate the message hash. + let message_hash = + calculate_message_hash_from_bytes(message_bytes, crypto_primitives, ctx)?; + + // Check signature. + let valid_signature = + crypto_primitives.verify_ed25519_signature(signer, signature, &message_hash); + ensure!(valid_signature, CustomContractError::WrongSignature.into()); // Transfer service fee host.state_mut().transfer_cis2_tokens( @@ -620,7 +711,6 @@ fn internal_transfer_cis2_tokens( )?; for Cis2TokensInternalTransferBatch { - from, to, token_amount, token_id, @@ -629,7 +719,7 @@ fn internal_transfer_cis2_tokens( { // Update the contract state host.state_mut().transfer_cis2_tokens( - from, + signer, to, cis2_token_contract_address, token_id.clone(), diff --git a/examples/cis5-smart-contract-wallet/tests/tests.rs b/examples/cis5-smart-contract-wallet/tests/tests.rs index 0ce65aa77..c33566d0b 100644 --- a/examples/cis5-smart-contract-wallet/tests/tests.rs +++ b/examples/cis5-smart-contract-wallet/tests/tests.rs @@ -9,6 +9,8 @@ use smart_contract_wallet::*; const ALICE: AccountAddress = AccountAddress([0; 32]); const ALICE_ADDR: Address = Address::Account(ALICE); const BOB: AccountAddress = AccountAddress([1; 32]); +const CHARLIE: AccountAddress = AccountAddress([2; 32]); +const CHARLIE_ADDR: Address = Address::Account(CHARLIE); const ALICE_PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([8; 32]); const BOB_PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([9; 32]); @@ -80,7 +82,12 @@ fn test_deposit_cis2_tokens() { let (mut chain, smart_contract_wallet, cis2_token_contract_address) = initialize_chain_and_contract(); - alice_deposits_cis2_tokens(&mut chain, smart_contract_wallet, cis2_token_contract_address); + alice_deposits_cis2_tokens( + &mut chain, + smart_contract_wallet, + cis2_token_contract_address, + ALICE_PUBLIC_KEY, + ); } /// Test internal transfer of cis2 tokens. @@ -89,43 +96,81 @@ fn test_internal_transfer_cis2_tokens() { let (mut chain, smart_contract_wallet, cis2_token_contract_address) = initialize_chain_and_contract(); - alice_deposits_cis2_tokens(&mut chain, smart_contract_wallet, cis2_token_contract_address); + use ed25519_dalek::{Signer, SigningKey}; + + let rng = &mut rand::thread_rng(); + + // Construct message, verifying_key, and signature. + let signing_key = SigningKey::generate(rng); + let alice_public_key = PublicKeyEd25519(signing_key.verifying_key().to_bytes()); + + alice_deposits_cis2_tokens( + &mut chain, + smart_contract_wallet, + cis2_token_contract_address, + alice_public_key, + ); let service_fee_amount: TokenAmountU256 = TokenAmountU256(1.into()); let transfer_amount: TokenAmountU256 = TokenAmountU256(5.into()); let contract_token_id: TokenIdVec = TokenIdVec(vec![TOKEN_ID.0]); - let internal_transfer_param = Cis2TokensInternalTransferParameter { - transfers: vec![Cis2TokensInternalTransfer { - signer: ALICE_PUBLIC_KEY, - signature: SIGNATURE, - expiry_time: Timestamp::now(), - nonce: 0u64, - service_fee: service_fee_amount, - service_fee_recipient: SERVICE_FEE_RECIPIENT_KEY, - simple_transfers: vec![Cis2TokensInternalTransferBatch { - from: ALICE_PUBLIC_KEY, - to: BOB_PUBLIC_KEY, - token_amount: transfer_amount, - token_id: contract_token_id.clone(), - cis2_token_contract_address, - }], - service_fee_token_amount: service_fee_amount, - service_fee_token_id: contract_token_id.clone(), - service_fee_cis2_token_contract_address: cis2_token_contract_address, + let mut internal_transfer_param = Cis2TokensInternalTransfer { + signer: alice_public_key, + signature: SIGNATURE, + entry_point: OwnedEntrypointName::new_unchecked("internalTransferCis2Tokens".to_string()), + expiry_time: Timestamp::now(), + nonce: 0u64, + service_fee_recipient: SERVICE_FEE_RECIPIENT_KEY, + simple_transfers: vec![Cis2TokensInternalTransferBatch { + to: BOB_PUBLIC_KEY, + token_amount: transfer_amount, + token_id: contract_token_id.clone(), + cis2_token_contract_address, }], + service_fee_token_amount: service_fee_amount, + service_fee_token_id: contract_token_id.clone(), + service_fee_cis2_token_contract_address: cis2_token_contract_address, }; - let update = chain - .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + // Get the message hash to be signed. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { amount: Amount::zero(), + address: smart_contract_wallet, receive_name: OwnedReceiveName::new_unchecked( - "smart_contract_wallet.internalTransferCis2Tokens".to_string(), + "smart_contract_wallet.viewMessageHash".to_string(), ), - address: smart_contract_wallet, message: OwnedParameter::from_serial(&internal_transfer_param) - .expect("Internal transfer cis2 tokens params"), + .expect("Should be a valid inut parameter"), }) + .expect("Should be able to query viewMessageHash"); + + let signature = signing_key.sign(&invoke.return_value); + + internal_transfer_param.signature = SignatureEd25519(signature.to_bytes()); + + let internal_transfer_param = Cis2TokensInternalTransferParameter { + transfers: vec![internal_transfer_param.clone()], + }; + + let update = chain + .contract_update( + SIGNER, + CHARLIE, + CHARLIE_ADDR, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "smart_contract_wallet.internalTransferCis2Tokens".to_string(), + ), + address: smart_contract_wallet, + message: OwnedParameter::from_serial(&internal_transfer_param) + .expect("Internal transfer cis2 tokens params"), + }, + ) + .print_emitted_events() .expect("Should be able to internally transfer cis2 tokens"); // Check that Alice now has 100 tokens and Bob has 0 tokens on their public @@ -134,6 +179,7 @@ fn test_internal_transfer_cis2_tokens() { &chain, smart_contract_wallet, cis2_token_contract_address, + alice_public_key, ); assert_eq!(balances.0, [ TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()) - transfer_amount - service_fee_amount, @@ -149,14 +195,14 @@ fn test_internal_transfer_cis2_tokens() { token_amount: service_fee_amount, token_id: contract_token_id.clone(), cis2_token_contract_address, - from: ALICE_PUBLIC_KEY, + from: alice_public_key, to: SERVICE_FEE_RECIPIENT_KEY }), Event::InternalCis2TokensTransfer(InternalCis2TokensTransferEvent { token_amount: transfer_amount, token_id: contract_token_id, cis2_token_contract_address, - from: ALICE_PUBLIC_KEY, + from: alice_public_key, to: BOB_PUBLIC_KEY }) ]); @@ -175,11 +221,13 @@ fn initialize_chain_and_contract() -> (Chain, ContractAddress, ContractAddress) // Create some accounts on the chain. chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new(CHARLIE, ACC_INITIAL_BALANCE)); // Load and deploy cis2 token module. let module = module_load_v1("../cis2-multi/concordium-out/module.wasm.v1").expect("Module exists"); - let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); + let deployment = + chain.module_deploy_v1_debug(SIGNER, ALICE, module, true).expect("Deploy valid module"); // Initialize the auction contract. let cis2_token_contract_init = chain @@ -194,7 +242,8 @@ fn initialize_chain_and_contract() -> (Chain, ContractAddress, ContractAddress) // Load and deploy the module. let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); - let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); + let deployment = + chain.module_deploy_v1_debug(SIGNER, ALICE, module, true).expect("Deploy valid module"); // Initialize the auction contract. let smart_contract_wallet_init = chain @@ -214,13 +263,14 @@ fn get_cis2_tokens_balances_from_alice_and_bob_and_service_fee_recipient( chain: &Chain, smart_contract_wallet: ContractAddress, cis2_token_contract_address: ContractAddress, + alice_public_key: PublicKeyEd25519, ) -> Cis2TokensBalanceOfResponse { let balance_of_params = Cis2TokensBalanceOfParameter { queries: vec![ Cis2TokensBalanceOfQuery { token_id: TokenIdVec(vec![TOKEN_ID.0]), cis2_token_contract_address, - public_key: ALICE_PUBLIC_KEY, + public_key: alice_public_key, }, Cis2TokensBalanceOfQuery { token_id: TokenIdVec(vec![TOKEN_ID.0]), @@ -286,6 +336,7 @@ fn alice_deposits_cis2_tokens( chain: &mut Chain, smart_contract_wallet: ContractAddress, cis2_token_contract_address: ContractAddress, + alice_public_key: PublicKeyEd25519, ) { let new_metadata_url = "https://new-url.com".to_string(); @@ -299,7 +350,7 @@ fn alice_deposits_cis2_tokens( hash: None, }, token_id: TOKEN_ID, - data: AdditionalData::from(to_bytes(&ALICE_PUBLIC_KEY)), + data: AdditionalData::from(to_bytes(&alice_public_key)), }; // Check that Alice has 0 tokens and Bob has 0 tokens on their public keys. @@ -307,6 +358,7 @@ fn alice_deposits_cis2_tokens( &chain, smart_contract_wallet, cis2_token_contract_address, + alice_public_key, ); assert_eq!(balances.0, [ TokenAmountU256(0u8.into()), @@ -330,6 +382,7 @@ fn alice_deposits_cis2_tokens( &chain, smart_contract_wallet, cis2_token_contract_address, + alice_public_key, ); assert_eq!(balances.0, [ TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()), @@ -345,7 +398,7 @@ fn alice_deposits_cis2_tokens( token_id: TokenIdVec(vec![TOKEN_ID.0]), cis2_token_contract_address, from: Address::Contract(cis2_token_contract_address), - to: ALICE_PUBLIC_KEY + to: alice_public_key })]); } From bb70c44d83cf0274ab5be0ed23d889e5842f6e80 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 26 Mar 2024 10:13:46 +0200 Subject: [PATCH 13/28] Simplified signature verification --- .../cis5-smart-contract-wallet/src/lib.rs | 115 ++++++++++-------- .../cis5-smart-contract-wallet/tests/tests.rs | 16 ++- 2 files changed, 75 insertions(+), 56 deletions(-) diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index 44029dee6..56cfaa77b 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -164,9 +164,9 @@ pub struct DepositNativeCurrencyEvent { #[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] pub struct NonceEvent { /// Account that signed the `PermitMessage`. - pub account: AccountAddress, + pub public_key: PublicKeyEd25519, /// The nonce that was used in the `PermitMessage`. - pub nonce: u64, + pub nonce: u64, } /// The state for each address. @@ -542,11 +542,7 @@ pub struct Cis2TokensInternalTransferBatch { } #[derive(Debug, Serialize, Clone, SchemaType)] -pub struct Cis2TokensInternalTransfer { - /// The address owning the tokens being transferred. - pub signer: PublicKeyEd25519, - /// The address owning the tokens being transferred. - pub signature: SignatureEd25519, +pub struct Cis2TokensInternalTransferMessage { /// The address owning the tokens being transferred. pub entry_point: OwnedEntrypointName, /// The address owning the tokens being transferred. @@ -577,12 +573,14 @@ pub struct Cis2TokensInternalTransferParameter { pub transfers: Vec, } -#[derive(Serialize)] -pub struct Cis2TokensInternalTransferPartial { +#[derive(Debug, Serialize, Clone, SchemaType)] +pub struct Cis2TokensInternalTransfer { /// The address owning the tokens being transferred. pub signer: PublicKeyEd25519, /// The address owning the tokens being transferred. pub signature: SignatureEd25519, + /// + pub message: Cis2TokensInternalTransferMessage, } fn calculate_message_hash_from_bytes( @@ -601,11 +599,45 @@ fn calculate_message_hash_from_bytes( Ok(crypto_primitives.hash_sha2_256(&[&msg_prepend[0..48], &message_bytes].concat()).0) } +fn validate_signature_and_increase_nonce( + message: Cis2TokensInternalTransferMessage, + signer: PublicKeyEd25519, + signature: SignatureEd25519, + host: &mut Host, + crypto_primitives: &impl HasCryptoPrimitives, + ctx: &ReceiveContext, +) -> ContractResult<()> { + // Check signature is not expired. + ensure!(message.expiry_time > ctx.metadata().slot_time(), CustomContractError::Expired.into()); + + // Calculate the message hash. + let message_hash = + calculate_message_hash_from_bytes(&to_bytes(&message), crypto_primitives, ctx)?; + + // Check signature. + let valid_signature = + crypto_primitives.verify_ed25519_signature(signer, signature, &message_hash); + ensure!(valid_signature, CustomContractError::WrongSignature.into()); + + // Update the nonce. + let mut entry = host.state_mut().nonces_registry.entry(signer).or_insert_with(|| 0); + + // Get the current nonce. + let nonce_state = *entry; + // Bump nonce. + *entry += 1; + + // Check the nonce to prevent replay attacks. + ensure_eq!(message.nonce, nonce_state, CustomContractError::NonceMismatch.into()); + + Ok(()) +} + /// Helper function to calculate the `message_hash`. #[receive( contract = "smart_contract_wallet", name = "viewMessageHash", - parameter = "Cis2TokensInternalTransfer", + parameter = "Cis2TokensInternalTransferMessage", return_value = "[u8;32]", error = "ContractError", crypto_primitives, @@ -617,21 +649,9 @@ fn contract_view_message_hash( crypto_primitives: &impl HasCryptoPrimitives, ) -> ContractResult<[u8; 32]> { // Parse the parameter. - let mut cursor = ctx.parameter_cursor(); - // The input parameter is `PermitParam` but we only read the initial part of it - // with `PermitParamPartial`. I.e. we read the `signature` and the - // `signer`, but not the `message` here. - let _param: Cis2TokensInternalTransferPartial = cursor.get()?; - - // The input parameter is `PermitParam` but we have only read the initial part - // of it with `PermitParamPartial` so far. We read in the `message` now. - // `(cursor.size() - cursor.cursor_position()` is the length of the message in - // bytes. - let mut message_bytes = vec![0; (cursor.size() - cursor.cursor_position()) as usize]; + let param: Cis2TokensInternalTransferMessage = ctx.parameter_cursor().get()?; - cursor.read_exact(&mut message_bytes)?; - - calculate_message_hash_from_bytes(&message_bytes, crypto_primitives, ctx) + calculate_message_hash_from_bytes(&to_bytes(¶m), crypto_primitives, ctx) } /// @@ -657,27 +677,19 @@ fn internal_transfer_cis2_tokens( let Cis2TokensInternalTransfer { signer, signature, + message, + } = cis2_tokens_internal_transfer.clone(); + + let Cis2TokensInternalTransferMessage { entry_point, - expiry_time, + expiry_time: _, nonce, service_fee_recipient, service_fee_token_amount, service_fee_token_id, service_fee_cis2_token_contract_address, simple_transfers, - } = cis2_tokens_internal_transfer.clone(); - - // Update the nonce. - let mut entry = host.state_mut().nonces_registry.entry(signer).or_insert_with(|| 0); - - // Get the current nonce. - let nonce_state = *entry; - // Bump nonce. - *entry += 1; - drop(entry); - - // Check the nonce to prevent replay attacks. - ensure_eq!(nonce, nonce_state, CustomContractError::NonceMismatch.into()); + } = message.clone(); ensure_eq!( entry_point, @@ -685,20 +697,14 @@ fn internal_transfer_cis2_tokens( CustomContractError::WrongEntryPoint.into() ); - // Check signature is not expired. - ensure!(expiry_time > ctx.metadata().slot_time(), CustomContractError::Expired.into()); - - // We start message_bytes at 96 to remove signature and signer - let message_bytes = &to_bytes(&cis2_tokens_internal_transfer)[96..]; - - // Calculate the message hash. - let message_hash = - calculate_message_hash_from_bytes(message_bytes, crypto_primitives, ctx)?; - - // Check signature. - let valid_signature = - crypto_primitives.verify_ed25519_signature(signer, signature, &message_hash); - ensure!(valid_signature, CustomContractError::WrongSignature.into()); + validate_signature_and_increase_nonce( + message.clone(), + signer, + signature, + host, + crypto_primitives, + ctx, + )?; // Transfer service fee host.state_mut().transfer_cis2_tokens( @@ -727,6 +733,11 @@ fn internal_transfer_cis2_tokens( logger, )?; } + + logger.log(&Event::Nonce(NonceEvent { + public_key: signer, + nonce, + }))?; } Ok(()) diff --git a/examples/cis5-smart-contract-wallet/tests/tests.rs b/examples/cis5-smart-contract-wallet/tests/tests.rs index c33566d0b..b286ca7da 100644 --- a/examples/cis5-smart-contract-wallet/tests/tests.rs +++ b/examples/cis5-smart-contract-wallet/tests/tests.rs @@ -115,9 +115,7 @@ fn test_internal_transfer_cis2_tokens() { let transfer_amount: TokenAmountU256 = TokenAmountU256(5.into()); let contract_token_id: TokenIdVec = TokenIdVec(vec![TOKEN_ID.0]); - let mut internal_transfer_param = Cis2TokensInternalTransfer { - signer: alice_public_key, - signature: SIGNATURE, + let message = Cis2TokensInternalTransferMessage { entry_point: OwnedEntrypointName::new_unchecked("internalTransferCis2Tokens".to_string()), expiry_time: Timestamp::now(), nonce: 0u64, @@ -133,6 +131,12 @@ fn test_internal_transfer_cis2_tokens() { service_fee_cis2_token_contract_address: cis2_token_contract_address, }; + let mut internal_transfer_param = Cis2TokensInternalTransfer { + signer: alice_public_key, + signature: SIGNATURE, + message: message.clone(), + }; + // Get the message hash to be signed. let invoke = chain .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { @@ -141,7 +145,7 @@ fn test_internal_transfer_cis2_tokens() { receive_name: OwnedReceiveName::new_unchecked( "smart_contract_wallet.viewMessageHash".to_string(), ), - message: OwnedParameter::from_serial(&internal_transfer_param) + message: OwnedParameter::from_serial(&message) .expect("Should be a valid inut parameter"), }) .expect("Should be able to query viewMessageHash"); @@ -204,6 +208,10 @@ fn test_internal_transfer_cis2_tokens() { cis2_token_contract_address, from: alice_public_key, to: BOB_PUBLIC_KEY + }), + Event::Nonce(NonceEvent { + public_key: alice_public_key, + nonce: 0, }) ]); } From 0d77638e1f473104b957c3dce9f1e2b5e10325cf Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 26 Mar 2024 12:08:40 +0200 Subject: [PATCH 14/28] Refactor signingMessage --- .../cis5-smart-contract-wallet/src/lib.rs | 391 ++++++++++-------- .../cis5-smart-contract-wallet/tests/tests.rs | 216 +++++++++- 2 files changed, 408 insertions(+), 199 deletions(-) diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index 56cfaa77b..06c82e9c5 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -5,51 +5,6 @@ use concordium_std::*; // TODO: look up genesis hash const GENESIS_HASH: [u8; 32] = [1u8; 32]; -#[derive(SchemaType, Serialize)] -pub struct VerificationParameter { - pub public_key: PublicKeyEd25519, - pub signature: SignatureEd25519, - pub message: Vec, -} - -/// Part of the parameter type for the contract function `permit`. -/// Specifies the message that is signed. -#[derive(SchemaType, Serialize)] -pub struct SigningMessage { - /// 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: SigningMessage, -} - -#[derive(Serialize)] -pub struct PermitParamPartial { - /// Signature/s. The CIS3 standard supports multi-sig accounts. - pub signature: AccountSignatures, - /// Account that created the above signature. - pub signer: AccountAddress, -} - /// Contract token ID type. pub type ContractTokenId = TokenIdVec; @@ -67,7 +22,7 @@ pub enum Event { InternalCis2TokensTransfer(InternalCis2TokensTransferEvent), /// The event tracks when a role is revoked from an address. #[concordium(tag = 245)] - InternalNativeCurrencyTransferEvent(InternalNativeCurrencyTransferEvent), + InternalNativeCurrencyTransfer(InternalNativeCurrencyTransferEvent), /// The event tracks when a role is revoked from an address. #[concordium(tag = 246)] WithdrawCis2Tokens(WithdrawCis2TokensEvent), @@ -260,31 +215,38 @@ impl State { &mut self, from_public_key: PublicKeyEd25519, to_public_key: PublicKeyEd25519, - amount: Amount, - ) -> ContractResult<()> { + ccd_amount: Amount, + logger: &mut impl HasLogger, + ) -> ReceiveResult<()> { // A zero transfer does not modify the state. - if amount == Amount::zero() { - return Ok(()); + if ccd_amount != Amount::zero() { + { + let mut from_public_key_native_balance = self + .native_balances + .entry(from_public_key) + .occupied_or(CustomContractError::InvalidPublicKey)?; + + ensure!( + *from_public_key_native_balance >= ccd_amount, + ContractError::InsufficientFunds.into() + ); + *from_public_key_native_balance -= ccd_amount; + } + + let mut to_public_key_native_balance = + self.native_balances.entry(to_public_key).or_insert_with(Amount::zero); + + // TODO: check if overflow possible + *to_public_key_native_balance += ccd_amount; } - { - let mut from_public_key_native_balance = self - .native_balances - .entry(from_public_key) - .occupied_or(CustomContractError::InvalidPublicKey) - .map(|x| *x)?; - - ensure!(from_public_key_native_balance >= amount, ContractError::InsufficientFunds); - from_public_key_native_balance -= amount; - } - - let mut to_public_key_native_balance = self - .native_balances - .entry(to_public_key) - .occupied_or(CustomContractError::InvalidPublicKey) - .map(|x| *x)?; - - to_public_key_native_balance += amount; + logger.log(&Event::InternalNativeCurrencyTransfer( + InternalNativeCurrencyTransferEvent { + ccd_amount, + from: from_public_key, + to: to_public_key, + }, + ))?; Ok(()) } @@ -303,34 +265,35 @@ impl State { ) -> ReceiveResult<()> { let zero_token_amount = TokenAmountU256(0u8.into()); // A zero transfer does not modify the state. - if token_amount == zero_token_amount { - return Ok(()); - } - - let mut contract_balances = self - .token_balances - .entry(cis2_token_contract_address) - .occupied_or(CustomContractError::InvalidPublicKey)?; - - let mut token_balances = contract_balances - .balances - .entry(token_id.clone()) - .occupied_or(CustomContractError::InvalidContractAddress)?; - - let mut from_cis2_token_balance = token_balances - .entry(from_public_key) - .occupied_or(CustomContractError::InsufficientFunds)?; + if token_amount != zero_token_amount { + let mut contract_balances = self + .token_balances + .entry(cis2_token_contract_address) + .occupied_or(CustomContractError::InvalidPublicKey)?; + + let mut token_balances = contract_balances + .balances + .entry(token_id.clone()) + .occupied_or(CustomContractError::InvalidContractAddress)?; + + let mut from_cis2_token_balance = token_balances + .entry(from_public_key) + .occupied_or(CustomContractError::InsufficientFunds)?; - ensure!(*from_cis2_token_balance >= token_amount, ContractError::InsufficientFunds.into()); - *from_cis2_token_balance -= token_amount; + ensure!( + *from_cis2_token_balance >= token_amount, + ContractError::InsufficientFunds.into() + ); + *from_cis2_token_balance -= token_amount; - drop(from_cis2_token_balance); + drop(from_cis2_token_balance); - let mut to_cis2_token_balance = - token_balances.entry(to_public_key).or_insert_with(|| TokenAmountU256(0u8.into())); + let mut to_cis2_token_balance = + token_balances.entry(to_public_key).or_insert_with(|| TokenAmountU256(0u8.into())); - // CHECK: can overflow happen - *to_cis2_token_balance += token_amount; + // CHECK: can overflow happen + *to_cis2_token_balance += token_amount; + } logger.log(&Event::InternalCis2TokensTransfer(InternalCis2TokensTransferEvent { token_amount, @@ -385,6 +348,7 @@ pub enum CustomContractError { Expired, // -12 WrongEntryPoint, // -13 UnAuthorized, // -14 + WrongSigningAmountType, // -15 } pub type ContractError = Cis2Error; @@ -437,11 +401,7 @@ fn deposit_native_currency( #[receive( contract = "smart_contract_wallet", name = "depositCis2Tokens", - parameter = "OnReceivingCis2DataParams< - ContractTokenId, - ContractTokenAmount, - PublicKeyEd25519, ->", + parameter = "OnReceivingCis2DataParams", error = "CustomContractError", enable_logger, mutable @@ -492,74 +452,57 @@ fn deposit_cis2_tokens( Ok(()) } -/// -#[receive( - contract = "smart_contract_wallet", - name = "internalTransferNativeCurrency", - parameter = "PublicKeyEd25519", - error = "CustomContractError", - payable, - mutable -)] -fn internal_transfer_native_currency( - _ctx: &ReceiveContext, - _host: &mut Host, - _amount: Amount, -) -> ReceiveResult<()> { - // TODO: this needs to be authorized by the singer instead. - // Authenticate the sender for this transfer - // Get the sender who invoked this contract function. - // let sender = ctx.sender(); - // ensure!(from == sender || state.is_operator(&sender, &from), - // ContractError::Unauthorized); - - // let beneficiary: PublicKeyEd25519 = ctx.parameter_cursor().get()?; - - // let (state, builder) = host.state_and_builder(); - - // let mut public_key_balances = - // state.balances.entry(beneficiary).or_insert_with(|| - // PublicKeyState::empty(builder)); - - // public_key_balances.native_balance = amount; +#[derive(Debug, Serialize, Clone, SchemaType)] +pub enum SigningAmount { + CCDAmount(Amount), + TokenAmount(TokenAmount), +} - Ok(()) +#[derive(Debug, Serialize, Clone, SchemaType)] +pub struct TokenAmount { + pub token_amount: ContractTokenAmount, + /// The ID of the token being transferred. + pub token_id: ContractTokenId, + /// + pub cis2_token_contract_address: ContractAddress, } /// A single transfer of some amount of a token. // Note: For the serialization to be derived according to the CIS2 // specification, the order of the fields cannot be changed. #[derive(Debug, Serialize, Clone, SchemaType)] -pub struct Cis2TokensInternalTransferBatch { +pub struct InternalTransferBatch { /// The address receiving the tokens being transferred. - pub to: PublicKeyEd25519, + pub to: PublicKeyEd25519, /// The amount of tokens being transferred. - pub token_amount: ContractTokenAmount, - /// The ID of the token being transferred. - pub token_id: ContractTokenId, - /// - pub cis2_token_contract_address: ContractAddress, + pub transfer_amount: SigningAmount, } #[derive(Debug, Serialize, Clone, SchemaType)] -pub struct Cis2TokensInternalTransferMessage { +pub struct InternalTransferMessage { /// The address owning the tokens being transferred. - pub entry_point: OwnedEntrypointName, + pub entry_point: OwnedEntrypointName, /// The address owning the tokens being transferred. - pub expiry_time: Timestamp, + pub expiry_time: Timestamp, /// The address owning the tokens being transferred. - pub nonce: u64, + pub nonce: u64, /// The address owning the tokens being transferred. pub service_fee_recipient: PublicKeyEd25519, /// The amount of tokens being transferred. - pub service_fee_token_amount: ContractTokenAmount, - /// The ID of the token being transferred. - pub service_fee_token_id: ContractTokenId, - /// - pub service_fee_cis2_token_contract_address: ContractAddress, + pub service_fee_amount: SigningAmount, /// List of balance queries. #[concordium(size_length = 2)] - pub simple_transfers: Vec, + pub simple_transfers: Vec, +} + +#[derive(Debug, Serialize, Clone, SchemaType)] +pub struct InternalTransfer { + /// The address owning the tokens being transferred. + pub signer: PublicKeyEd25519, + /// The address owning the tokens being transferred. + pub signature: SignatureEd25519, + /// + pub message: InternalTransferMessage, } /// The parameter type for the contract function `balanceOfNativeCurrency`. @@ -567,20 +510,10 @@ pub struct Cis2TokensInternalTransferMessage { // specification, the order of the fields cannot be changed. #[derive(Debug, Serialize, SchemaType)] #[concordium(transparent)] -pub struct Cis2TokensInternalTransferParameter { +pub struct InternalTransferParameter { /// List of balance queries. #[concordium(size_length = 2)] - pub transfers: Vec, -} - -#[derive(Debug, Serialize, Clone, SchemaType)] -pub struct Cis2TokensInternalTransfer { - /// The address owning the tokens being transferred. - pub signer: PublicKeyEd25519, - /// The address owning the tokens being transferred. - pub signature: SignatureEd25519, - /// - pub message: Cis2TokensInternalTransferMessage, + pub transfers: Vec, } fn calculate_message_hash_from_bytes( @@ -600,7 +533,7 @@ fn calculate_message_hash_from_bytes( } fn validate_signature_and_increase_nonce( - message: Cis2TokensInternalTransferMessage, + message: InternalTransferMessage, signer: PublicKeyEd25519, signature: SignatureEd25519, host: &mut Host, @@ -633,23 +566,113 @@ fn validate_signature_and_increase_nonce( Ok(()) } +/// +#[receive( + contract = "smart_contract_wallet", + name = "internalTransferNativeCurrency", + parameter = "InternalTransferParameter", + error = "CustomContractError", + crypto_primitives, + enable_logger, + mutable +)] +fn internal_transfer_native_currency( + ctx: &ReceiveContext, + host: &mut Host, + logger: &mut impl HasLogger, + crypto_primitives: &impl HasCryptoPrimitives, +) -> ReceiveResult<()> { + // Parse the parameter. + let param: InternalTransferParameter = ctx.parameter_cursor().get()?; + + for internal_transfer in param.transfers { + let InternalTransfer { + signer, + signature, + message, + } = internal_transfer.clone(); + + let InternalTransferMessage { + entry_point, + expiry_time: _, + nonce, + service_fee_recipient, + service_fee_amount, + simple_transfers, + } = message.clone(); + + ensure_eq!( + entry_point, + "internalTransferNativeCurrency", + CustomContractError::WrongEntryPoint.into() + ); + + let service_fee_ccd_amount = match service_fee_amount { + SigningAmount::CCDAmount(ccd_amount) => ccd_amount, + SigningAmount::TokenAmount(_) => { + bail!(CustomContractError::WrongSigningAmountType.into()) + } + }; + + validate_signature_and_increase_nonce( + message.clone(), + signer, + signature, + host, + crypto_primitives, + ctx, + )?; + + // Transfer service fee + host.state_mut().transfer_native_currency( + signer, + service_fee_recipient, + service_fee_ccd_amount, + logger, + )?; + + for InternalTransferBatch { + to, + transfer_amount, + } in simple_transfers + { + let ccd_amount = match transfer_amount { + SigningAmount::CCDAmount(ccd_amount) => ccd_amount, + SigningAmount::TokenAmount(_) => { + bail!(CustomContractError::WrongSigningAmountType.into()) + } + }; + + // Update the contract state + host.state_mut().transfer_native_currency(signer, to, ccd_amount, logger)?; + } + + logger.log(&Event::Nonce(NonceEvent { + public_key: signer, + nonce, + }))?; + } + + Ok(()) +} + /// Helper function to calculate the `message_hash`. #[receive( contract = "smart_contract_wallet", - name = "viewMessageHash", - parameter = "Cis2TokensInternalTransferMessage", + name = "viewInternalTransferMessageHash", + parameter = "InternalTransferMessage", return_value = "[u8;32]", error = "ContractError", crypto_primitives, mutable )] -fn contract_view_message_hash( +fn contract_view_internal_transfer_message_hash( ctx: &ReceiveContext, _host: &mut Host, crypto_primitives: &impl HasCryptoPrimitives, ) -> ContractResult<[u8; 32]> { // Parse the parameter. - let param: Cis2TokensInternalTransferMessage = ctx.parameter_cursor().get()?; + let param: InternalTransferMessage = ctx.parameter_cursor().get()?; calculate_message_hash_from_bytes(&to_bytes(¶m), crypto_primitives, ctx) } @@ -658,7 +681,7 @@ fn contract_view_message_hash( #[receive( contract = "smart_contract_wallet", name = "internalTransferCis2Tokens", - parameter = "Cis2TokensInternalTransferParameter", + parameter = "InternalTransferParameter", error = "CustomContractError", crypto_primitives, enable_logger, @@ -671,23 +694,21 @@ fn internal_transfer_cis2_tokens( crypto_primitives: &impl HasCryptoPrimitives, ) -> ReceiveResult<()> { // Parse the parameter. - let param: Cis2TokensInternalTransferParameter = ctx.parameter_cursor().get()?; + let param: InternalTransferParameter = ctx.parameter_cursor().get()?; for cis2_tokens_internal_transfer in param.transfers { - let Cis2TokensInternalTransfer { + let InternalTransfer { signer, signature, message, } = cis2_tokens_internal_transfer.clone(); - let Cis2TokensInternalTransferMessage { + let InternalTransferMessage { entry_point, expiry_time: _, nonce, service_fee_recipient, - service_fee_token_amount, - service_fee_token_id, - service_fee_cis2_token_contract_address, + service_fee_amount, simple_transfers, } = message.clone(); @@ -697,6 +718,13 @@ fn internal_transfer_cis2_tokens( CustomContractError::WrongEntryPoint.into() ); + let service_fee = match service_fee_amount { + SigningAmount::CCDAmount(_) => { + bail!(CustomContractError::WrongSigningAmountType.into()) + } + SigningAmount::TokenAmount(token_amount) => token_amount, + }; + validate_signature_and_increase_nonce( message.clone(), signer, @@ -710,26 +738,31 @@ fn internal_transfer_cis2_tokens( host.state_mut().transfer_cis2_tokens( signer, service_fee_recipient, - service_fee_cis2_token_contract_address, - service_fee_token_id.clone(), - service_fee_token_amount, + service_fee.cis2_token_contract_address, + service_fee.token_id, + service_fee.token_amount, logger, )?; - for Cis2TokensInternalTransferBatch { + for InternalTransferBatch { to, - token_amount, - token_id, - cis2_token_contract_address, + transfer_amount, } in simple_transfers { + let transfer = match transfer_amount { + SigningAmount::CCDAmount(_) => { + bail!(CustomContractError::WrongSigningAmountType.into()) + } + SigningAmount::TokenAmount(token_amount) => token_amount, + }; + // Update the contract state host.state_mut().transfer_cis2_tokens( signer, to, - cis2_token_contract_address, - token_id.clone(), - token_amount, + transfer.cis2_token_contract_address, + transfer.token_id, + transfer.token_amount, logger, )?; } diff --git a/examples/cis5-smart-contract-wallet/tests/tests.rs b/examples/cis5-smart-contract-wallet/tests/tests.rs index b286ca7da..ae880f664 100644 --- a/examples/cis5-smart-contract-wallet/tests/tests.rs +++ b/examples/cis5-smart-contract-wallet/tests/tests.rs @@ -28,6 +28,7 @@ const TOKEN_ID: TokenIdU8 = TokenIdU8(4); const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); const AIRDROP_TOKEN_AMOUNT: TokenAmountU64 = TokenAmountU64(100); +const AIRDROP_CCD_AMOUNT: Amount = Amount::from_micro_ccd(100); /// A signer for all the transactions. const SIGNER: Signer = Signer::with_one_key(); @@ -46,8 +47,12 @@ fn test_deposit_native_currency() { initialize_chain_and_contract(); // Check that Alice has 0 CCD and Bob has 0 CCD on their public keys. - let balances = get_native_currency_balance_from_alice_and_bob(&chain, smart_contract_wallet); - assert_eq!(balances.0, [Amount::zero(), Amount::zero()]); + let balances = get_native_currency_balance_from_alice_and_bob_and_service_fee_recipient( + &chain, + smart_contract_wallet, + ALICE_PUBLIC_KEY, + ); + assert_eq!(balances.0, [Amount::zero(), Amount::zero(), Amount::zero()]); let send_amount = Amount::from_micro_ccd(100); let update = chain @@ -63,8 +68,12 @@ fn test_deposit_native_currency() { .expect("Should be able to deposit CCD"); // Check that Alice now has 100 CCD and Bob has 0 CCD on their public keys. - let balances = get_native_currency_balance_from_alice_and_bob(&chain, smart_contract_wallet); - assert_eq!(balances.0, [send_amount, Amount::zero()]); + let balances = get_native_currency_balance_from_alice_and_bob_and_service_fee_recipient( + &chain, + smart_contract_wallet, + ALICE_PUBLIC_KEY, + ); + assert_eq!(balances.0, [send_amount, Amount::zero(), Amount::zero()]); // Check that the logs are correct. let events = deserialize_update_events_of_specified_contract(&update, smart_contract_wallet); @@ -90,6 +99,119 @@ fn test_deposit_cis2_tokens() { ); } +/// Test internal transfer of ccd. +#[test] +fn test_internal_transfer_ccd() { + let (mut chain, smart_contract_wallet, _cis2_token_contract_address) = + initialize_chain_and_contract(); + + use ed25519_dalek::{Signer, SigningKey}; + + let rng = &mut rand::thread_rng(); + + // Construct message, verifying_key, and signature. + let signing_key = SigningKey::generate(rng); + let alice_public_key = PublicKeyEd25519(signing_key.verifying_key().to_bytes()); + + alice_deposits_ccd(&mut chain, smart_contract_wallet, alice_public_key); + + let service_fee_amount: Amount = Amount::from_micro_ccd(1); + let transfer_amount: Amount = Amount::from_micro_ccd(5); + + let message = InternalTransferMessage { + entry_point: OwnedEntrypointName::new_unchecked( + "internalTransferNativeCurrency".to_string(), + ), + expiry_time: Timestamp::now(), + nonce: 0u64, + service_fee_recipient: SERVICE_FEE_RECIPIENT_KEY, + simple_transfers: vec![InternalTransferBatch { + to: BOB_PUBLIC_KEY, + transfer_amount: SigningAmount::CCDAmount(transfer_amount), + }], + service_fee_amount: SigningAmount::CCDAmount(service_fee_amount), + }; + + let mut internal_transfer_param = InternalTransfer { + signer: alice_public_key, + signature: SIGNATURE, + message: message.clone(), + }; + + // Get the message hash to be signed. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + address: smart_contract_wallet, + receive_name: OwnedReceiveName::new_unchecked( + "smart_contract_wallet.viewInternalTransferMessageHash".to_string(), + ), + message: OwnedParameter::from_serial(&message) + .expect("Should be a valid inut parameter"), + }) + .expect("Should be able to query viewInternalTransferMessageHash"); + + let signature = signing_key.sign(&invoke.return_value); + + internal_transfer_param.signature = SignatureEd25519(signature.to_bytes()); + + let internal_transfer_param = InternalTransferParameter { + transfers: vec![internal_transfer_param.clone()], + }; + + let update = chain + .contract_update( + SIGNER, + CHARLIE, + CHARLIE_ADDR, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "smart_contract_wallet.internalTransferNativeCurrency".to_string(), + ), + address: smart_contract_wallet, + message: OwnedParameter::from_serial(&internal_transfer_param) + .expect("Internal transfer native currency params"), + }, + ) + .print_emitted_events() + .expect("Should be able to internally transfer native currency"); + + // Check that Alice now has 100 ccd and Bob has 0 ccd on their public + // keys. + let balances = get_native_currency_balance_from_alice_and_bob_and_service_fee_recipient( + &chain, + smart_contract_wallet, + alice_public_key, + ); + assert_eq!(balances.0, [ + AIRDROP_CCD_AMOUNT - transfer_amount - service_fee_amount, + transfer_amount, + service_fee_amount + ]); + + // Check that the logs are correct. + let events = deserialize_update_events_of_specified_contract(&update, smart_contract_wallet); + + assert_eq!(events, [ + Event::InternalNativeCurrencyTransfer(InternalNativeCurrencyTransferEvent { + ccd_amount: service_fee_amount, + from: alice_public_key, + to: SERVICE_FEE_RECIPIENT_KEY, + }), + Event::InternalNativeCurrencyTransfer(InternalNativeCurrencyTransferEvent { + ccd_amount: transfer_amount, + from: alice_public_key, + to: BOB_PUBLIC_KEY, + }), + Event::Nonce(NonceEvent { + public_key: alice_public_key, + nonce: 0, + }) + ]); +} + /// Test internal transfer of cis2 tokens. #[test] fn test_internal_transfer_cis2_tokens() { @@ -115,23 +237,29 @@ fn test_internal_transfer_cis2_tokens() { let transfer_amount: TokenAmountU256 = TokenAmountU256(5.into()); let contract_token_id: TokenIdVec = TokenIdVec(vec![TOKEN_ID.0]); - let message = Cis2TokensInternalTransferMessage { - entry_point: OwnedEntrypointName::new_unchecked("internalTransferCis2Tokens".to_string()), - expiry_time: Timestamp::now(), - nonce: 0u64, + let message = InternalTransferMessage { + entry_point: OwnedEntrypointName::new_unchecked( + "internalTransferCis2Tokens".to_string(), + ), + expiry_time: Timestamp::now(), + nonce: 0u64, service_fee_recipient: SERVICE_FEE_RECIPIENT_KEY, - simple_transfers: vec![Cis2TokensInternalTransferBatch { - to: BOB_PUBLIC_KEY, - token_amount: transfer_amount, + service_fee_amount: SigningAmount::TokenAmount(TokenAmount { + token_amount: service_fee_amount, token_id: contract_token_id.clone(), cis2_token_contract_address, + }), + simple_transfers: vec![InternalTransferBatch { + to: BOB_PUBLIC_KEY, + transfer_amount: SigningAmount::TokenAmount(TokenAmount { + token_amount: transfer_amount, + token_id: contract_token_id.clone(), + cis2_token_contract_address, + }), }], - service_fee_token_amount: service_fee_amount, - service_fee_token_id: contract_token_id.clone(), - service_fee_cis2_token_contract_address: cis2_token_contract_address, }; - let mut internal_transfer_param = Cis2TokensInternalTransfer { + let mut internal_transfer_param = InternalTransfer { signer: alice_public_key, signature: SIGNATURE, message: message.clone(), @@ -143,18 +271,18 @@ fn test_internal_transfer_cis2_tokens() { amount: Amount::zero(), address: smart_contract_wallet, receive_name: OwnedReceiveName::new_unchecked( - "smart_contract_wallet.viewMessageHash".to_string(), + "smart_contract_wallet.viewInternalTransferMessageHash".to_string(), ), message: OwnedParameter::from_serial(&message) .expect("Should be a valid inut parameter"), }) - .expect("Should be able to query viewMessageHash"); + .expect("Should be able to query viewInternalTransferMessageHash"); let signature = signing_key.sign(&invoke.return_value); internal_transfer_param.signature = SignatureEd25519(signature.to_bytes()); - let internal_transfer_param = Cis2TokensInternalTransferParameter { + let internal_transfer_param = InternalTransferParameter { transfers: vec![internal_transfer_param.clone()], }; @@ -309,18 +437,22 @@ fn get_cis2_tokens_balances_from_alice_and_bob_and_service_fee_recipient( } /// Get the native currency balances for Alice and Bob. -fn get_native_currency_balance_from_alice_and_bob( +fn get_native_currency_balance_from_alice_and_bob_and_service_fee_recipient( chain: &Chain, smart_contract_wallet: ContractAddress, + alice_public_key: PublicKeyEd25519, ) -> NativeCurrencyBalanceOfResponse { let balance_of_params = NativeCurrencyBalanceOfParameter { queries: vec![ NativeCurrencyBalanceOfQuery { - public_key: ALICE_PUBLIC_KEY, + public_key: alice_public_key, }, NativeCurrencyBalanceOfQuery { public_key: BOB_PUBLIC_KEY, }, + NativeCurrencyBalanceOfQuery { + public_key: SERVICE_FEE_RECIPIENT_KEY, + }, ], }; let invoke = chain @@ -410,6 +542,50 @@ fn alice_deposits_cis2_tokens( })]); } +/// Get the token balances for Alice and Bob. +fn alice_deposits_ccd( + chain: &mut Chain, + smart_contract_wallet: ContractAddress, + alice_public_key: PublicKeyEd25519, +) { + // Check that Alice has 0 CCD and Bob has 0 CCD on their public keys. + let balances = get_native_currency_balance_from_alice_and_bob_and_service_fee_recipient( + &chain, + smart_contract_wallet, + alice_public_key, + ); + assert_eq!(balances.0, [Amount::zero(), Amount::zero(), Amount::zero()]); + + let update = chain + .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: AIRDROP_CCD_AMOUNT, + receive_name: OwnedReceiveName::new_unchecked( + "smart_contract_wallet.depositNativeCurrency".to_string(), + ), + address: smart_contract_wallet, + message: OwnedParameter::from_serial(&alice_public_key) + .expect("Deposit native currency params"), + }) + .expect("Should be able to deposit CCD"); + + // Check that Alice now has 100 CCD and Bob has 0 CCD on their public keys. + let balances = get_native_currency_balance_from_alice_and_bob_and_service_fee_recipient( + &chain, + smart_contract_wallet, + alice_public_key, + ); + assert_eq!(balances.0, [AIRDROP_CCD_AMOUNT, Amount::zero(), Amount::zero()]); + + // Check that the logs are correct. + let events = deserialize_update_events_of_specified_contract(&update, smart_contract_wallet); + + assert_eq!(events, [Event::DepositNativeCurrency(DepositNativeCurrencyEvent { + ccd_amount: AIRDROP_CCD_AMOUNT, + from: ALICE_ADDR, + to: alice_public_key, + })]); +} + // /// Deserialize the events from an update. fn deserialize_update_events_of_specified_contract( update: &ContractInvokeSuccess, From 6a66d41b5382904ddb535c3ec9182ad0c89219f5 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 26 Mar 2024 12:25:20 +0200 Subject: [PATCH 15/28] Add CIS0 specifications --- .../cis5-smart-contract-wallet/src/lib.rs | 110 +++++++++++++++--- 1 file changed, 92 insertions(+), 18 deletions(-) diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index 06c82e9c5..f398e4002 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -5,6 +5,14 @@ use concordium_std::*; // TODO: look up genesis hash const GENESIS_HASH: [u8; 32] = [1u8; 32]; +/// The standard identifier for the CIS-5: Smart Contract Wallet Standard. +pub const CIS5_STANDARD_IDENTIFIER: StandardIdentifier<'static> = + StandardIdentifier::new_unchecked("CIS-5"); + +/// List of supported standards by this contract address. +const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = + [CIS0_STANDARD_IDENTIFIER, CIS5_STANDARD_IDENTIFIER]; + /// Contract token ID type. pub type ContractTokenId = TokenIdVec; @@ -306,24 +314,23 @@ impl State { Ok(()) } - // /// 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) { - // SupportResult::SupportBy(addresses.to_vec()) - // } else { - // SupportResult::NoSupport - // } - // } - - // /// Set implementors for a given standard. - // fn set_implementors( - // &mut self, - // std_id: StandardIdentifierOwned, - // implementors: Vec, - // ) { - // self.implementors.insert(std_id, implementors); - // } + /// 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) { + SupportResult::SupportBy(addresses.to_vec()) + } else { + SupportResult::NoSupport + } + } + + /// Set implementors for a given standard. + fn set_implementors( + &mut self, + std_id: StandardIdentifierOwned, + implementors: Vec, + ) { + self.implementors.insert(std_id, implementors); + } } /// The different errors the contract can produce. @@ -776,6 +783,73 @@ fn internal_transfer_cis2_tokens( Ok(()) } +/// 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, +} + +/// Set the addresses for an implementation given a standard identifier and a +/// list of contract addresses. +/// +/// It rejects if: +/// - Sender is not the owner of the contract instance. +/// - It fails to parse the parameter. +#[receive( + contract = "smart_contract_wallet", + name = "setImplementors", + parameter = "SetImplementorsParams", + error = "ContractError", + mutable +)] +fn contract_set_implementor(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { + // Authorize the sender. + ensure!(ctx.sender().matches_account(&ctx.owner()), ContractError::Unauthorized); + // Parse the parameter. + let params: SetImplementorsParams = ctx.parameter_cursor().get()?; + // Update the implementors in the state + host.state_mut().set_implementors(params.id, params.implementors); + + Ok(()) +} + +/// Get the supported standards or addresses for a implementation given list of +/// standard identifiers. +/// +/// It rejects if: +/// - It fails to parse the parameter. +#[receive( + contract = "smart_contract_wallet", + name = "supports", + parameter = "SupportsQueryParams", + return_value = "SupportsQueryResponse", + error = "ContractError" +)] +fn contract_supports( + ctx: &ReceiveContext, + host: &Host, +) -> ContractResult { + // Parse the parameter. + let params: SupportsQueryParams = ctx.parameter_cursor().get()?; + + // Build the response. + let mut response = Vec::with_capacity(params.queries.len()); + for std_id in params.queries { + if SUPPORTS_STANDARDS.contains(&std_id.as_standard_identifier()) { + response.push(SupportResult::Support); + } else { + response.push(host.state().have_implementors(&std_id)); + } + } + let result = SupportsQueryResponse::from(response); + Ok(result) +} + /// A query for the balance of a given address for a given token. // Note: For the serialization to be derived according to the CIS2 // specification, the order of the fields cannot be changed. From 89308a1b506b896963595cde16ea0faba0df7104 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 26 Mar 2024 15:06:08 +0200 Subject: [PATCH 16/28] Add withdraw ccd function --- .../cis5-smart-contract-wallet/src/lib.rs | 272 ++++++++++++++++-- .../cis5-smart-contract-wallet/tests/tests.rs | 122 ++++++++ 2 files changed, 373 insertions(+), 21 deletions(-) diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index f398e4002..9c1317304 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -232,7 +232,7 @@ impl State { let mut from_public_key_native_balance = self .native_balances .entry(from_public_key) - .occupied_or(CustomContractError::InvalidPublicKey)?; + .occupied_or(CustomContractError::InsufficientFunds)?; ensure!( *from_public_key_native_balance >= ccd_amount, @@ -277,12 +277,12 @@ impl State { let mut contract_balances = self .token_balances .entry(cis2_token_contract_address) - .occupied_or(CustomContractError::InvalidPublicKey)?; + .occupied_or(CustomContractError::InsufficientFunds)?; let mut token_balances = contract_balances .balances .entry(token_id.clone()) - .occupied_or(CustomContractError::InvalidContractAddress)?; + .occupied_or(CustomContractError::InsufficientFunds)?; let mut from_cis2_token_balance = token_balances .entry(from_public_key) @@ -345,17 +345,14 @@ pub enum CustomContractError { LogMalformed, // -3 /// Invalid contract name. OnlyContract, // -4 - InvalidPublicKey, // -5 - InvalidContractAddress, // -6 - InvalidTokenId, // -7 - InsufficientFunds, // -8 - WrongSignature, // -9 - NonceMismatch, // -10 - WrongContract, // -11 - Expired, // -12 - WrongEntryPoint, // -13 - UnAuthorized, // -14 - WrongSigningAmountType, // -15 + InsufficientFunds, // -5 + WrongSignature, // -6 + NonceMismatch, // -7 + WrongContract, // -8 + Expired, // -9 + WrongEntryPoint, // -10 + UnAuthorized, // -11 + WrongSigningAmountType, // -12 } pub type ContractError = Cis2Error; @@ -539,7 +536,7 @@ fn calculate_message_hash_from_bytes( Ok(crypto_primitives.hash_sha2_256(&[&msg_prepend[0..48], &message_bytes].concat()).0) } -fn validate_signature_and_increase_nonce( +fn validate_signature_and_increase_nonce_internal_transfer_message( message: InternalTransferMessage, signer: PublicKeyEd25519, signature: SignatureEd25519, @@ -573,6 +570,27 @@ fn validate_signature_and_increase_nonce( Ok(()) } +/// Helper function to calculate the `message_hash`. +#[receive( + contract = "smart_contract_wallet", + name = "viewInternalTransferMessageHash", + parameter = "InternalTransferMessage", + return_value = "[u8;32]", + error = "ContractError", + crypto_primitives, + mutable +)] +fn contract_view_internal_transfer_message_hash( + ctx: &ReceiveContext, + _host: &mut Host, + crypto_primitives: &impl HasCryptoPrimitives, +) -> ContractResult<[u8; 32]> { + // Parse the parameter. + let param: InternalTransferMessage = ctx.parameter_cursor().get()?; + + calculate_message_hash_from_bytes(&to_bytes(¶m), crypto_primitives, ctx) +} + /// #[receive( contract = "smart_contract_wallet", @@ -621,7 +639,7 @@ fn internal_transfer_native_currency( } }; - validate_signature_and_increase_nonce( + validate_signature_and_increase_nonce_internal_transfer_message( message.clone(), signer, signature, @@ -663,27 +681,239 @@ fn internal_transfer_native_currency( Ok(()) } +/// A single transfer of some amount of a token. +// Note: For the serialization to be derived according to the CIS2 +// specification, the order of the fields cannot be changed. +#[derive(Debug, Serialize, Clone, SchemaType)] +pub struct WithdrawBatch { + /// The address receiving the tokens being transferred. + pub to: Receiver, + /// The amount of tokens being transferred. + pub withdraw_amount: SigningAmount, + /// + pub data: AdditionalData, +} + +#[derive(Debug, Serialize, Clone, SchemaType)] +pub struct WithdrawMessage { + /// The address owning the tokens being transferred. + pub entry_point: OwnedEntrypointName, + /// The address owning the tokens being transferred. + pub expiry_time: Timestamp, + /// The address owning the tokens being transferred. + pub nonce: u64, + /// The address owning the tokens being transferred. + pub service_fee_recipient: PublicKeyEd25519, + /// The amount of tokens being transferred. + pub service_fee_amount: SigningAmount, + /// List of balance queries. + #[concordium(size_length = 2)] + pub simple_withdraws: Vec, +} + +#[derive(Debug, Serialize, Clone, SchemaType)] +pub struct Withdraw { + /// The address owning the tokens being transferred. + pub signer: PublicKeyEd25519, + /// The address owning the tokens being transferred. + pub signature: SignatureEd25519, + /// + pub message: WithdrawMessage, +} + +/// The parameter type for the contract function `balanceOfNativeCurrency`. +// Note: For the serialization to be derived according to the CIS2 +// specification, the order of the fields cannot be changed. +#[derive(Debug, Serialize, SchemaType)] +#[concordium(transparent)] +pub struct WithdrawParameter { + /// List of balance queries. + #[concordium(size_length = 2)] + pub transfers: Vec, +} + +fn validate_signature_and_increase_nonce_withdraw_message( + message: WithdrawMessage, + signer: PublicKeyEd25519, + signature: SignatureEd25519, + host: &mut Host, + crypto_primitives: &impl HasCryptoPrimitives, + ctx: &ReceiveContext, +) -> ContractResult<()> { + // Check signature is not expired. + ensure!(message.expiry_time > ctx.metadata().slot_time(), CustomContractError::Expired.into()); + + // Calculate the message hash. + let message_hash = + calculate_message_hash_from_bytes(&to_bytes(&message), crypto_primitives, ctx)?; + + // Check signature. + let valid_signature = + crypto_primitives.verify_ed25519_signature(signer, signature, &message_hash); + ensure!(valid_signature, CustomContractError::WrongSignature.into()); + + // Update the nonce. + let mut entry = host.state_mut().nonces_registry.entry(signer).or_insert_with(|| 0); + + // Get the current nonce. + let nonce_state = *entry; + // Bump nonce. + *entry += 1; + + // Check the nonce to prevent replay attacks. + ensure_eq!(message.nonce, nonce_state, CustomContractError::NonceMismatch.into()); + + Ok(()) +} + /// Helper function to calculate the `message_hash`. #[receive( contract = "smart_contract_wallet", - name = "viewInternalTransferMessageHash", - parameter = "InternalTransferMessage", + name = "viewWithdrawMessageHash", + parameter = "WithdrawMessage", return_value = "[u8;32]", error = "ContractError", crypto_primitives, mutable )] -fn contract_view_internal_transfer_message_hash( +fn contract_view_withdraw_message_hash( ctx: &ReceiveContext, _host: &mut Host, crypto_primitives: &impl HasCryptoPrimitives, ) -> ContractResult<[u8; 32]> { // Parse the parameter. - let param: InternalTransferMessage = ctx.parameter_cursor().get()?; + let param: WithdrawMessage = ctx.parameter_cursor().get()?; calculate_message_hash_from_bytes(&to_bytes(¶m), crypto_primitives, ctx) } +/// +#[receive( + contract = "smart_contract_wallet", + name = "withdrawNativeCurrency", + parameter = "WithdrawParameter", + error = "CustomContractError", + crypto_primitives, + enable_logger, + mutable +)] +fn withdraw_native_currency( + ctx: &ReceiveContext, + host: &mut Host, + logger: &mut impl HasLogger, + crypto_primitives: &impl HasCryptoPrimitives, +) -> ReceiveResult<()> { + // Parse the parameter. + let param: WithdrawParameter = ctx.parameter_cursor().get()?; + + for internal_transfer in param.transfers { + let Withdraw { + signer, + signature, + message, + } = internal_transfer.clone(); + + let WithdrawMessage { + entry_point, + expiry_time: _, + nonce, + service_fee_recipient, + service_fee_amount, + simple_withdraws, + } = message.clone(); + + ensure_eq!( + entry_point, + "withdrawNativeCurrency", + CustomContractError::WrongEntryPoint.into() + ); + + let service_fee_ccd_amount = match service_fee_amount { + SigningAmount::CCDAmount(ccd_amount) => ccd_amount, + SigningAmount::TokenAmount(_) => { + bail!(CustomContractError::WrongSigningAmountType.into()) + } + }; + + validate_signature_and_increase_nonce_withdraw_message( + message.clone(), + signer, + signature, + host, + crypto_primitives, + ctx, + )?; + + // Transfer service fee + host.state_mut().transfer_native_currency( + signer, + service_fee_recipient, + service_fee_ccd_amount, + logger, + )?; + + for WithdrawBatch { + to, + withdraw_amount, + data, + } in simple_withdraws + { + let ccd_amount = match withdraw_amount { + SigningAmount::CCDAmount(ccd_amount) => ccd_amount, + SigningAmount::TokenAmount(_) => { + bail!(CustomContractError::WrongSigningAmountType.into()) + } + }; + + // Update the contract state + { + let mut from_public_key_native_balance = host + .state_mut() + .native_balances + .entry(signer) + .occupied_or(CustomContractError::InsufficientFunds)?; + + ensure!( + *from_public_key_native_balance >= ccd_amount, + ContractError::InsufficientFunds.into() + ); + *from_public_key_native_balance -= ccd_amount; + } + + // If the receiver is a contract: invoke the receive hook function. + // Withdraw ccd out of the contract + let to_address = match to { + Receiver::Account(account_address) => { + host.invoke_transfer(&account_address, ccd_amount)?; + Address::Account(account_address) + } + Receiver::Contract(contract_address, function) => { + host.invoke_contract( + &contract_address, + &data, + function.as_entrypoint_name(), + ccd_amount, + )?; + Address::Contract(contract_address) + } + }; + + logger.log(&Event::WithdrawNativeCurrency(WithdrawNativeCurrencyEvent { + ccd_amount, + from: signer, + to: to_address, + }))?; + } + + logger.log(&Event::Nonce(NonceEvent { + public_key: signer, + nonce, + }))?; + } + + Ok(()) +} + /// #[receive( contract = "smart_contract_wallet", @@ -732,7 +962,7 @@ fn internal_transfer_cis2_tokens( SigningAmount::TokenAmount(token_amount) => token_amount, }; - validate_signature_and_increase_nonce( + validate_signature_and_increase_nonce_internal_transfer_message( message.clone(), signer, signature, diff --git a/examples/cis5-smart-contract-wallet/tests/tests.rs b/examples/cis5-smart-contract-wallet/tests/tests.rs index ae880f664..81c913ed1 100644 --- a/examples/cis5-smart-contract-wallet/tests/tests.rs +++ b/examples/cis5-smart-contract-wallet/tests/tests.rs @@ -9,6 +9,7 @@ use smart_contract_wallet::*; const ALICE: AccountAddress = AccountAddress([0; 32]); const ALICE_ADDR: Address = Address::Account(ALICE); const BOB: AccountAddress = AccountAddress([1; 32]); +const BOB_ADDR: Address = Address::Account(BOB); const CHARLIE: AccountAddress = AccountAddress([2; 32]); const CHARLIE_ADDR: Address = Address::Account(CHARLIE); @@ -99,6 +100,127 @@ fn test_deposit_cis2_tokens() { ); } +/// Test withdraw of ccd. +#[test] +fn test_withdraw_ccd() { + let (mut chain, smart_contract_wallet, _cis2_token_contract_address) = + initialize_chain_and_contract(); + + use ed25519_dalek::{Signer, SigningKey}; + + let rng = &mut rand::thread_rng(); + + // Construct message, verifying_key, and signature. + let signing_key = SigningKey::generate(rng); + let alice_public_key = PublicKeyEd25519(signing_key.verifying_key().to_bytes()); + + alice_deposits_ccd(&mut chain, smart_contract_wallet, alice_public_key); + + let service_fee_amount: Amount = Amount::from_micro_ccd(1); + let transfer_amount: Amount = Amount::from_micro_ccd(5); + + let message = WithdrawMessage { + entry_point: OwnedEntrypointName::new_unchecked( + "withdrawNativeCurrency".to_string(), + ), + expiry_time: Timestamp::now(), + nonce: 0u64, + service_fee_recipient: SERVICE_FEE_RECIPIENT_KEY, + simple_withdraws: vec![WithdrawBatch { + to: Receiver::Account(BOB), + withdraw_amount: SigningAmount::CCDAmount(transfer_amount), + data: AdditionalData::empty(), + }], + service_fee_amount: SigningAmount::CCDAmount(service_fee_amount), + }; + + let mut withdraw_param = Withdraw { + signer: alice_public_key, + signature: SIGNATURE, + message: message.clone(), + }; + + // Get the message hash to be signed. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + address: smart_contract_wallet, + receive_name: OwnedReceiveName::new_unchecked( + "smart_contract_wallet.viewWithdrawMessageHash".to_string(), + ), + message: OwnedParameter::from_serial(&message) + .expect("Should be a valid inut parameter"), + }) + .expect("Should be able to query viewWithdrawMessageHash"); + + let signature = signing_key.sign(&invoke.return_value); + + withdraw_param.signature = SignatureEd25519(signature.to_bytes()); + + let withdraw_param = WithdrawParameter { + transfers: vec![withdraw_param.clone()], + }; + + let ccd_balance_bob_before = chain.account_balance(BOB).unwrap().total; + + let update = chain + .contract_update( + SIGNER, + CHARLIE, + CHARLIE_ADDR, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "smart_contract_wallet.withdrawNativeCurrency".to_string(), + ), + address: smart_contract_wallet, + message: OwnedParameter::from_serial(&withdraw_param) + .expect("Internal transfer native currency params"), + }, + ) + .print_emitted_events() + .expect("Should be able to internally transfer native currency"); + + // Check that Alice now has 100 ccd and Bob has 0 ccd on their public + // keys. + let balances = get_native_currency_balance_from_alice_and_bob_and_service_fee_recipient( + &chain, + smart_contract_wallet, + alice_public_key, + ); + assert_eq!(balances.0, [ + AIRDROP_CCD_AMOUNT - transfer_amount - service_fee_amount, + Amount::zero(), + service_fee_amount + ]); + + assert_eq!( + chain.account_balance(BOB).unwrap().total, + ccd_balance_bob_before.checked_add(transfer_amount).unwrap(), + ); + + // Check that the logs are correct. + let events = deserialize_update_events_of_specified_contract(&update, smart_contract_wallet); + + assert_eq!(events, [ + Event::InternalNativeCurrencyTransfer(InternalNativeCurrencyTransferEvent { + ccd_amount: service_fee_amount, + from: alice_public_key, + to: SERVICE_FEE_RECIPIENT_KEY, + }), + Event::WithdrawNativeCurrency(WithdrawNativeCurrencyEvent { + ccd_amount: transfer_amount, + from: alice_public_key, + to: BOB_ADDR, + }), + Event::Nonce(NonceEvent { + public_key: alice_public_key, + nonce: 0, + }) + ]); +} + /// Test internal transfer of ccd. #[test] fn test_internal_transfer_ccd() { From d34cf179e083cce076f3dd35959477bb95cfdce0 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Wed, 27 Mar 2024 17:48:15 +0200 Subject: [PATCH 17/28] Add withdraw cis2 token function --- .../cis5-smart-contract-wallet/src/lib.rs | 140 +++++++++++++ .../cis5-smart-contract-wallet/tests/tests.rs | 187 +++++++++++++++++- 2 files changed, 317 insertions(+), 10 deletions(-) diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index 9c1317304..2d933cb03 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -21,6 +21,8 @@ pub type ContractTokenId = TokenIdVec; /// most 1 and it is fine to use a small type for representing token amounts. pub type ContractTokenAmount = TokenAmountU256; +type TransferParameter = TransferParams; + /// Tagged events to be serialized for the event log. #[derive(Debug, Serial, Deserial, PartialEq, Eq, SchemaType)] #[concordium(repr(u8))] @@ -914,6 +916,144 @@ fn withdraw_native_currency( Ok(()) } +/// +#[receive( + contract = "smart_contract_wallet", + name = "withdrawCis2Tokens", + parameter = "WithdrawParameter", + error = "CustomContractError", + crypto_primitives, + enable_logger, + mutable +)] +fn withdraw_cis2_tokens( + ctx: &ReceiveContext, + host: &mut Host, + logger: &mut impl HasLogger, + crypto_primitives: &impl HasCryptoPrimitives, +) -> ReceiveResult<()> { + // Parse the parameter. + let param: WithdrawParameter = ctx.parameter_cursor().get()?; + + for internal_transfer in param.transfers { + let Withdraw { + signer, + signature, + message, + } = internal_transfer.clone(); + + let WithdrawMessage { + entry_point, + expiry_time: _, + nonce, + service_fee_recipient, + service_fee_amount, + simple_withdraws, + } = message.clone(); + + ensure_eq!(entry_point, "withdrawCis2Tokens", CustomContractError::WrongEntryPoint.into()); + + let service_fee = match service_fee_amount { + SigningAmount::CCDAmount(_) => { + bail!(CustomContractError::WrongSigningAmountType.into()) + } + SigningAmount::TokenAmount(ref token_amount) => token_amount, + }; + + validate_signature_and_increase_nonce_withdraw_message( + message.clone(), + signer, + signature, + host, + crypto_primitives, + ctx, + )?; + + // Transfer service fee + host.state_mut().transfer_cis2_tokens( + signer, + service_fee_recipient, + service_fee.cis2_token_contract_address, + service_fee.token_id.clone(), + service_fee.token_amount, + logger, + )?; + + for WithdrawBatch { + to, + withdraw_amount, + data, + } in simple_withdraws + { + let single_withdraw = match withdraw_amount { + SigningAmount::CCDAmount(_) => { + bail!(CustomContractError::WrongSigningAmountType.into()) + } + SigningAmount::TokenAmount(ref single_withdraw) => single_withdraw, + }; + + // Update the contract state + { + let mut contract_balances = host + .state_mut() + .token_balances + .entry(single_withdraw.cis2_token_contract_address) + .occupied_or(CustomContractError::InsufficientFunds)?; + + let mut token_balances = contract_balances + .balances + .entry(single_withdraw.token_id.clone()) + .occupied_or(CustomContractError::InsufficientFunds)?; + + let mut from_cis2_token_balance = token_balances + .entry(signer) + .occupied_or(CustomContractError::InsufficientFunds)?; + + ensure!( + *from_cis2_token_balance >= single_withdraw.token_amount, + ContractError::InsufficientFunds.into() + ); + *from_cis2_token_balance -= single_withdraw.token_amount; + } + + let data: TransferParameter = TransferParams(vec![Transfer { + token_id: single_withdraw.token_id.clone(), + amount: single_withdraw.token_amount, + from: Address::Contract(ctx.self_address()), + to: to.clone(), + data, + }]); + + host.invoke_contract( + &single_withdraw.cis2_token_contract_address, + &data, + EntrypointName::new_unchecked("transfer"), + Amount::zero(), + )?; + + let to_address = match to { + Receiver::Account(account_address) => Address::Account(account_address), + Receiver::Contract(contract_address, _) => Address::Contract(contract_address), + }; + + logger.log(&Event::WithdrawCis2Tokens(WithdrawCis2TokensEvent { + token_amount: single_withdraw.token_amount, + token_id: single_withdraw.token_id.clone(), + cis2_token_contract_address: single_withdraw.cis2_token_contract_address, + from: signer, + to: to_address, + }))?; + } + + logger.log(&Event::Nonce(NonceEvent { + public_key: signer, + nonce, + }))?; + } + + Ok(()) +} + /// #[receive( contract = "smart_contract_wallet", diff --git a/examples/cis5-smart-contract-wallet/tests/tests.rs b/examples/cis5-smart-contract-wallet/tests/tests.rs index 81c913ed1..87cd1c6a4 100644 --- a/examples/cis5-smart-contract-wallet/tests/tests.rs +++ b/examples/cis5-smart-contract-wallet/tests/tests.rs @@ -1,5 +1,5 @@ //! Tests for the `cis2_wCCD` contract. -use cis2_multi::MintParams; +use cis2_multi::{ContractBalanceOfQueryParams, ContractBalanceOfQueryResponse, MintParams}; use concordium_cis2::*; use concordium_smart_contract_testing::*; use concordium_std::{PublicKeyEd25519, SignatureEd25519}; @@ -134,7 +134,7 @@ fn test_withdraw_ccd() { service_fee_amount: SigningAmount::CCDAmount(service_fee_amount), }; - let mut withdraw_param = Withdraw { + let mut withdraw = Withdraw { signer: alice_public_key, signature: SIGNATURE, message: message.clone(), @@ -155,10 +155,10 @@ fn test_withdraw_ccd() { let signature = signing_key.sign(&invoke.return_value); - withdraw_param.signature = SignatureEd25519(signature.to_bytes()); + withdraw.signature = SignatureEd25519(signature.to_bytes()); let withdraw_param = WithdrawParameter { - transfers: vec![withdraw_param.clone()], + transfers: vec![withdraw.clone()], }; let ccd_balance_bob_before = chain.account_balance(BOB).unwrap().total; @@ -221,6 +221,147 @@ fn test_withdraw_ccd() { ]); } +/// Test withdrawing of cis2 tokens. +#[test] +fn test_withdraw_cis2_tokens() { + let (mut chain, smart_contract_wallet, cis2_token_contract_address) = + initialize_chain_and_contract(); + + use ed25519_dalek::{Signer, SigningKey}; + + let rng = &mut rand::thread_rng(); + + // Construct message, verifying_key, and signature. + let signing_key = SigningKey::generate(rng); + let alice_public_key = PublicKeyEd25519(signing_key.verifying_key().to_bytes()); + + alice_deposits_cis2_tokens( + &mut chain, + smart_contract_wallet, + cis2_token_contract_address, + alice_public_key, + ); + + let service_fee_amount: TokenAmountU256 = TokenAmountU256(1.into()); + let transfer_amount: TokenAmountU256 = TokenAmountU256(5.into()); + let contract_token_id: TokenIdVec = TokenIdVec(vec![TOKEN_ID.0]); + + let message = WithdrawMessage { + entry_point: OwnedEntrypointName::new_unchecked("withdrawCis2Tokens".to_string()), + expiry_time: Timestamp::now(), + nonce: 0u64, + service_fee_recipient: SERVICE_FEE_RECIPIENT_KEY, + simple_withdraws: vec![WithdrawBatch { + to: Receiver::Account(BOB), + withdraw_amount: SigningAmount::TokenAmount(TokenAmount { + token_amount: transfer_amount, + token_id: contract_token_id.clone(), + cis2_token_contract_address, + }), + data: AdditionalData::empty(), + }], + service_fee_amount: SigningAmount::TokenAmount(TokenAmount { + token_amount: service_fee_amount, + token_id: contract_token_id.clone(), + cis2_token_contract_address, + }), + }; + + let mut withdraw = Withdraw { + signer: alice_public_key, + signature: SIGNATURE, + message: message.clone(), + }; + + // Get the message hash to be signed. + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + address: smart_contract_wallet, + receive_name: OwnedReceiveName::new_unchecked( + "smart_contract_wallet.viewWithdrawMessageHash".to_string(), + ), + message: OwnedParameter::from_serial(&message) + .expect("Should be a valid inut parameter"), + }) + .expect("Should be able to query viewWithdrawMessageHash"); + + let signature = signing_key.sign(&invoke.return_value); + + withdraw.signature = SignatureEd25519(signature.to_bytes()); + + let withdraw_param = WithdrawParameter { + transfers: vec![withdraw.clone()], + }; + + // Check balances in state. + let token_balance_of_bob = get_balances(&chain, cis2_token_contract_address); + + assert_eq!(token_balance_of_bob.0, [TokenAmountU64(0u64)]); + + let update = chain + .contract_update( + SIGNER, + CHARLIE, + CHARLIE_ADDR, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "smart_contract_wallet.withdrawCis2Tokens".to_string(), + ), + address: smart_contract_wallet, + message: OwnedParameter::from_serial(&withdraw_param) + .expect("Withdraw cis2 tokens params"), + }, + ) + .print_emitted_events() + .expect("Should be able to withdraw cis2 tokens"); + + // Check that Alice now has 100 tokens and Bob has 0 tokens on their public + // keys. + let balances = get_cis2_tokens_balances_from_alice_and_bob_and_service_fee_recipient( + &chain, + smart_contract_wallet, + cis2_token_contract_address, + alice_public_key, + ); + assert_eq!(balances.0, [ + TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()) - transfer_amount - service_fee_amount, + TokenAmountU256(0.into()), + TokenAmountU256(service_fee_amount.into()) + ]); + + // Check balances in state. + let token_balance_of_bob = get_balances(&chain, cis2_token_contract_address); + + assert_eq!(token_balance_of_bob.0, [TokenAmountU64(transfer_amount.0.as_u64())]); + + // Check that the logs are correct. + let events = deserialize_update_events_of_specified_contract(&update, smart_contract_wallet); + + assert_eq!(events, [ + Event::InternalCis2TokensTransfer(InternalCis2TokensTransferEvent { + token_amount: service_fee_amount, + token_id: contract_token_id.clone(), + cis2_token_contract_address, + from: alice_public_key, + to: SERVICE_FEE_RECIPIENT_KEY + }), + Event::WithdrawCis2Tokens(WithdrawCis2TokensEvent { + token_amount: transfer_amount, + token_id: contract_token_id, + cis2_token_contract_address, + from: alice_public_key, + to: BOB_ADDR + }), + Event::Nonce(NonceEvent { + public_key: alice_public_key, + nonce: 0, + }) + ]); +} + /// Test internal transfer of ccd. #[test] fn test_internal_transfer_ccd() { @@ -254,7 +395,7 @@ fn test_internal_transfer_ccd() { service_fee_amount: SigningAmount::CCDAmount(service_fee_amount), }; - let mut internal_transfer_param = InternalTransfer { + let mut internal_transfer = InternalTransfer { signer: alice_public_key, signature: SIGNATURE, message: message.clone(), @@ -275,10 +416,10 @@ fn test_internal_transfer_ccd() { let signature = signing_key.sign(&invoke.return_value); - internal_transfer_param.signature = SignatureEd25519(signature.to_bytes()); + internal_transfer.signature = SignatureEd25519(signature.to_bytes()); let internal_transfer_param = InternalTransferParameter { - transfers: vec![internal_transfer_param.clone()], + transfers: vec![internal_transfer.clone()], }; let update = chain @@ -381,7 +522,7 @@ fn test_internal_transfer_cis2_tokens() { }], }; - let mut internal_transfer_param = InternalTransfer { + let mut internal_transfer = InternalTransfer { signer: alice_public_key, signature: SIGNATURE, message: message.clone(), @@ -402,10 +543,10 @@ fn test_internal_transfer_cis2_tokens() { let signature = signing_key.sign(&invoke.return_value); - internal_transfer_param.signature = SignatureEd25519(signature.to_bytes()); + internal_transfer.signature = SignatureEd25519(signature.to_bytes()); let internal_transfer_param = InternalTransferParameter { - transfers: vec![internal_transfer_param.clone()], + transfers: vec![internal_transfer.clone()], }; let update = chain @@ -725,3 +866,29 @@ fn deserialize_update_events_of_specified_contract( .flatten() .collect() } + +/// Get the `TOKEN_ID` balances for Bob. +fn get_balances( + chain: &Chain, + contract_address: ContractAddress, +) -> ContractBalanceOfQueryResponse { + let balance_of_params = ContractBalanceOfQueryParams { + queries: vec![BalanceOfQuery { + token_id: TOKEN_ID, + address: BOB_ADDR, + }], + }; + + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked("cis2_multi.balanceOf".to_string()), + address: contract_address, + message: OwnedParameter::from_serial(&balance_of_params) + .expect("BalanceOf params"), + }) + .expect("Invoke balanceOf"); + let rv: ContractBalanceOfQueryResponse = + invoke.parse_return_value().expect("BalanceOf return value"); + rv +} From 29b72ca81b059900c951ddfa56c2c14d0c348be3 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Tue, 2 Apr 2024 21:29:38 +0300 Subject: [PATCH 18/28] Add comments --- .../cis5-smart-contract-wallet/src/lib.rs | 894 ++++++++++-------- .../cis5-smart-contract-wallet/tests/tests.rs | 20 +- 2 files changed, 521 insertions(+), 393 deletions(-) diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index 2d933cb03..b708a6752 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -1,9 +1,49 @@ -//! # +//! A smart contract wallet. +//! +//! This contract implements the CIS-5 (Concordium standard interface 5) that +//! defines a smart contract wallet that can hold and transfer native currency +//! and CIS-2 tokens. https://proposals.concordium.software/CIS/cis-5.html +//! +//! Native currency/CIS-2 tokens can be deposited into the smart contract wallet +//! by specifying to which public key (PublicKeyEd25519 schema) the deposit +//! should be assigned. Authorization for token and currency transfers from the +//! smart contract's assigned public key balance is exclusively granted to the +//! holder of the corresponding private key, ensuring self-custodial control +//! over the assets. +//! +//! Transfers of native currency and CIS-2 token balances do not require +//! on-chain transaction submissions. Instead, the holder of the corresponding +//! private key can generate a valid signature and identify a third party to +//! submit the transaction on-chain, potentially incentivizing the third-party +//! involvement through a service fees. The `withdraw` and `internalTransfer` +//! functions transfer (authorized as part of the message that was signed) the +//! amount of service fee to the service fee recipient's public key. +//! +//! Any withdrawal (native currency or CIS-2 tokens) to a smart contract will +//! invoke a `receiveHook` function on that smart contract. +//! +//! The three main actions in the smart contract wallet that can be taken are: +//! - *deposit*: assigns the balance to a public key within the smart contract +//! wallet. +//! - *internal transfer*: assigns the balance to a new public key within the +//! smart contract wallet. +//! - *withdraw*: withdraws the balance out of the smart contract wallet to a +//! native account or smart contract. +//! +//! The goal of this standard is to simplify the account creation onboarding +//! flow on Concordium. Users can hold/transfer native currency/CIS-2 tokens +//! without a valid account as a starting point (no KYC required). Once users +//! are ready to go through the KYC process to create a valid account on +//! Concordium, they can withdraw assets out of the smart contract wallet. use concordium_cis2::*; use concordium_std::*; -// TODO: look up genesis hash -const GENESIS_HASH: [u8; 32] = [1u8; 32]; +// The testnet genesis hash is: +// 0x4221332d34e1694168c2a0c0b3fd0f273809612cb13d000d5c2e00e85f50f796 +const TESTNET_GENESIS_HASH: [u8; 32] = [ + 66, 33, 51, 45, 52, 225, 105, 65, 104, 194, 160, 192, 179, 253, 15, 39, 56, 9, 97, 44, 177, 61, + 0, 13, 92, 46, 0, 232, 95, 80, 247, 150, +]; /// The standard identifier for the CIS-5: Smart Contract Wallet Standard. pub const CIS5_STANDARD_IDENTIFIER: StandardIdentifier<'static> = @@ -17,133 +57,150 @@ const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = pub type ContractTokenId = TokenIdVec; /// Contract token amount. -/// Since the tokens are non-fungible the total supply of any token will be at -/// most 1 and it is fine to use a small type for representing token amounts. +/// Since the wallet should be able to hold the balance of any CIS-2 token +/// contract, we use the largest TokenAmount. pub type ContractTokenAmount = TokenAmountU256; -type TransferParameter = TransferParams; - /// Tagged events to be serialized for the event log. #[derive(Debug, Serial, Deserial, PartialEq, Eq, SchemaType)] #[concordium(repr(u8))] pub enum Event { - /// The event tracks when a role is revoked from an address. - #[concordium(tag = 244)] - InternalCis2TokensTransfer(InternalCis2TokensTransferEvent), - /// The event tracks when a role is revoked from an address. - #[concordium(tag = 245)] - InternalNativeCurrencyTransfer(InternalNativeCurrencyTransferEvent), - /// The event tracks when a role is revoked from an address. - #[concordium(tag = 246)] - WithdrawCis2Tokens(WithdrawCis2TokensEvent), - /// The event tracks when a role is revoked from an address. - #[concordium(tag = 247)] - WithdrawNativeCurrency(WithdrawNativeCurrencyEvent), - /// The event tracks when a role is revoked from an address. - #[concordium(tag = 248)] - DepositCis2Tokens(DepositCis2TokensEvent), - /// The event tracks when a role is revoked from an address. - #[concordium(tag = 249)] - DepositNativeCurrency(DepositNativeCurrencyEvent), - /// Cis3 event. - /// The event tracks the nonce used by the signer of the `PermitMessage` - /// whenever the `permit` function is invoked. + /// The event tracks the nonce used in the message that was signed. #[concordium(tag = 250)] Nonce(NonceEvent), + /// The event tracks ever time a CCD amount received by the contract is + /// assigned to a public key. + #[concordium(tag = 249)] + DepositNativeCurrency(DepositNativeCurrencyEvent), + /// The event tracks ever time a token amount received by the contract is + /// assigned to a public key. + #[concordium(tag = 248)] + DepositCis2Tokens(DepositCis2TokensEvent), + /// The event tracks ever time a CCD amount held by a public key is + /// withdrawn to an address. + #[concordium(tag = 247)] + WithdrawNativeCurrency(WithdrawNativeCurrencyEvent), + /// The event tracks ever time a token amount held by a public key is + /// withdrawn to an address. + #[concordium(tag = 246)] + WithdrawCis2Tokens(WithdrawCis2TokensEvent), + /// The event tracks ever time a CCD amount held by a public key is + /// transferred to another public key within the contract. + #[concordium(tag = 245)] + InternalNativeCurrencyTransfer(InternalNativeCurrencyTransferEvent), + /// The event tracks ever time a token amount held by a public key is + /// transferred to another public key within the contract. + #[concordium(tag = 244)] + InternalCis2TokensTransfer(InternalCis2TokensTransferEvent), } +/// The `NonceEvent` is logged whenever a signature is checked. The event +/// tracks the nonce used by the signer of the message. #[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] -pub struct InternalCis2TokensTransferEvent { - /// Account that signed the `PermitMessage`. - pub token_amount: ContractTokenAmount, - /// The nonce that was used in the `PermitMessage`. - pub token_id: ContractTokenId, - /// The nonce that was used in the `PermitMessage`. - pub cis2_token_contract_address: ContractAddress, - /// The nonce that was used in the `PermitMessage`. - pub from: PublicKeyEd25519, - /// The nonce that was used in the `PermitMessage`. - pub to: PublicKeyEd25519, +pub struct NonceEvent { + /// Account that signed the message. + pub public_key: PublicKeyEd25519, + /// The nonce that was used in the message. + pub nonce: u64, } +/// The `DepositNativeCurrencyEvent` is logged whenever a CCD amount received by +/// the contract is assigned to a public key. #[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] -pub struct InternalNativeCurrencyTransferEvent { - /// Account that signed the `PermitMessage`. +pub struct DepositNativeCurrencyEvent { + /// The CCD amount assigned to a public key. pub ccd_amount: Amount, - /// The nonce that was used in the `PermitMessage`. - pub from: PublicKeyEd25519, - /// The nonce that was used in the `PermitMessage`. + /// The address that invoked the deposit entrypoint. + pub from: Address, + /// The public key that the CCD amount is assigned to. pub to: PublicKeyEd25519, } +/// The `DepositCis2TokensEvent` is logged whenever a token amount received by +/// the contract is assigned to a public key. #[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] -pub struct WithdrawCis2TokensEvent { - /// Account that signed the `PermitMessage`. +pub struct DepositCis2TokensEvent { + /// The token amount assigned to a public key. pub token_amount: ContractTokenAmount, - /// The nonce that was used in the `PermitMessage`. + /// The token id of the token deposit. pub token_id: ContractTokenId, - /// The nonce that was used in the `PermitMessage`. + /// The token contract address of the token deposit. pub cis2_token_contract_address: ContractAddress, - /// The nonce that was used in the `PermitMessage`. - pub from: PublicKeyEd25519, - /// The nonce that was used in the `PermitMessage`. - pub to: Address, + /// The address that invoked the deposit entrypoint. + pub from: Address, + /// The public key that the CCD amount is assigned to. + pub to: PublicKeyEd25519, } +/// The `WithdrawNativeCurrencyEvent` is logged whenever a CCD amount held by a +/// public key is withdrawn to an address. #[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] pub struct WithdrawNativeCurrencyEvent { - /// Account that signed the `PermitMessage`. + /// The CCD amount withdrawn. pub ccd_amount: Amount, - /// The nonce that was used in the `PermitMessage`. + /// The public key that the CCD amount will be withdrawn from. pub from: PublicKeyEd25519, - /// The nonce that was used in the `PermitMessage`. + /// The address that the CCD amount is withdrawn to. pub to: Address, } +/// The `WithdrawCis2TokensEvent` is logged whenever a token amount held by a +/// public key is withdrawn to an address. #[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] -pub struct DepositCis2TokensEvent { - /// Account that signed the `PermitMessage`. +pub struct WithdrawCis2TokensEvent { + /// The token amount withdrawn. pub token_amount: ContractTokenAmount, - /// The nonce that was used in the `PermitMessage`. + /// The token id of the token withdrawn. pub token_id: ContractTokenId, - /// The nonce that was used in the `PermitMessage`. + /// The token contract address of the token withdrawn. pub cis2_token_contract_address: ContractAddress, - /// The nonce that was used in the `PermitMessage`. - pub from: Address, - /// The nonce that was used in the `PermitMessage`. - pub to: PublicKeyEd25519, + /// The public key that the token amount will be withdrawn from. + pub from: PublicKeyEd25519, + /// The address that the token amount is withdrawn to. + pub to: Address, } +/// The `InternalNativeCurrencyTransferEvent` is logged whenever a CCD amount +/// held by a public key is transferred to another public key within the +/// contract. #[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] -pub struct DepositNativeCurrencyEvent { - /// Account that signed the `PermitMessage`. +pub struct InternalNativeCurrencyTransferEvent { + /// The CCD amount transferred. pub ccd_amount: Amount, - /// The nonce that was used in the `PermitMessage`. - pub from: Address, - /// The nonce that was used in the `PermitMessage`. + /// The public key that the CCD amount will be transferred from. + pub from: PublicKeyEd25519, + /// The public key that the CCD amount is transferred to. pub to: PublicKeyEd25519, } -/// The NonceEvent is logged when the `permit` function is invoked. The event -/// tracks the nonce used by the signer of the `PermitMessage`. +/// The `InternalCis2TokensTransferEvent` is logged whenever a token amount held +/// by a public key is transferred to another public key within the contract. #[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] -pub struct NonceEvent { - /// Account that signed the `PermitMessage`. - pub public_key: PublicKeyEd25519, - /// The nonce that was used in the `PermitMessage`. - pub nonce: u64, +pub struct InternalCis2TokensTransferEvent { + /// The token amount transferred. + pub token_amount: ContractTokenAmount, + /// The token id of the token transferred. + pub token_id: ContractTokenId, + /// The token contract address of the token transferred. + pub cis2_token_contract_address: ContractAddress, + /// The public key that the token amount will be transferred from. + pub from: PublicKeyEd25519, + /// The public key that the token amount is transferred to. + pub to: PublicKeyEd25519, } -/// The state for each address. +/// The token balances stored in the state for each token id. #[derive(Serial, DeserialWithState, Deletable)] #[concordium(state_parameter = "S")] #[concordium(transparent)] struct ContractAddressState { - /// The amount of tokens owned by this address. + /// The amount of tokens owned by public keys mapped for each token id. balances: StateMap, S>, } +// Implementation of the `ContractAddressState`. impl ContractAddressState { + // Creates an new `ContractAddressState` with emtpy balances. fn empty(state_builder: &mut StateBuilder) -> Self { ContractAddressState { balances: state_builder.new_map(), @@ -151,32 +208,29 @@ impl ContractAddressState { } } -/// The contract state, -/// -/// Note: The specification does not specify how to structure the contract state -/// and this could be structured in a more space-efficient way. +/// The contract state. #[derive(Serial, DeserialWithState)] #[concordium(state_parameter = "S")] struct State { - /// The state of addresses. + /// The token balances stored in the state. token_balances: StateMap, S>, - /// + /// The CCD balances stored in the state. native_balances: StateMap, /// 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 + /// A registry to link a public key to its next nonce. The nonce is used to /// prevent replay attacks of the signed message. The nonce is increased /// sequentially every time a signed message (corresponding to the - /// account) is successfully executed in the `permit` function. This + /// public key) is successfully executed. This /// mapping keeps track of the next nonce that needs to be used by the - /// account to generate a signature. + /// public key to generate a signature. nonces_registry: StateMap, } // Functions for creating, updating and querying the contract state. impl State { - /// Creates a new state with no tokens. + /// Creates a new state with emtpy balances. fn empty(state_builder: &mut StateBuilder) -> Self { State { native_balances: state_builder.new_map(), @@ -186,41 +240,38 @@ impl State { } } - /// Get the current balance of a given token ID for a given address. - /// Results in an error if the token ID does not exist in the state. - /// Since this contract only contains NFTs, the balance will always be - /// either 1 or 0. + /// Gets the current native currency balance of a given public key. + /// Returns a balance of 0 if the public key does not exist in the state. + fn balance_native_currency(&self, public_key: &PublicKeyEd25519) -> ContractResult { + Ok(self.native_balances.get(public_key).map(|s| *s).unwrap_or_else(Amount::zero)) + } + + /// Gets the current balance of a given token ID, a given token contract, + /// and a given public key. Returns a balance of 0 if the token + /// contract, token id or public key does not exist in the state. fn balance_tokens( &self, token_id: &ContractTokenId, cis2_token_contract_address: &ContractAddress, public_key: &PublicKeyEd25519, ) -> ContractResult { - let zero_token_amount = TokenAmountU256(0u8.into()); - Ok(self .token_balances .get(cis2_token_contract_address) .map(|a| { a.balances .get(token_id) - .map(|s| s.get(public_key).map(|s| *s).unwrap_or_else(|| zero_token_amount)) - .unwrap_or_else(|| zero_token_amount) + .map(|b| { + b.get(public_key).map(|c| *c).unwrap_or_else(|| TokenAmountU256(0.into())) + }) + .unwrap_or_else(|| TokenAmountU256(0.into())) }) - .unwrap_or_else(|| zero_token_amount)) - } - - /// Get the current balance of a given token ID for a given address. - /// Results in an error if the token ID does not exist in the state. - /// Since this contract only contains NFTs, the balance will always be - /// either 1 or 0. - fn balance_native_currency(&self, public_key: &PublicKeyEd25519) -> ContractResult { - Ok(self.native_balances.get(public_key).map(|s| *s).unwrap_or_else(Amount::zero)) + .unwrap_or_else(|| TokenAmountU256(0.into()))) } - /// Update the state with a transfer of some token. - /// Results in an error if the token ID does not exist in the state or if - /// the from address have insufficient tokens to do the transfer. + /// Updates the state with a transfer of CCD amount and logs an + /// `InternalNativeCurrencyTransfer` event. Results in an error if the + /// from public key has insufficient balance to do the transfer. fn transfer_native_currency( &mut self, from_public_key: PublicKeyEd25519, @@ -261,9 +312,10 @@ impl State { Ok(()) } - /// Update the state with a transfer of some token. - /// Results in an error if the token ID does not exist in the state or if - /// the from address have insufficient tokens to do the transfer. + /// Updates the state with a transfer of some tokens and logs an + /// `InternalCis2TokensTransfer` event. Results in an error if the token + /// contract, or the token id does not exist in the state or the from public + /// key has insufficient balance to do the transfer. fn transfer_cis2_tokens( &mut self, from_public_key: PublicKeyEd25519, @@ -273,9 +325,8 @@ impl State { token_amount: ContractTokenAmount, logger: &mut impl HasLogger, ) -> ReceiveResult<()> { - let zero_token_amount = TokenAmountU256(0u8.into()); // A zero transfer does not modify the state. - if token_amount != zero_token_amount { + if token_amount != TokenAmountU256(0.into()) { let mut contract_balances = self .token_balances .entry(cis2_token_contract_address) @@ -299,7 +350,7 @@ impl State { drop(from_cis2_token_balance); let mut to_cis2_token_balance = - token_balances.entry(to_public_key).or_insert_with(|| TokenAmountU256(0u8.into())); + token_balances.entry(to_public_key).or_insert_with(|| TokenAmountU256(0.into())); // CHECK: can overflow happen *to_cis2_token_balance += token_amount; @@ -316,7 +367,7 @@ impl State { Ok(()) } - /// Check if state contains any implementors for a given standard. + /// Checks if the 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) { SupportResult::SupportBy(addresses.to_vec()) @@ -325,7 +376,7 @@ impl State { } } - /// Set implementors for a given standard. + /// Sets implementors for a given standard. fn set_implementors( &mut self, std_id: StandardIdentifierOwned, @@ -345,20 +396,28 @@ pub enum CustomContractError { LogFull, // -2 /// Failed logging: Log is malformed. LogMalformed, // -3 - /// Invalid contract name. + /// Failed because an account cannot call the entry point. OnlyContract, // -4 - InsufficientFunds, // -5 - WrongSignature, // -6 - NonceMismatch, // -7 - WrongContract, // -8 - Expired, // -9 - WrongEntryPoint, // -10 - UnAuthorized, // -11 - WrongSigningAmountType, // -12 + /// Failed because the public key has an insufficient balance. + InsufficientFunds, // -5 + /// Failed because the signature is invalid. + WrongSignature, // -6 + /// Failed because the nonce is wrong in the message. + NonceMismatch, // -7 + /// Failed because the signature is expired. + Expired, // -8 + /// Failed because the message was intended for a different entry point. + WrongEntryPoint, // -9 + /// Failed because the sender is unauthorized to invoke the entry point. + UnAuthorized, // -10 + /// Failed because the signed amount type is wrong. + WrongSigningAmountType, // -11 } +/// ContractError type. pub type ContractError = Cis2Error; +/// ContractResult type. pub type ContractResult = Result; /// Mapping CustomContractError to ContractError @@ -366,12 +425,17 @@ impl From for ContractError { fn from(c: CustomContractError) -> Self { Cis2Error::Custom(c) } } +// Contract functions + +/// Initializes the contract instance with no balances. #[init(contract = "smart_contract_wallet", event = "Event", error = "CustomContractError")] fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { Ok(State::empty(state_builder)) } -/// +/// The function is payable and deposits/assigns the send CCD amount +/// (native currency) to a public key. +/// Logs a `DepositNativeCurrency` event. #[receive( contract = "smart_contract_wallet", name = "depositNativeCurrency", @@ -389,10 +453,11 @@ fn deposit_native_currency( ) -> ReceiveResult<()> { let to: PublicKeyEd25519 = ctx.parameter_cursor().get()?; - let mut public_key_balances = + let mut public_key_balance = host.state_mut().native_balances.entry(to).or_insert_with(Amount::zero); - *public_key_balances = amount; + // CHECK: overflow can happen + *public_key_balance += amount; logger.log(&Event::DepositNativeCurrency(DepositNativeCurrencyEvent { ccd_amount: amount, @@ -403,11 +468,15 @@ fn deposit_native_currency( Ok(()) } -/// +/// The function should be called through the receive hook mechanism of a CIS-2 +/// token contract. The function deposits/assigns the sent CIS-2 tokens to a +/// public key. Logs a `DepositCis2Tokens` event. +/// It rejects if: +/// - Sender is not a contract. #[receive( contract = "smart_contract_wallet", name = "depositCis2Tokens", - parameter = "OnReceivingCis2DataParams", + parameter = "OnReceivingCis2DataParams", error = "CustomContractError", enable_logger, mutable @@ -423,9 +492,9 @@ fn deposit_cis2_tokens( PublicKeyEd25519, > = ctx.parameter_cursor().get()?; - // Ensure that only contracts can call this hook function - let contract_sender_address = match ctx.sender() { - Address::Contract(contract_sender_address) => contract_sender_address, + // Ensures that only contracts can call this hook function. + let sender_contract_address = match ctx.sender() { + Address::Contract(sender_contract_address) => sender_contract_address, Address::Account(_) => bail!(CustomContractError::OnlyContract.into()), }; @@ -433,7 +502,7 @@ fn deposit_cis2_tokens( let mut contract_balances = state .token_balances - .entry(contract_sender_address) + .entry(sender_contract_address) .or_insert_with(|| ContractAddressState::empty(builder)); let mut contract_token_balances = contract_balances @@ -443,14 +512,15 @@ fn deposit_cis2_tokens( let mut cis2_token_balance = contract_token_balances .entry(cis2_hook_param.data) - .or_insert_with(|| TokenAmountU256(0u8.into())); + .or_insert_with(|| TokenAmountU256(0.into())); + // CHECK: overflow can happen *cis2_token_balance += cis2_hook_param.amount; logger.log(&Event::DepositCis2Tokens(DepositCis2TokensEvent { token_amount: cis2_hook_param.amount, token_id: cis2_hook_param.token_id, - cis2_token_contract_address: contract_sender_address, + cis2_token_contract_address: sender_contract_address, from: cis2_hook_param.from, to: cis2_hook_param.data, }))?; @@ -458,70 +528,83 @@ fn deposit_cis2_tokens( Ok(()) } +/// The native currency or CIS-2 token amount signed in the message. #[derive(Debug, Serialize, Clone, SchemaType)] pub enum SigningAmount { + /// The CCD amount signed in the message. CCDAmount(Amount), + /// The token amount signed in the message. TokenAmount(TokenAmount), } +/// The token amount signed in the message. #[derive(Debug, Serialize, Clone, SchemaType)] pub struct TokenAmount { + /// The token amount signed in the message. pub token_amount: ContractTokenAmount, - /// The ID of the token being transferred. + /// The token id of the token signed in the message. pub token_id: ContractTokenId, - /// + /// The token contract of the token signed in the message. pub cis2_token_contract_address: ContractAddress, } -/// A single transfer of some amount of a token. -// Note: For the serialization to be derived according to the CIS2 -// specification, the order of the fields cannot be changed. +/// A single withdrawal of native currency or some amount of tokens. #[derive(Debug, Serialize, Clone, SchemaType)] -pub struct InternalTransferBatch { - /// The address receiving the tokens being transferred. - pub to: PublicKeyEd25519, - /// The amount of tokens being transferred. - pub transfer_amount: SigningAmount, +pub struct Withdraw { + /// The address receiving the native currency or tokens being withdrawn. + pub to: Receiver, + /// The amount being withdrawn. + pub withdraw_amount: SigningAmount, + /// Some additional data for the receive hook function. + pub data: AdditionalData, } +/// The withdraw message that is signed by the signer. #[derive(Debug, Serialize, Clone, SchemaType)] -pub struct InternalTransferMessage { - /// The address owning the tokens being transferred. +pub struct WithdrawMessage { + /// The entry_point that the signature is intended for. pub entry_point: OwnedEntrypointName, - /// The address owning the tokens being transferred. + /// A timestamp to make the signatures expire. pub expiry_time: Timestamp, - /// The address owning the tokens being transferred. + /// A nonce to prevent replay attacks. pub nonce: u64, - /// The address owning the tokens being transferred. + /// The recipient public key of the service fee. pub service_fee_recipient: PublicKeyEd25519, - /// The amount of tokens being transferred. + /// The amount of native currency or tokens to be received as a service fee. pub service_fee_amount: SigningAmount, - /// List of balance queries. + /// List of withdrawals. #[concordium(size_length = 2)] - pub simple_transfers: Vec, + pub simple_withdraws: Vec, } +/// A batch of withdrawals signed by a signer. #[derive(Debug, Serialize, Clone, SchemaType)] -pub struct InternalTransfer { - /// The address owning the tokens being transferred. +pub struct WithdrawBatch { + /// The signer public key. pub signer: PublicKeyEd25519, - /// The address owning the tokens being transferred. + /// The signature. pub signature: SignatureEd25519, - /// - pub message: InternalTransferMessage, + /// The message being signed. + pub message: WithdrawMessage, } -/// The parameter type for the contract function `balanceOfNativeCurrency`. -// Note: For the serialization to be derived according to the CIS2 -// specification, the order of the fields cannot be changed. +/// The parameter type for the contract functions +/// `withdrawNativeCurrency/withdrawCis2Tokens`. #[derive(Debug, Serialize, SchemaType)] #[concordium(transparent)] -pub struct InternalTransferParameter { - /// List of balance queries. +pub struct WithdrawParameter { + /// List of withdraw batches. #[concordium(size_length = 2)] - pub transfers: Vec, + pub withdraws: Vec, } +/// The `TransferParameter` type for the `transfer` function in the CIS-2 token +/// contract. +type TransferParameter = TransferParams; + +/// Calculates the message hash from the message bytes. +/// It prepends the message bytes with a context string consisting of the +/// `genesis_hash` and this contract address. fn calculate_message_hash_from_bytes( message_bytes: &[u8], crypto_primitives: &impl HasCryptoPrimitives, @@ -530,7 +613,7 @@ fn calculate_message_hash_from_bytes( // We prepend the message with a context string consistent of the genesis_hash // and this contract address. let mut msg_prepend = [0; 32 + 16]; - msg_prepend[0..32].copy_from_slice(GENESIS_HASH.as_ref()); + msg_prepend[0..32].copy_from_slice(TESTNET_GENESIS_HASH.as_ref()); msg_prepend[32..40].copy_from_slice(&ctx.self_address().index.to_le_bytes()); msg_prepend[40..48].copy_from_slice(&ctx.self_address().subindex.to_le_bytes()); @@ -538,202 +621,12 @@ fn calculate_message_hash_from_bytes( Ok(crypto_primitives.hash_sha2_256(&[&msg_prepend[0..48], &message_bytes].concat()).0) } -fn validate_signature_and_increase_nonce_internal_transfer_message( - message: InternalTransferMessage, - signer: PublicKeyEd25519, - signature: SignatureEd25519, - host: &mut Host, - crypto_primitives: &impl HasCryptoPrimitives, - ctx: &ReceiveContext, -) -> ContractResult<()> { - // Check signature is not expired. - ensure!(message.expiry_time > ctx.metadata().slot_time(), CustomContractError::Expired.into()); - - // Calculate the message hash. - let message_hash = - calculate_message_hash_from_bytes(&to_bytes(&message), crypto_primitives, ctx)?; - - // Check signature. - let valid_signature = - crypto_primitives.verify_ed25519_signature(signer, signature, &message_hash); - ensure!(valid_signature, CustomContractError::WrongSignature.into()); - - // Update the nonce. - let mut entry = host.state_mut().nonces_registry.entry(signer).or_insert_with(|| 0); - - // Get the current nonce. - let nonce_state = *entry; - // Bump nonce. - *entry += 1; - - // Check the nonce to prevent replay attacks. - ensure_eq!(message.nonce, nonce_state, CustomContractError::NonceMismatch.into()); - - Ok(()) -} - -/// Helper function to calculate the `message_hash`. -#[receive( - contract = "smart_contract_wallet", - name = "viewInternalTransferMessageHash", - parameter = "InternalTransferMessage", - return_value = "[u8;32]", - error = "ContractError", - crypto_primitives, - mutable -)] -fn contract_view_internal_transfer_message_hash( - ctx: &ReceiveContext, - _host: &mut Host, - crypto_primitives: &impl HasCryptoPrimitives, -) -> ContractResult<[u8; 32]> { - // Parse the parameter. - let param: InternalTransferMessage = ctx.parameter_cursor().get()?; - - calculate_message_hash_from_bytes(&to_bytes(¶m), crypto_primitives, ctx) -} - +/// Validates the withdraw message signature and increases the public key nonce. /// -#[receive( - contract = "smart_contract_wallet", - name = "internalTransferNativeCurrency", - parameter = "InternalTransferParameter", - error = "CustomContractError", - crypto_primitives, - enable_logger, - mutable -)] -fn internal_transfer_native_currency( - ctx: &ReceiveContext, - host: &mut Host, - logger: &mut impl HasLogger, - crypto_primitives: &impl HasCryptoPrimitives, -) -> ReceiveResult<()> { - // Parse the parameter. - let param: InternalTransferParameter = ctx.parameter_cursor().get()?; - - for internal_transfer in param.transfers { - let InternalTransfer { - signer, - signature, - message, - } = internal_transfer.clone(); - - let InternalTransferMessage { - entry_point, - expiry_time: _, - nonce, - service_fee_recipient, - service_fee_amount, - simple_transfers, - } = message.clone(); - - ensure_eq!( - entry_point, - "internalTransferNativeCurrency", - CustomContractError::WrongEntryPoint.into() - ); - - let service_fee_ccd_amount = match service_fee_amount { - SigningAmount::CCDAmount(ccd_amount) => ccd_amount, - SigningAmount::TokenAmount(_) => { - bail!(CustomContractError::WrongSigningAmountType.into()) - } - }; - - validate_signature_and_increase_nonce_internal_transfer_message( - message.clone(), - signer, - signature, - host, - crypto_primitives, - ctx, - )?; - - // Transfer service fee - host.state_mut().transfer_native_currency( - signer, - service_fee_recipient, - service_fee_ccd_amount, - logger, - )?; - - for InternalTransferBatch { - to, - transfer_amount, - } in simple_transfers - { - let ccd_amount = match transfer_amount { - SigningAmount::CCDAmount(ccd_amount) => ccd_amount, - SigningAmount::TokenAmount(_) => { - bail!(CustomContractError::WrongSigningAmountType.into()) - } - }; - - // Update the contract state - host.state_mut().transfer_native_currency(signer, to, ccd_amount, logger)?; - } - - logger.log(&Event::Nonce(NonceEvent { - public_key: signer, - nonce, - }))?; - } - - Ok(()) -} - -/// A single transfer of some amount of a token. -// Note: For the serialization to be derived according to the CIS2 -// specification, the order of the fields cannot be changed. -#[derive(Debug, Serialize, Clone, SchemaType)] -pub struct WithdrawBatch { - /// The address receiving the tokens being transferred. - pub to: Receiver, - /// The amount of tokens being transferred. - pub withdraw_amount: SigningAmount, - /// - pub data: AdditionalData, -} - -#[derive(Debug, Serialize, Clone, SchemaType)] -pub struct WithdrawMessage { - /// The address owning the tokens being transferred. - pub entry_point: OwnedEntrypointName, - /// The address owning the tokens being transferred. - pub expiry_time: Timestamp, - /// The address owning the tokens being transferred. - pub nonce: u64, - /// The address owning the tokens being transferred. - pub service_fee_recipient: PublicKeyEd25519, - /// The amount of tokens being transferred. - pub service_fee_amount: SigningAmount, - /// List of balance queries. - #[concordium(size_length = 2)] - pub simple_withdraws: Vec, -} - -#[derive(Debug, Serialize, Clone, SchemaType)] -pub struct Withdraw { - /// The address owning the tokens being transferred. - pub signer: PublicKeyEd25519, - /// The address owning the tokens being transferred. - pub signature: SignatureEd25519, - /// - pub message: WithdrawMessage, -} - -/// The parameter type for the contract function `balanceOfNativeCurrency`. -// Note: For the serialization to be derived according to the CIS2 -// specification, the order of the fields cannot be changed. -#[derive(Debug, Serialize, SchemaType)] -#[concordium(transparent)] -pub struct WithdrawParameter { - /// List of balance queries. - #[concordium(size_length = 2)] - pub transfers: Vec, -} - +/// It rejects if: +/// - If the message is expired. +/// - If the signature is invalid. +/// - If the nonce is wrong. fn validate_signature_and_increase_nonce_withdraw_message( message: WithdrawMessage, signer: PublicKeyEd25519, @@ -742,14 +635,14 @@ fn validate_signature_and_increase_nonce_withdraw_message( crypto_primitives: &impl HasCryptoPrimitives, ctx: &ReceiveContext, ) -> ContractResult<()> { - // Check signature is not expired. + // Check that the signature is not expired. ensure!(message.expiry_time > ctx.metadata().slot_time(), CustomContractError::Expired.into()); // Calculate the message hash. - let message_hash = + let message_hash: [u8; 32] = calculate_message_hash_from_bytes(&to_bytes(&message), crypto_primitives, ctx)?; - // Check signature. + // Check the signature. let valid_signature = crypto_primitives.verify_ed25519_signature(signer, signature, &message_hash); ensure!(valid_signature, CustomContractError::WrongSignature.into()); @@ -757,18 +650,16 @@ fn validate_signature_and_increase_nonce_withdraw_message( // Update the nonce. let mut entry = host.state_mut().nonces_registry.entry(signer).or_insert_with(|| 0); - // Get the current nonce. - let nonce_state = *entry; + // Check the nonce to prevent replay attacks. + ensure_eq!(message.nonce, *entry, CustomContractError::NonceMismatch.into()); + // Bump nonce. *entry += 1; - // Check the nonce to prevent replay attacks. - ensure_eq!(message.nonce, nonce_state, CustomContractError::NonceMismatch.into()); - Ok(()) } -/// Helper function to calculate the `message_hash`. +/// Helper function to calculate the `WithdrawMessageHash`. #[receive( contract = "smart_contract_wallet", name = "viewWithdrawMessageHash", @@ -789,7 +680,24 @@ fn contract_view_withdraw_message_hash( calculate_message_hash_from_bytes(&to_bytes(¶m), crypto_primitives, ctx) } +/// The function executes a list of CCDs (native currency) withdrawals +/// to native accounts and/or smart contracts. +/// When withdrawing CCD to a contract address, a CCD receive hook function is +/// triggered. +/// +/// The function logs `WithdrawNativeCurrency`, `InternalNativeCurrencyTransfer` +/// and `Nonce` events. /// +/// It rejects if: +/// - If fails to parse the parameter. +/// - If the message was intended for a different entry point. +/// - If the `SigningAmountType` is not CCD for the service fee transfer or for +/// any withdrawal. +/// - If the message is expired. +/// - If the signature is invalid. +/// - If the nonce is wrong. +/// - If the `signer` has an insufficient balance. +/// - If the CCD receive hook function reverts for any withdrawal. #[receive( contract = "smart_contract_wallet", name = "withdrawNativeCurrency", @@ -808,12 +716,12 @@ fn withdraw_native_currency( // Parse the parameter. let param: WithdrawParameter = ctx.parameter_cursor().get()?; - for internal_transfer in param.transfers { - let Withdraw { + for withdraw_batch in param.withdraws { + let WithdrawBatch { signer, signature, message, - } = internal_transfer.clone(); + } = withdraw_batch; let WithdrawMessage { entry_point, @@ -838,7 +746,7 @@ fn withdraw_native_currency( }; validate_signature_and_increase_nonce_withdraw_message( - message.clone(), + message, signer, signature, host, @@ -854,7 +762,7 @@ fn withdraw_native_currency( logger, )?; - for WithdrawBatch { + for Withdraw { to, withdraw_amount, data, @@ -882,14 +790,14 @@ fn withdraw_native_currency( *from_public_key_native_balance -= ccd_amount; } - // If the receiver is a contract: invoke the receive hook function. - // Withdraw ccd out of the contract + // Withdraw CCD out of the contract. let to_address = match to { Receiver::Account(account_address) => { host.invoke_transfer(&account_address, ccd_amount)?; Address::Account(account_address) } Receiver::Contract(contract_address, function) => { + // If the receiver is a contract: invoke the receive hook function. host.invoke_contract( &contract_address, &data, @@ -916,7 +824,24 @@ fn withdraw_native_currency( Ok(()) } +/// The function executes a list of token withdrawals to native accounts and/or +/// smart contracts. This function calls the `transfer` function on the CIS-2 +/// token contract for every withdrawal. +/// +/// The function logs `WithdrawCis2Tokens`, `InternalNativeCurrencyTransfer` +/// and `Nonce` events. /// +/// It rejects if: +/// - If fails to parse the parameter. +/// - If the message was intended for a different entry point. +/// - If the `SigningAmountType` is not a CIS-2 token for the service fee +/// transfer or for any withdrawal. +/// - If the message is expired. +/// - If the signature is invalid. +/// - If the nonce is wrong. +/// - If the `signer` has an insufficient balance. +/// - If the `transfer` function on the CIS-2 contract reverts for any +/// withdrawal. #[receive( contract = "smart_contract_wallet", name = "withdrawCis2Tokens", @@ -935,12 +860,12 @@ fn withdraw_cis2_tokens( // Parse the parameter. let param: WithdrawParameter = ctx.parameter_cursor().get()?; - for internal_transfer in param.transfers { - let Withdraw { + for withdraw_batch in param.withdraws { + let WithdrawBatch { signer, signature, message, - } = internal_transfer.clone(); + } = withdraw_batch; let WithdrawMessage { entry_point, @@ -961,7 +886,7 @@ fn withdraw_cis2_tokens( }; validate_signature_and_increase_nonce_withdraw_message( - message.clone(), + message, signer, signature, host, @@ -979,7 +904,7 @@ fn withdraw_cis2_tokens( logger, )?; - for WithdrawBatch { + for Withdraw { to, withdraw_amount, data, @@ -1016,6 +941,7 @@ fn withdraw_cis2_tokens( *from_cis2_token_balance -= single_withdraw.token_amount; } + // Create Transfer parameter. let data: TransferParameter = TransferParams(vec![Transfer { token_id: single_withdraw.token_id.clone(), amount: single_withdraw.token_amount, @@ -1024,6 +950,7 @@ fn withdraw_cis2_tokens( data, }]); + // Invoke the `transfer` function on the CIS-2 token contract. host.invoke_contract( &single_withdraw.cis2_token_contract_address, &data, @@ -1054,6 +981,207 @@ fn withdraw_cis2_tokens( Ok(()) } +/// A single transfer of native currency or some amount of tokens. +#[derive(Debug, Serialize, Clone, SchemaType)] +pub struct InternalTransfer { + /// The public key receiving the tokens being transferred. + pub to: PublicKeyEd25519, + /// The amount of tokens being transferred. + pub transfer_amount: SigningAmount, +} + +/// The transfer message that is signed by the signer. +#[derive(Debug, Serialize, Clone, SchemaType)] +pub struct InternalTransferMessage { + /// The entry_point that the signature is intended for. + pub entry_point: OwnedEntrypointName, + /// A timestamp to make the signatures expire. + pub expiry_time: Timestamp, + /// A nonce to prevent replay attacks. + pub nonce: u64, + /// The recipient public key of the service fee. + pub service_fee_recipient: PublicKeyEd25519, + /// The amount of native currency or tokens to be received as a service fee. + pub service_fee_amount: SigningAmount, + /// List of transfers. + #[concordium(size_length = 2)] + pub simple_transfers: Vec, +} + +/// A batch of transfers signed by a signer. +#[derive(Debug, Serialize, Clone, SchemaType)] +pub struct InternalTransferBatch { + /// The signer public key. + pub signer: PublicKeyEd25519, + /// The signature. + pub signature: SignatureEd25519, + /// The message being signed. + pub message: InternalTransferMessage, +} + +/// The parameter type for the contract functions +/// `internalTransferNativeCurrency/internalTransferCis2Tokens`. +#[derive(Debug, Serialize, SchemaType)] +#[concordium(transparent)] +pub struct InternalTransferParameter { + /// List of transfer batches. + #[concordium(size_length = 2)] + pub transfers: Vec, +} + +/// Validates the transfer message signature and increases the public key nonce. +/// +/// It rejects if: +/// - If the message is expired. +/// - If the signature is invalid. +/// - If the nonce is wrong. +fn validate_signature_and_increase_nonce_internal_transfer_message( + message: InternalTransferMessage, + signer: PublicKeyEd25519, + signature: SignatureEd25519, + host: &mut Host, + crypto_primitives: &impl HasCryptoPrimitives, + ctx: &ReceiveContext, +) -> ContractResult<()> { + // Check that the signature is not expired. + ensure!(message.expiry_time > ctx.metadata().slot_time(), CustomContractError::Expired.into()); + + // Calculate the message hash. + let message_hash = + calculate_message_hash_from_bytes(&to_bytes(&message), crypto_primitives, ctx)?; + + // Check the signature. + let valid_signature = + crypto_primitives.verify_ed25519_signature(signer, signature, &message_hash); + ensure!(valid_signature, CustomContractError::WrongSignature.into()); + + // Update the nonce. + let mut entry = host.state_mut().nonces_registry.entry(signer).or_insert_with(|| 0); + + // Check the nonce to prevent replay attacks. + ensure_eq!(message.nonce, *entry, CustomContractError::NonceMismatch.into()); + + // Bump nonce. + *entry += 1; + + Ok(()) +} + +/// Helper function to calculate the `InternalTransferMessageHash`. +#[receive( + contract = "smart_contract_wallet", + name = "viewInternalTransferMessageHash", + parameter = "InternalTransferMessage", + return_value = "[u8;32]", + error = "ContractError", + crypto_primitives, + mutable +)] +fn contract_view_internal_transfer_message_hash( + ctx: &ReceiveContext, + _host: &mut Host, + crypto_primitives: &impl HasCryptoPrimitives, +) -> ContractResult<[u8; 32]> { + // Parse the parameter. + let param: InternalTransferMessage = ctx.parameter_cursor().get()?; + + calculate_message_hash_from_bytes(&to_bytes(¶m), crypto_primitives, ctx) +} + +/// +/// +/// +/// +/// +#[receive( + contract = "smart_contract_wallet", + name = "internalTransferNativeCurrency", + parameter = "InternalTransferParameter", + error = "CustomContractError", + crypto_primitives, + enable_logger, + mutable +)] +fn internal_transfer_native_currency( + ctx: &ReceiveContext, + host: &mut Host, + logger: &mut impl HasLogger, + crypto_primitives: &impl HasCryptoPrimitives, +) -> ReceiveResult<()> { + // Parse the parameter. + let param: InternalTransferParameter = ctx.parameter_cursor().get()?; + + for internal_transfer in param.transfers { + let InternalTransferBatch { + signer, + signature, + message, + } = internal_transfer.clone(); + + let InternalTransferMessage { + entry_point, + expiry_time: _, + nonce, + service_fee_recipient, + service_fee_amount, + simple_transfers, + } = message.clone(); + + ensure_eq!( + entry_point, + "internalTransferNativeCurrency", + CustomContractError::WrongEntryPoint.into() + ); + + let service_fee_ccd_amount = match service_fee_amount { + SigningAmount::CCDAmount(ccd_amount) => ccd_amount, + SigningAmount::TokenAmount(_) => { + bail!(CustomContractError::WrongSigningAmountType.into()) + } + }; + + validate_signature_and_increase_nonce_internal_transfer_message( + message.clone(), + signer, + signature, + host, + crypto_primitives, + ctx, + )?; + + // Transfer service fee + host.state_mut().transfer_native_currency( + signer, + service_fee_recipient, + service_fee_ccd_amount, + logger, + )?; + + for InternalTransfer { + to, + transfer_amount, + } in simple_transfers + { + let ccd_amount = match transfer_amount { + SigningAmount::CCDAmount(ccd_amount) => ccd_amount, + SigningAmount::TokenAmount(_) => { + bail!(CustomContractError::WrongSigningAmountType.into()) + } + }; + + // Update the contract state + host.state_mut().transfer_native_currency(signer, to, ccd_amount, logger)?; + } + + logger.log(&Event::Nonce(NonceEvent { + public_key: signer, + nonce, + }))?; + } + + Ok(()) +} + /// #[receive( contract = "smart_contract_wallet", @@ -1074,7 +1202,7 @@ fn internal_transfer_cis2_tokens( let param: InternalTransferParameter = ctx.parameter_cursor().get()?; for cis2_tokens_internal_transfer in param.transfers { - let InternalTransfer { + let InternalTransferBatch { signer, signature, message, @@ -1121,7 +1249,7 @@ fn internal_transfer_cis2_tokens( logger, )?; - for InternalTransferBatch { + for InternalTransfer { to, transfer_amount, } in simple_transfers diff --git a/examples/cis5-smart-contract-wallet/tests/tests.rs b/examples/cis5-smart-contract-wallet/tests/tests.rs index 87cd1c6a4..5017bcfdc 100644 --- a/examples/cis5-smart-contract-wallet/tests/tests.rs +++ b/examples/cis5-smart-contract-wallet/tests/tests.rs @@ -126,7 +126,7 @@ fn test_withdraw_ccd() { expiry_time: Timestamp::now(), nonce: 0u64, service_fee_recipient: SERVICE_FEE_RECIPIENT_KEY, - simple_withdraws: vec![WithdrawBatch { + simple_withdraws: vec![Withdraw { to: Receiver::Account(BOB), withdraw_amount: SigningAmount::CCDAmount(transfer_amount), data: AdditionalData::empty(), @@ -134,7 +134,7 @@ fn test_withdraw_ccd() { service_fee_amount: SigningAmount::CCDAmount(service_fee_amount), }; - let mut withdraw = Withdraw { + let mut withdraw = WithdrawBatch { signer: alice_public_key, signature: SIGNATURE, message: message.clone(), @@ -158,7 +158,7 @@ fn test_withdraw_ccd() { withdraw.signature = SignatureEd25519(signature.to_bytes()); let withdraw_param = WithdrawParameter { - transfers: vec![withdraw.clone()], + withdraws: vec![withdraw.clone()], }; let ccd_balance_bob_before = chain.account_balance(BOB).unwrap().total; @@ -251,7 +251,7 @@ fn test_withdraw_cis2_tokens() { expiry_time: Timestamp::now(), nonce: 0u64, service_fee_recipient: SERVICE_FEE_RECIPIENT_KEY, - simple_withdraws: vec![WithdrawBatch { + simple_withdraws: vec![Withdraw { to: Receiver::Account(BOB), withdraw_amount: SigningAmount::TokenAmount(TokenAmount { token_amount: transfer_amount, @@ -267,7 +267,7 @@ fn test_withdraw_cis2_tokens() { }), }; - let mut withdraw = Withdraw { + let mut withdraw = WithdrawBatch { signer: alice_public_key, signature: SIGNATURE, message: message.clone(), @@ -291,7 +291,7 @@ fn test_withdraw_cis2_tokens() { withdraw.signature = SignatureEd25519(signature.to_bytes()); let withdraw_param = WithdrawParameter { - transfers: vec![withdraw.clone()], + withdraws: vec![withdraw.clone()], }; // Check balances in state. @@ -388,14 +388,14 @@ fn test_internal_transfer_ccd() { expiry_time: Timestamp::now(), nonce: 0u64, service_fee_recipient: SERVICE_FEE_RECIPIENT_KEY, - simple_transfers: vec![InternalTransferBatch { + simple_transfers: vec![InternalTransfer { to: BOB_PUBLIC_KEY, transfer_amount: SigningAmount::CCDAmount(transfer_amount), }], service_fee_amount: SigningAmount::CCDAmount(service_fee_amount), }; - let mut internal_transfer = InternalTransfer { + let mut internal_transfer = InternalTransferBatch { signer: alice_public_key, signature: SIGNATURE, message: message.clone(), @@ -512,7 +512,7 @@ fn test_internal_transfer_cis2_tokens() { token_id: contract_token_id.clone(), cis2_token_contract_address, }), - simple_transfers: vec![InternalTransferBatch { + simple_transfers: vec![InternalTransfer { to: BOB_PUBLIC_KEY, transfer_amount: SigningAmount::TokenAmount(TokenAmount { token_amount: transfer_amount, @@ -522,7 +522,7 @@ fn test_internal_transfer_cis2_tokens() { }], }; - let mut internal_transfer = InternalTransfer { + let mut internal_transfer = InternalTransferBatch { signer: alice_public_key, signature: SIGNATURE, message: message.clone(), From 184f5e3cedb26e59b52ca74c168bae6e9cc3edaa Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Wed, 3 Apr 2024 15:18:05 +0300 Subject: [PATCH 19/28] Clean up code --- .../cis5-smart-contract-wallet/src/lib.rs | 255 +++++++++--------- .../cis5-smart-contract-wallet/tests/tests.rs | 215 ++++++--------- 2 files changed, 211 insertions(+), 259 deletions(-) diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index b708a6752..4e524901b 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -242,8 +242,8 @@ impl State { /// Gets the current native currency balance of a given public key. /// Returns a balance of 0 if the public key does not exist in the state. - fn balance_native_currency(&self, public_key: &PublicKeyEd25519) -> ContractResult { - Ok(self.native_balances.get(public_key).map(|s| *s).unwrap_or_else(Amount::zero)) + fn balance_native_currency(&self, public_key: &PublicKeyEd25519) -> Amount { + self.native_balances.get(public_key).map(|s| *s).unwrap_or_else(Amount::zero) } /// Gets the current balance of a given token ID, a given token contract, @@ -254,9 +254,8 @@ impl State { token_id: &ContractTokenId, cis2_token_contract_address: &ContractAddress, public_key: &PublicKeyEd25519, - ) -> ContractResult { - Ok(self - .token_balances + ) -> ContractTokenAmount { + self.token_balances .get(cis2_token_contract_address) .map(|a| { a.balances @@ -266,7 +265,7 @@ impl State { }) .unwrap_or_else(|| TokenAmountU256(0.into())) }) - .unwrap_or_else(|| TokenAmountU256(0.into()))) + .unwrap_or_else(|| TokenAmountU256(0.into())) } /// Updates the state with a transfer of CCD amount and logs an @@ -289,7 +288,7 @@ impl State { ensure!( *from_public_key_native_balance >= ccd_amount, - ContractError::InsufficientFunds.into() + CustomContractError::InsufficientFunds.into() ); *from_public_key_native_balance -= ccd_amount; } @@ -343,7 +342,7 @@ impl State { ensure!( *from_cis2_token_balance >= token_amount, - ContractError::InsufficientFunds.into() + CustomContractError::InsufficientFunds.into() ); *from_cis2_token_balance -= token_amount; @@ -387,7 +386,7 @@ impl State { } /// The different errors the contract can produce. -#[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] +#[derive(Serialize, PartialEq, Eq, Reject, SchemaType)] pub enum CustomContractError { /// Failed parsing the parameter. #[from(ParseError)] @@ -411,19 +410,11 @@ pub enum CustomContractError { /// Failed because the sender is unauthorized to invoke the entry point. UnAuthorized, // -10 /// Failed because the signed amount type is wrong. - WrongSigningAmountType, // -11 + WrongAmountType, // -11 } -/// ContractError type. -pub type ContractError = Cis2Error; - /// ContractResult type. -pub type ContractResult = Result; - -/// Mapping CustomContractError to ContractError -impl From for ContractError { - fn from(c: CustomContractError) -> Self { Cis2Error::Custom(c) } -} +pub type ContractResult = Result; // Contract functions @@ -472,7 +463,7 @@ fn deposit_native_currency( /// token contract. The function deposits/assigns the sent CIS-2 tokens to a /// public key. Logs a `DepositCis2Tokens` event. /// It rejects if: -/// - Sender is not a contract. +/// - the sender is not a contract. #[receive( contract = "smart_contract_wallet", name = "depositCis2Tokens", @@ -529,7 +520,7 @@ fn deposit_cis2_tokens( } /// The native currency or CIS-2 token amount signed in the message. -#[derive(Debug, Serialize, Clone, SchemaType)] +#[derive(Serialize, Clone, SchemaType)] pub enum SigningAmount { /// The CCD amount signed in the message. CCDAmount(Amount), @@ -538,7 +529,7 @@ pub enum SigningAmount { } /// The token amount signed in the message. -#[derive(Debug, Serialize, Clone, SchemaType)] +#[derive(Serialize, Clone, SchemaType)] pub struct TokenAmount { /// The token amount signed in the message. pub token_amount: ContractTokenAmount, @@ -549,7 +540,7 @@ pub struct TokenAmount { } /// A single withdrawal of native currency or some amount of tokens. -#[derive(Debug, Serialize, Clone, SchemaType)] +#[derive(Serialize, Clone, SchemaType)] pub struct Withdraw { /// The address receiving the native currency or tokens being withdrawn. pub to: Receiver, @@ -560,7 +551,7 @@ pub struct Withdraw { } /// The withdraw message that is signed by the signer. -#[derive(Debug, Serialize, Clone, SchemaType)] +#[derive(Serialize, Clone, SchemaType)] pub struct WithdrawMessage { /// The entry_point that the signature is intended for. pub entry_point: OwnedEntrypointName, @@ -578,7 +569,7 @@ pub struct WithdrawMessage { } /// A batch of withdrawals signed by a signer. -#[derive(Debug, Serialize, Clone, SchemaType)] +#[derive(Serialize, SchemaType)] pub struct WithdrawBatch { /// The signer public key. pub signer: PublicKeyEd25519, @@ -590,7 +581,7 @@ pub struct WithdrawBatch { /// The parameter type for the contract functions /// `withdrawNativeCurrency/withdrawCis2Tokens`. -#[derive(Debug, Serialize, SchemaType)] +#[derive(Serialize, SchemaType)] #[concordium(transparent)] pub struct WithdrawParameter { /// List of withdraw batches. @@ -624,9 +615,9 @@ fn calculate_message_hash_from_bytes( /// Validates the withdraw message signature and increases the public key nonce. /// /// It rejects if: -/// - If the message is expired. -/// - If the signature is invalid. -/// - If the nonce is wrong. +/// - the message is expired. +/// - the signature is invalid. +/// - the nonce is wrong. fn validate_signature_and_increase_nonce_withdraw_message( message: WithdrawMessage, signer: PublicKeyEd25519, @@ -647,13 +638,13 @@ fn validate_signature_and_increase_nonce_withdraw_message( crypto_primitives.verify_ed25519_signature(signer, signature, &message_hash); ensure!(valid_signature, CustomContractError::WrongSignature.into()); - // Update the nonce. + // Get the nonce. let mut entry = host.state_mut().nonces_registry.entry(signer).or_insert_with(|| 0); // Check the nonce to prevent replay attacks. ensure_eq!(message.nonce, *entry, CustomContractError::NonceMismatch.into()); - // Bump nonce. + // Bump the nonce. *entry += 1; Ok(()) @@ -665,7 +656,7 @@ fn validate_signature_and_increase_nonce_withdraw_message( name = "viewWithdrawMessageHash", parameter = "WithdrawMessage", return_value = "[u8;32]", - error = "ContractError", + error = "CustomContractError", crypto_primitives, mutable )] @@ -689,15 +680,15 @@ fn contract_view_withdraw_message_hash( /// and `Nonce` events. /// /// It rejects if: -/// - If fails to parse the parameter. -/// - If the message was intended for a different entry point. -/// - If the `SigningAmountType` is not CCD for the service fee transfer or for -/// any withdrawal. -/// - If the message is expired. -/// - If the signature is invalid. -/// - If the nonce is wrong. -/// - If the `signer` has an insufficient balance. -/// - If the CCD receive hook function reverts for any withdrawal. +/// - it fails to parse the parameter. +/// - the message was intended for a different entry point. +/// - the `AmountType` is not CCD for the service fee transfer or for any +/// withdrawal. +/// - the message is expired. +/// - the signature is invalid. +/// - the nonce is wrong. +/// - the `signer` has an insufficient balance. +/// - the CCD receive hook function reverts for any withdrawal. #[receive( contract = "smart_contract_wallet", name = "withdrawNativeCurrency", @@ -741,7 +732,7 @@ fn withdraw_native_currency( let service_fee_ccd_amount = match service_fee_amount { SigningAmount::CCDAmount(ccd_amount) => ccd_amount, SigningAmount::TokenAmount(_) => { - bail!(CustomContractError::WrongSigningAmountType.into()) + bail!(CustomContractError::WrongAmountType.into()) } }; @@ -771,7 +762,7 @@ fn withdraw_native_currency( let ccd_amount = match withdraw_amount { SigningAmount::CCDAmount(ccd_amount) => ccd_amount, SigningAmount::TokenAmount(_) => { - bail!(CustomContractError::WrongSigningAmountType.into()) + bail!(CustomContractError::WrongAmountType.into()) } }; @@ -785,7 +776,7 @@ fn withdraw_native_currency( ensure!( *from_public_key_native_balance >= ccd_amount, - ContractError::InsufficientFunds.into() + CustomContractError::InsufficientFunds.into() ); *from_public_key_native_balance -= ccd_amount; } @@ -828,20 +819,19 @@ fn withdraw_native_currency( /// smart contracts. This function calls the `transfer` function on the CIS-2 /// token contract for every withdrawal. /// -/// The function logs `WithdrawCis2Tokens`, `InternalNativeCurrencyTransfer` +/// The function logs `WithdrawCis2Tokens`, `InternalCis2TokensTransfer` /// and `Nonce` events. /// /// It rejects if: -/// - If fails to parse the parameter. -/// - If the message was intended for a different entry point. -/// - If the `SigningAmountType` is not a CIS-2 token for the service fee -/// transfer or for any withdrawal. -/// - If the message is expired. -/// - If the signature is invalid. -/// - If the nonce is wrong. -/// - If the `signer` has an insufficient balance. -/// - If the `transfer` function on the CIS-2 contract reverts for any -/// withdrawal. +/// - it fails to parse the parameter. +/// - the message was intended for a different entry point. +/// - the `AmountType` is not a CIS-2 token for the service fee transfer or for +/// any withdrawal. +/// - the message is expired. +/// - the signature is invalid. +/// - the nonce is wrong. +/// - the `signer` has an insufficient balance. +/// - the `transfer` function on the CIS-2 contract reverts for any withdrawal. #[receive( contract = "smart_contract_wallet", name = "withdrawCis2Tokens", @@ -880,7 +870,7 @@ fn withdraw_cis2_tokens( let service_fee = match service_fee_amount { SigningAmount::CCDAmount(_) => { - bail!(CustomContractError::WrongSigningAmountType.into()) + bail!(CustomContractError::WrongAmountType.into()) } SigningAmount::TokenAmount(ref token_amount) => token_amount, }; @@ -912,7 +902,7 @@ fn withdraw_cis2_tokens( { let single_withdraw = match withdraw_amount { SigningAmount::CCDAmount(_) => { - bail!(CustomContractError::WrongSigningAmountType.into()) + bail!(CustomContractError::WrongAmountType.into()) } SigningAmount::TokenAmount(ref single_withdraw) => single_withdraw, }; @@ -936,7 +926,7 @@ fn withdraw_cis2_tokens( ensure!( *from_cis2_token_balance >= single_withdraw.token_amount, - ContractError::InsufficientFunds.into() + CustomContractError::InsufficientFunds.into() ); *from_cis2_token_balance -= single_withdraw.token_amount; } @@ -982,7 +972,7 @@ fn withdraw_cis2_tokens( } /// A single transfer of native currency or some amount of tokens. -#[derive(Debug, Serialize, Clone, SchemaType)] +#[derive(Serialize, Clone, SchemaType)] pub struct InternalTransfer { /// The public key receiving the tokens being transferred. pub to: PublicKeyEd25519, @@ -991,7 +981,7 @@ pub struct InternalTransfer { } /// The transfer message that is signed by the signer. -#[derive(Debug, Serialize, Clone, SchemaType)] +#[derive(Serialize, Clone, SchemaType)] pub struct InternalTransferMessage { /// The entry_point that the signature is intended for. pub entry_point: OwnedEntrypointName, @@ -1009,7 +999,7 @@ pub struct InternalTransferMessage { } /// A batch of transfers signed by a signer. -#[derive(Debug, Serialize, Clone, SchemaType)] +#[derive(Serialize, SchemaType)] pub struct InternalTransferBatch { /// The signer public key. pub signer: PublicKeyEd25519, @@ -1021,7 +1011,7 @@ pub struct InternalTransferBatch { /// The parameter type for the contract functions /// `internalTransferNativeCurrency/internalTransferCis2Tokens`. -#[derive(Debug, Serialize, SchemaType)] +#[derive(Serialize, SchemaType)] #[concordium(transparent)] pub struct InternalTransferParameter { /// List of transfer batches. @@ -1032,9 +1022,9 @@ pub struct InternalTransferParameter { /// Validates the transfer message signature and increases the public key nonce. /// /// It rejects if: -/// - If the message is expired. -/// - If the signature is invalid. -/// - If the nonce is wrong. +/// - the message is expired. +/// - the signature is invalid. +/// - the nonce is wrong. fn validate_signature_and_increase_nonce_internal_transfer_message( message: InternalTransferMessage, signer: PublicKeyEd25519, @@ -1055,13 +1045,13 @@ fn validate_signature_and_increase_nonce_internal_transfer_message( crypto_primitives.verify_ed25519_signature(signer, signature, &message_hash); ensure!(valid_signature, CustomContractError::WrongSignature.into()); - // Update the nonce. + // Get the nonce. let mut entry = host.state_mut().nonces_registry.entry(signer).or_insert_with(|| 0); // Check the nonce to prevent replay attacks. ensure_eq!(message.nonce, *entry, CustomContractError::NonceMismatch.into()); - // Bump nonce. + // Bump the nonce. *entry += 1; Ok(()) @@ -1073,7 +1063,7 @@ fn validate_signature_and_increase_nonce_internal_transfer_message( name = "viewInternalTransferMessageHash", parameter = "InternalTransferMessage", return_value = "[u8;32]", - error = "ContractError", + error = "CustomContractError", crypto_primitives, mutable )] @@ -1088,11 +1078,21 @@ fn contract_view_internal_transfer_message_hash( calculate_message_hash_from_bytes(&to_bytes(¶m), crypto_primitives, ctx) } +/// The function executes a list of CCD internal transfers to public keys within +/// the smart contract wallet. /// +/// The function logs `InternalNativeCurrencyTransfer` +/// and `Nonce` events. /// -/// -/// -/// +/// It rejects if: +/// - it fails to parse the parameter. +/// - the message was intended for a different entry point. +/// - the `AmountType` is not CCD for the service fee transfer or for any +/// internal transfer. +/// - the message is expired. +/// - the signature is invalid. +/// - the nonce is wrong. +/// - the `signer` has an insufficient balance. #[receive( contract = "smart_contract_wallet", name = "internalTransferNativeCurrency", @@ -1111,12 +1111,12 @@ fn internal_transfer_native_currency( // Parse the parameter. let param: InternalTransferParameter = ctx.parameter_cursor().get()?; - for internal_transfer in param.transfers { + for transfer_batch in param.transfers { let InternalTransferBatch { signer, signature, message, - } = internal_transfer.clone(); + } = transfer_batch; let InternalTransferMessage { entry_point, @@ -1136,12 +1136,12 @@ fn internal_transfer_native_currency( let service_fee_ccd_amount = match service_fee_amount { SigningAmount::CCDAmount(ccd_amount) => ccd_amount, SigningAmount::TokenAmount(_) => { - bail!(CustomContractError::WrongSigningAmountType.into()) + bail!(CustomContractError::WrongAmountType.into()) } }; validate_signature_and_increase_nonce_internal_transfer_message( - message.clone(), + message, signer, signature, host, @@ -1165,7 +1165,7 @@ fn internal_transfer_native_currency( let ccd_amount = match transfer_amount { SigningAmount::CCDAmount(ccd_amount) => ccd_amount, SigningAmount::TokenAmount(_) => { - bail!(CustomContractError::WrongSigningAmountType.into()) + bail!(CustomContractError::WrongAmountType.into()) } }; @@ -1182,7 +1182,21 @@ fn internal_transfer_native_currency( Ok(()) } +/// The function executes a list of token internal transfers to public keys +/// within the smart contract wallet. /// +/// The function logs `InternalCis2TokensTransfer` +/// and `Nonce` events. +/// +/// It rejects: +/// - it fails to parse the parameter. +/// - the message was intended for a different entry point. +/// - the `AmountType` is not a CIS-2 token for the service fee transfer or for +/// any internal transfer. +/// - the message is expired. +/// - the signature is invalid. +/// - the nonce is wrong. +/// - the `signer` has an insufficient balance. #[receive( contract = "smart_contract_wallet", name = "internalTransferCis2Tokens", @@ -1201,12 +1215,12 @@ fn internal_transfer_cis2_tokens( // Parse the parameter. let param: InternalTransferParameter = ctx.parameter_cursor().get()?; - for cis2_tokens_internal_transfer in param.transfers { + for transfer_batch in param.transfers { let InternalTransferBatch { signer, signature, message, - } = cis2_tokens_internal_transfer.clone(); + } = transfer_batch; let InternalTransferMessage { entry_point, @@ -1225,13 +1239,13 @@ fn internal_transfer_cis2_tokens( let service_fee = match service_fee_amount { SigningAmount::CCDAmount(_) => { - bail!(CustomContractError::WrongSigningAmountType.into()) + bail!(CustomContractError::WrongAmountType.into()) } SigningAmount::TokenAmount(token_amount) => token_amount, }; validate_signature_and_increase_nonce_internal_transfer_message( - message.clone(), + message, signer, signature, host, @@ -1254,20 +1268,20 @@ fn internal_transfer_cis2_tokens( transfer_amount, } in simple_transfers { - let transfer = match transfer_amount { + let single_transfer = match transfer_amount { SigningAmount::CCDAmount(_) => { - bail!(CustomContractError::WrongSigningAmountType.into()) + bail!(CustomContractError::WrongAmountType.into()) } - SigningAmount::TokenAmount(token_amount) => token_amount, + SigningAmount::TokenAmount(single_transfer) => single_transfer, }; // Update the contract state host.state_mut().transfer_cis2_tokens( signer, to, - transfer.cis2_token_contract_address, - transfer.token_id, - transfer.token_amount, + single_transfer.cis2_token_contract_address, + single_transfer.token_id, + single_transfer.token_amount, logger, )?; } @@ -1284,7 +1298,7 @@ fn internal_transfer_cis2_tokens( /// 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)] +#[derive(Serialize, SchemaType)] struct SetImplementorsParams { /// The identifier for the standard. id: StandardIdentifierOwned, @@ -1296,18 +1310,18 @@ struct SetImplementorsParams { /// list of contract addresses. /// /// It rejects if: -/// - Sender is not the owner of the contract instance. -/// - It fails to parse the parameter. +/// - the sender is not the owner of the contract instance. +/// - it fails to parse the parameter. #[receive( contract = "smart_contract_wallet", name = "setImplementors", parameter = "SetImplementorsParams", - error = "ContractError", + error = "CustomContractError", mutable )] fn contract_set_implementor(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { // Authorize the sender. - ensure!(ctx.sender().matches_account(&ctx.owner()), ContractError::Unauthorized); + ensure!(ctx.sender().matches_account(&ctx.owner()), CustomContractError::UnAuthorized); // Parse the parameter. let params: SetImplementorsParams = ctx.parameter_cursor().get()?; // Update the implementors in the state @@ -1320,13 +1334,13 @@ fn contract_set_implementor(ctx: &ReceiveContext, host: &mut Host) -> Con /// standard identifiers. /// /// It rejects if: -/// - It fails to parse the parameter. +/// - it fails to parse the parameter. #[receive( contract = "smart_contract_wallet", name = "supports", parameter = "SupportsQueryParams", return_value = "SupportsQueryResponse", - error = "ContractError" + error = "CustomContractError" )] fn contract_supports( ctx: &ReceiveContext, @@ -1348,42 +1362,31 @@ fn contract_supports( Ok(result) } -/// A query for the balance of a given address for a given token. -// Note: For the serialization to be derived according to the CIS2 -// specification, the order of the fields cannot be changed. -#[derive(Debug, Serialize, SchemaType)] -pub struct NativeCurrencyBalanceOfQuery { - /// The public key for which to query the balance of. - pub public_key: PublicKeyEd25519, -} - /// The parameter type for the contract function `balanceOfNativeCurrency`. -// Note: For the serialization to be derived according to the CIS2 -// specification, the order of the fields cannot be changed. -#[derive(Debug, Serialize, SchemaType)] +#[derive(Serialize, SchemaType)] #[concordium(transparent)] pub struct NativeCurrencyBalanceOfParameter { /// List of balance queries. #[concordium(size_length = 2)] - pub queries: Vec, + pub queries: Vec, } /// The response which is sent back when calling the contract function /// `balanceOfNativeCurrency`. /// It consists of the list of results corresponding to the list of queries. -#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] +#[derive(Serialize, SchemaType, PartialEq, Eq)] #[concordium(transparent)] pub struct NativeCurrencyBalanceOfResponse(#[concordium(size_length = 2)] pub Vec); +/// Conversion helper function. impl From> for NativeCurrencyBalanceOfResponse { fn from(results: Vec) -> Self { NativeCurrencyBalanceOfResponse(results) } } -/// Get the balance of given token IDs and addresses. +/// The function queries the CCD balances of a list of public keys. /// /// It rejects if: /// - It fails to parse the parameter. -/// - Any of the queried `token_id` does not exist. #[receive( contract = "smart_contract_wallet", name = "balanceOfNativeCurrency", @@ -1399,32 +1402,28 @@ fn contract_balance_of_native_currency( let params: NativeCurrencyBalanceOfParameter = ctx.parameter_cursor().get()?; // Build the response. let mut response = Vec::with_capacity(params.queries.len()); - for query in params.queries { - // Query the state for balance. - let amount = host.state().balance_native_currency(&query.public_key)?; + for public_key in params.queries { + // Query the state for the balance. + let amount = host.state().balance_native_currency(&public_key); response.push(amount); } let result = NativeCurrencyBalanceOfResponse::from(response); Ok(result) } -/// A query for the balance of a given address for a given token. -// Note: For the serialization to be derived according to the CIS2 -// specification, the order of the fields cannot be changed. -#[derive(Debug, Serialize, SchemaType)] +/// A query for the token balance of a given public key. +#[derive(Serialize, SchemaType)] pub struct Cis2TokensBalanceOfQuery { - /// The ID of the token for which to query the balance of. + /// The ID of the token. pub token_id: ContractTokenId, - /// + /// The token contract address. pub cis2_token_contract_address: ContractAddress, - /// The public key for which to query the balance of. + /// The public key. pub public_key: PublicKeyEd25519, } -/// The parameter type for the contract function `balanceOf`. -// Note: For the serialization to be derived according to the CIS2 -// specification, the order of the fields cannot be changed. -#[derive(Debug, Serialize, SchemaType)] +/// The parameter type for the contract function `balanceOfCis2Tokens`. +#[derive(Serialize, SchemaType)] #[concordium(transparent)] pub struct Cis2TokensBalanceOfParameter { /// List of balance queries. @@ -1433,21 +1432,21 @@ pub struct Cis2TokensBalanceOfParameter { } /// The response which is sent back when calling the contract function -/// `balanceOf`. +/// `balanceOfCis2Tokens`. /// It consists of the list of results corresponding to the list of queries. -#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)] +#[derive(Serialize, SchemaType, PartialEq, Eq)] #[concordium(transparent)] pub struct Cis2TokensBalanceOfResponse(#[concordium(size_length = 2)] pub Vec); +/// Conversion helper function. impl From> for Cis2TokensBalanceOfResponse { fn from(results: Vec) -> Self { Cis2TokensBalanceOfResponse(results) } } -/// Get the balance of given token IDs and addresses. +/// The function queries the token balances of a list of public keys. /// /// It rejects if: /// - It fails to parse the parameter. -/// - Any of the queried `token_id` does not exist. #[receive( contract = "smart_contract_wallet", name = "balanceOfCis2Tokens", @@ -1469,7 +1468,7 @@ fn contract_balance_of_cis2_tokens( &query.token_id, &query.cis2_token_contract_address, &query.public_key, - )?; + ); response.push(amount); } let result = Cis2TokensBalanceOfResponse::from(response); diff --git a/examples/cis5-smart-contract-wallet/tests/tests.rs b/examples/cis5-smart-contract-wallet/tests/tests.rs index 5017bcfdc..a3708cd25 100644 --- a/examples/cis5-smart-contract-wallet/tests/tests.rs +++ b/examples/cis5-smart-contract-wallet/tests/tests.rs @@ -1,4 +1,4 @@ -//! Tests for the `cis2_wCCD` contract. +//! Tests for the `smart_contract_wallet` contract. use cis2_multi::{ContractBalanceOfQueryParams, ContractBalanceOfQueryResponse, MintParams}; use concordium_cis2::*; use concordium_smart_contract_testing::*; @@ -13,77 +13,34 @@ const BOB_ADDR: Address = Address::Account(BOB); const CHARLIE: AccountAddress = AccountAddress([2; 32]); const CHARLIE_ADDR: Address = Address::Account(CHARLIE); -const ALICE_PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([8; 32]); -const BOB_PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([9; 32]); -const SERVICE_FEE_RECIPIENT_KEY: PublicKeyEd25519 = PublicKeyEd25519([4; 32]); - -const SIGNATURE: SignatureEd25519 = SignatureEd25519([ - 68, 134, 96, 171, 184, 199, 1, 93, 76, 87, 144, 68, 55, 180, 93, 56, 107, 95, 127, 112, 24, 55, - 162, 131, 165, 91, 133, 104, 2, 5, 78, 224, 214, 21, 66, 0, 44, 108, 52, 4, 108, 10, 123, 75, - 21, 68, 42, 79, 106, 106, 87, 125, 122, 77, 154, 114, 208, 145, 171, 47, 108, 96, 221, 13, -]); - -const TOKEN_ID: TokenIdU8 = TokenIdU8(4); +const ALICE_PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([7; 32]); +const BOB_PUBLIC_KEY: PublicKeyEd25519 = PublicKeyEd25519([8; 32]); +const SERVICE_FEE_RECIPIENT_KEY: PublicKeyEd25519 = PublicKeyEd25519([9; 32]); /// Initial balance of the accounts. const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); +const TOKEN_ID: TokenIdU8 = TokenIdU8(4); + const AIRDROP_TOKEN_AMOUNT: TokenAmountU64 = TokenAmountU64(100); -const AIRDROP_CCD_AMOUNT: Amount = Amount::from_micro_ccd(100); +const AIRDROP_CCD_AMOUNT: Amount = Amount::from_micro_ccd(200); + +const DUMMY_SIGNATURE: SignatureEd25519 = SignatureEd25519([ + 68, 134, 96, 171, 184, 199, 1, 93, 76, 87, 144, 68, 55, 180, 93, 56, 107, 95, 127, 112, 24, 55, + 162, 131, 165, 91, 133, 104, 2, 5, 78, 224, 214, 21, 66, 0, 44, 108, 52, 4, 108, 10, 123, 75, + 21, 68, 42, 79, 106, 106, 87, 125, 122, 77, 154, 114, 208, 145, 171, 47, 108, 96, 221, 13, +]); /// A signer for all the transactions. const SIGNER: Signer = Signer::with_one_key(); -/// Test that init produces the correct logs. -#[test] -fn test_init() { - let (_chain, _smart_contract_wallet, _cis2_token_contract_address) = - initialize_chain_and_contract(); -} - /// Test depositing of native currency. #[test] fn test_deposit_native_currency() { let (mut chain, smart_contract_wallet, _cis2_token_contract_address) = initialize_chain_and_contract(); - // Check that Alice has 0 CCD and Bob has 0 CCD on their public keys. - let balances = get_native_currency_balance_from_alice_and_bob_and_service_fee_recipient( - &chain, - smart_contract_wallet, - ALICE_PUBLIC_KEY, - ); - assert_eq!(balances.0, [Amount::zero(), Amount::zero(), Amount::zero()]); - - let send_amount = Amount::from_micro_ccd(100); - let update = chain - .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { - amount: send_amount, - receive_name: OwnedReceiveName::new_unchecked( - "smart_contract_wallet.depositNativeCurrency".to_string(), - ), - address: smart_contract_wallet, - message: OwnedParameter::from_serial(&ALICE_PUBLIC_KEY) - .expect("Deposit native currency params"), - }) - .expect("Should be able to deposit CCD"); - - // Check that Alice now has 100 CCD and Bob has 0 CCD on their public keys. - let balances = get_native_currency_balance_from_alice_and_bob_and_service_fee_recipient( - &chain, - smart_contract_wallet, - ALICE_PUBLIC_KEY, - ); - assert_eq!(balances.0, [send_amount, Amount::zero(), Amount::zero()]); - - // Check that the logs are correct. - let events = deserialize_update_events_of_specified_contract(&update, smart_contract_wallet); - - assert_eq!(events, [Event::DepositNativeCurrency(DepositNativeCurrencyEvent { - ccd_amount: send_amount, - from: ALICE_ADDR, - to: ALICE_PUBLIC_KEY, - })]); + alice_deposits_ccd(&mut chain, smart_contract_wallet, ALICE_PUBLIC_KEY); } /// Test depositing of cis2 tokens. @@ -100,7 +57,7 @@ fn test_deposit_cis2_tokens() { ); } -/// Test withdraw of ccd. +/// Test withdrawing of CCD. #[test] fn test_withdraw_ccd() { let (mut chain, smart_contract_wallet, _cis2_token_contract_address) = @@ -110,14 +67,14 @@ fn test_withdraw_ccd() { let rng = &mut rand::thread_rng(); - // Construct message, verifying_key, and signature. + // Construct signing key. let signing_key = SigningKey::generate(rng); let alice_public_key = PublicKeyEd25519(signing_key.verifying_key().to_bytes()); alice_deposits_ccd(&mut chain, smart_contract_wallet, alice_public_key); let service_fee_amount: Amount = Amount::from_micro_ccd(1); - let transfer_amount: Amount = Amount::from_micro_ccd(5); + let withdraw_amount: Amount = Amount::from_micro_ccd(5); let message = WithdrawMessage { entry_point: OwnedEntrypointName::new_unchecked( @@ -128,7 +85,7 @@ fn test_withdraw_ccd() { service_fee_recipient: SERVICE_FEE_RECIPIENT_KEY, simple_withdraws: vec![Withdraw { to: Receiver::Account(BOB), - withdraw_amount: SigningAmount::CCDAmount(transfer_amount), + withdraw_amount: SigningAmount::CCDAmount(withdraw_amount), data: AdditionalData::empty(), }], service_fee_amount: SigningAmount::CCDAmount(service_fee_amount), @@ -136,7 +93,7 @@ fn test_withdraw_ccd() { let mut withdraw = WithdrawBatch { signer: alice_public_key, - signature: SIGNATURE, + signature: DUMMY_SIGNATURE, message: message.clone(), }; @@ -158,10 +115,11 @@ fn test_withdraw_ccd() { withdraw.signature = SignatureEd25519(signature.to_bytes()); let withdraw_param = WithdrawParameter { - withdraws: vec![withdraw.clone()], + withdraws: vec![withdraw], }; - let ccd_balance_bob_before = chain.account_balance(BOB).unwrap().total; + let ccd_balance_bob_before = + chain.account_balance(BOB).expect("Bob should have a balance").total; let update = chain .contract_update( @@ -176,28 +134,28 @@ fn test_withdraw_ccd() { ), address: smart_contract_wallet, message: OwnedParameter::from_serial(&withdraw_param) - .expect("Internal transfer native currency params"), + .expect("Withdraw native currency params"), }, ) - .print_emitted_events() - .expect("Should be able to internally transfer native currency"); + .expect("Should be able to withdraw native currency"); - // Check that Alice now has 100 ccd and Bob has 0 ccd on their public - // keys. + // Check that Alice now has `AIRDROP_CCD_AMOUNT - withdraw_amount - + // service_fee_amount` CCD, Bob has 0 CCD, and service_fee_recipient has + // `service_fee_amount` CCD on their public keys. let balances = get_native_currency_balance_from_alice_and_bob_and_service_fee_recipient( &chain, smart_contract_wallet, alice_public_key, ); assert_eq!(balances.0, [ - AIRDROP_CCD_AMOUNT - transfer_amount - service_fee_amount, + AIRDROP_CCD_AMOUNT - withdraw_amount - service_fee_amount, Amount::zero(), service_fee_amount ]); assert_eq!( - chain.account_balance(BOB).unwrap().total, - ccd_balance_bob_before.checked_add(transfer_amount).unwrap(), + chain.account_balance(BOB).expect("Bob should have a balance").total, + ccd_balance_bob_before.checked_add(withdraw_amount).expect("Expect no overflow"), ); // Check that the logs are correct. @@ -210,7 +168,7 @@ fn test_withdraw_ccd() { to: SERVICE_FEE_RECIPIENT_KEY, }), Event::WithdrawNativeCurrency(WithdrawNativeCurrencyEvent { - ccd_amount: transfer_amount, + ccd_amount: withdraw_amount, from: alice_public_key, to: BOB_ADDR, }), @@ -231,7 +189,7 @@ fn test_withdraw_cis2_tokens() { let rng = &mut rand::thread_rng(); - // Construct message, verifying_key, and signature. + // Construct signing key. let signing_key = SigningKey::generate(rng); let alice_public_key = PublicKeyEd25519(signing_key.verifying_key().to_bytes()); @@ -243,7 +201,7 @@ fn test_withdraw_cis2_tokens() { ); let service_fee_amount: TokenAmountU256 = TokenAmountU256(1.into()); - let transfer_amount: TokenAmountU256 = TokenAmountU256(5.into()); + let withdraw_amount: TokenAmountU256 = TokenAmountU256(5.into()); let contract_token_id: TokenIdVec = TokenIdVec(vec![TOKEN_ID.0]); let message = WithdrawMessage { @@ -254,7 +212,7 @@ fn test_withdraw_cis2_tokens() { simple_withdraws: vec![Withdraw { to: Receiver::Account(BOB), withdraw_amount: SigningAmount::TokenAmount(TokenAmount { - token_amount: transfer_amount, + token_amount: withdraw_amount, token_id: contract_token_id.clone(), cis2_token_contract_address, }), @@ -269,7 +227,7 @@ fn test_withdraw_cis2_tokens() { let mut withdraw = WithdrawBatch { signer: alice_public_key, - signature: SIGNATURE, + signature: DUMMY_SIGNATURE, message: message.clone(), }; @@ -291,13 +249,12 @@ fn test_withdraw_cis2_tokens() { withdraw.signature = SignatureEd25519(signature.to_bytes()); let withdraw_param = WithdrawParameter { - withdraws: vec![withdraw.clone()], + withdraws: vec![withdraw], }; - // Check balances in state. - let token_balance_of_bob = get_balances(&chain, cis2_token_contract_address); + let token_balance_of_bob_before = get_balances(&chain, cis2_token_contract_address); - assert_eq!(token_balance_of_bob.0, [TokenAmountU64(0u64)]); + assert_eq!(token_balance_of_bob_before.0, [TokenAmountU64(0u64)]); let update = chain .contract_update( @@ -315,27 +272,27 @@ fn test_withdraw_cis2_tokens() { .expect("Withdraw cis2 tokens params"), }, ) - .print_emitted_events() .expect("Should be able to withdraw cis2 tokens"); - // Check that Alice now has 100 tokens and Bob has 0 tokens on their public - // keys. - let balances = get_cis2_tokens_balances_from_alice_and_bob_and_service_fee_recipient( + // Check that Alice now has `AIRDROP_TOKEN_AMOUNT - withdraw_amount - + // service_fee_amount` tokens, Bob has 0 tokens, and service_fee_recipient has + // `service_fee_amount` tokens on their public keys. + let balances = get_cis2_token_balances_from_alice_and_bob_and_service_fee_recipient( &chain, smart_contract_wallet, cis2_token_contract_address, alice_public_key, ); assert_eq!(balances.0, [ - TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()) - transfer_amount - service_fee_amount, + TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()) - withdraw_amount - service_fee_amount, TokenAmountU256(0.into()), TokenAmountU256(service_fee_amount.into()) ]); // Check balances in state. - let token_balance_of_bob = get_balances(&chain, cis2_token_contract_address); + let token_balance_of_bob_after = get_balances(&chain, cis2_token_contract_address); - assert_eq!(token_balance_of_bob.0, [TokenAmountU64(transfer_amount.0.as_u64())]); + assert_eq!(token_balance_of_bob_after.0, [TokenAmountU64(withdraw_amount.0.as_u64())]); // Check that the logs are correct. let events = deserialize_update_events_of_specified_contract(&update, smart_contract_wallet); @@ -349,7 +306,7 @@ fn test_withdraw_cis2_tokens() { to: SERVICE_FEE_RECIPIENT_KEY }), Event::WithdrawCis2Tokens(WithdrawCis2TokensEvent { - token_amount: transfer_amount, + token_amount: withdraw_amount, token_id: contract_token_id, cis2_token_contract_address, from: alice_public_key, @@ -372,7 +329,7 @@ fn test_internal_transfer_ccd() { let rng = &mut rand::thread_rng(); - // Construct message, verifying_key, and signature. + // Construct signing key. let signing_key = SigningKey::generate(rng); let alice_public_key = PublicKeyEd25519(signing_key.verifying_key().to_bytes()); @@ -397,7 +354,7 @@ fn test_internal_transfer_ccd() { let mut internal_transfer = InternalTransferBatch { signer: alice_public_key, - signature: SIGNATURE, + signature: DUMMY_SIGNATURE, message: message.clone(), }; @@ -419,7 +376,7 @@ fn test_internal_transfer_ccd() { internal_transfer.signature = SignatureEd25519(signature.to_bytes()); let internal_transfer_param = InternalTransferParameter { - transfers: vec![internal_transfer.clone()], + transfers: vec![internal_transfer], }; let update = chain @@ -438,11 +395,11 @@ fn test_internal_transfer_ccd() { .expect("Internal transfer native currency params"), }, ) - .print_emitted_events() .expect("Should be able to internally transfer native currency"); - // Check that Alice now has 100 ccd and Bob has 0 ccd on their public - // keys. + // Check that Alice now has `AIRDROP_CCD_AMOUNT - transfer_amount - + // service_fee_amount` CCD and Bob has `transfer_amount` CCD, and + // service_fee_recipient has `service_fee_amount` CCD on their public keys. let balances = get_native_currency_balance_from_alice_and_bob_and_service_fee_recipient( &chain, smart_contract_wallet, @@ -485,7 +442,7 @@ fn test_internal_transfer_cis2_tokens() { let rng = &mut rand::thread_rng(); - // Construct message, verifying_key, and signature. + // Construct signing key. let signing_key = SigningKey::generate(rng); let alice_public_key = PublicKeyEd25519(signing_key.verifying_key().to_bytes()); @@ -524,7 +481,7 @@ fn test_internal_transfer_cis2_tokens() { let mut internal_transfer = InternalTransferBatch { signer: alice_public_key, - signature: SIGNATURE, + signature: DUMMY_SIGNATURE, message: message.clone(), }; @@ -546,7 +503,7 @@ fn test_internal_transfer_cis2_tokens() { internal_transfer.signature = SignatureEd25519(signature.to_bytes()); let internal_transfer_param = InternalTransferParameter { - transfers: vec![internal_transfer.clone()], + transfers: vec![internal_transfer], }; let update = chain @@ -565,12 +522,13 @@ fn test_internal_transfer_cis2_tokens() { .expect("Internal transfer cis2 tokens params"), }, ) - .print_emitted_events() .expect("Should be able to internally transfer cis2 tokens"); - // Check that Alice now has 100 tokens and Bob has 0 tokens on their public + // Check that Alice now has `AIRDROP_TOKEN_AMOUNT - transfer_amount - + // service_fee_amount` tokens, Bob has `transfer_amount` tokens, and + // service_fee_recipient has `service_fee_amount` tokens on their public // keys. - let balances = get_cis2_tokens_balances_from_alice_and_bob_and_service_fee_recipient( + let balances = get_cis2_token_balances_from_alice_and_bob_and_service_fee_recipient( &chain, smart_contract_wallet, cis2_token_contract_address, @@ -611,7 +569,7 @@ fn test_internal_transfer_cis2_tokens() { /// Setup chain and contract. /// -/// Also creates the two accounts, Alice and Bob. +/// Also creates the three accounts, Alice, Bob, and Charlie. /// /// Alice is the owner of the contract. fn initialize_chain_and_contract() -> (Chain, ContractAddress, ContractAddress) { @@ -628,7 +586,7 @@ fn initialize_chain_and_contract() -> (Chain, ContractAddress, ContractAddress) let deployment = chain.module_deploy_v1_debug(SIGNER, ALICE, module, true).expect("Deploy valid module"); - // Initialize the auction contract. + // Initialize the token contract. let cis2_token_contract_init = chain .contract_init(SIGNER, ALICE, Energy::from(10000), InitContractPayload { amount: Amount::zero(), @@ -644,7 +602,7 @@ fn initialize_chain_and_contract() -> (Chain, ContractAddress, ContractAddress) let deployment = chain.module_deploy_v1_debug(SIGNER, ALICE, module, true).expect("Deploy valid module"); - // Initialize the auction contract. + // Initialize the smart contract wallet. let smart_contract_wallet_init = chain .contract_init(SIGNER, ALICE, Energy::from(10000), InitContractPayload { amount: Amount::zero(), @@ -657,8 +615,8 @@ fn initialize_chain_and_contract() -> (Chain, ContractAddress, ContractAddress) (chain, smart_contract_wallet_init.contract_address, cis2_token_contract_init.contract_address) } -/// Get the token balances for Alice and Bob. -fn get_cis2_tokens_balances_from_alice_and_bob_and_service_fee_recipient( +/// Get the token balances for Alice, Bob, and the service_fee_recipient. +fn get_cis2_token_balances_from_alice_and_bob_and_service_fee_recipient( chain: &Chain, smart_contract_wallet: ContractAddress, cis2_token_contract_address: ContractAddress, @@ -691,32 +649,23 @@ fn get_cis2_tokens_balances_from_alice_and_bob_and_service_fee_recipient( ), address: smart_contract_wallet, message: OwnedParameter::from_serial(&balance_of_params) - .expect("BalanceOf params"), + .expect("BalanceOf cis2 token params"), }) - .expect("Invoke balanceOf"); + .expect("Invoke balanceOf cis2 token"); let rv: Cis2TokensBalanceOfResponse = - invoke.parse_return_value().expect("BalanceOf return value"); + invoke.parse_return_value().expect("BalanceOf CIS-2 token return value"); rv } -/// Get the native currency balances for Alice and Bob. +/// Get the native currency balances for Alice, Bob, and the +/// service_fee_recipient. fn get_native_currency_balance_from_alice_and_bob_and_service_fee_recipient( chain: &Chain, smart_contract_wallet: ContractAddress, alice_public_key: PublicKeyEd25519, ) -> NativeCurrencyBalanceOfResponse { let balance_of_params = NativeCurrencyBalanceOfParameter { - queries: vec![ - NativeCurrencyBalanceOfQuery { - public_key: alice_public_key, - }, - NativeCurrencyBalanceOfQuery { - public_key: BOB_PUBLIC_KEY, - }, - NativeCurrencyBalanceOfQuery { - public_key: SERVICE_FEE_RECIPIENT_KEY, - }, - ], + queries: vec![alice_public_key, BOB_PUBLIC_KEY, SERVICE_FEE_RECIPIENT_KEY], }; let invoke = chain .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { @@ -734,7 +683,7 @@ fn get_native_currency_balance_from_alice_and_bob_and_service_fee_recipient( rv } -/// Get the token balances for Alice and Bob. +/// Alice deposits cis2 tokens into the smart contract wallet. fn alice_deposits_cis2_tokens( chain: &mut Chain, smart_contract_wallet: ContractAddress, @@ -756,8 +705,9 @@ fn alice_deposits_cis2_tokens( data: AdditionalData::from(to_bytes(&alice_public_key)), }; - // Check that Alice has 0 tokens and Bob has 0 tokens on their public keys. - let balances = get_cis2_tokens_balances_from_alice_and_bob_and_service_fee_recipient( + // Check that Alice has 0 tokens, Bob has 0 tokens, and the + // service_fee_recipient has 0 tokens on their public keys. + let balances = get_cis2_token_balances_from_alice_and_bob_and_service_fee_recipient( &chain, smart_contract_wallet, cis2_token_contract_address, @@ -769,6 +719,7 @@ fn alice_deposits_cis2_tokens( TokenAmountU256(0u8.into()) ]); + // Deposit tokens. let update = chain .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { amount: Amount::zero(), @@ -779,9 +730,9 @@ fn alice_deposits_cis2_tokens( }) .expect("Should be able to deposit cis2 tokens"); - // Check that Alice now has 100 tokens and Bob has 0 tokens on their public - // keys. - let balances = get_cis2_tokens_balances_from_alice_and_bob_and_service_fee_recipient( + // Check that Alice now has AIRDROP_TOKEN_AMOUNT tokens, Bob has 0 tokens, and + // the service_fee_recipient has 0 tokens on their public keys. + let balances = get_cis2_token_balances_from_alice_and_bob_and_service_fee_recipient( &chain, smart_contract_wallet, cis2_token_contract_address, @@ -805,13 +756,14 @@ fn alice_deposits_cis2_tokens( })]); } -/// Get the token balances for Alice and Bob. +/// Alice deposits CCD into the smart contract wallet. fn alice_deposits_ccd( chain: &mut Chain, smart_contract_wallet: ContractAddress, alice_public_key: PublicKeyEd25519, ) { - // Check that Alice has 0 CCD and Bob has 0 CCD on their public keys. + // Check that Alice has 0 CCD, Bob has 0 CCD, and the service_fee_recipient has + // 0 CCD on their public keys. let balances = get_native_currency_balance_from_alice_and_bob_and_service_fee_recipient( &chain, smart_contract_wallet, @@ -831,7 +783,8 @@ fn alice_deposits_ccd( }) .expect("Should be able to deposit CCD"); - // Check that Alice now has 100 CCD and Bob has 0 CCD on their public keys. + // Check that Alice now has AIRDROP_CCD_AMOUNT CCD, Bob has 0 CCD, and the + // service_fee_recipient has 0 CCD on their public keys. let balances = get_native_currency_balance_from_alice_and_bob_and_service_fee_recipient( &chain, smart_contract_wallet, @@ -867,7 +820,7 @@ fn deserialize_update_events_of_specified_contract( .collect() } -/// Get the `TOKEN_ID` balances for Bob. +/// Get the `TOKEN_ID` balance for Bob. fn get_balances( chain: &Chain, contract_address: ContractAddress, From 69f01f817fd631694d8f1ee045070c17e43bb347 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Wed, 3 Apr 2024 20:59:36 +0300 Subject: [PATCH 20/28] Add CI pipeline --- .github/workflows/linter.yml | 8 ++++++-- concordium-rust-sdk | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 78164db78..a5e97fadc 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -36,6 +36,7 @@ jobs: - examples/cis2-wccd/Cargo.toml - examples/cis3-nft-sponsored-txs/Cargo.toml - examples/counter-notify/Cargo.toml + - examples/cis5-smart-contract-wallet/Cargo.toml - examples/credential-registry/Cargo.toml - examples/eSealing/Cargo.toml - examples/factory/Cargo.toml @@ -630,6 +631,7 @@ jobs: - examples/cis2-multi/Cargo.toml - examples/cis2-dynamic-nft/Cargo.toml - examples/cis2-multi-royalties/Cargo.toml + - examples/cis5-smart-contract-wallet/Cargo.toml - examples/nametoken/Cargo.toml - examples/account-signature-checks/Cargo.toml - examples/bump-alloc-tests/Cargo.toml @@ -714,6 +716,7 @@ jobs: - examples/cis2-nft/Cargo.toml - examples/cis3-nft-sponsored-txs/Cargo.toml - examples/cis2-wccd/Cargo.toml + - examples/cis5-smart-contract-wallet/Cargo.toml - examples/credential-registry/Cargo.toml - examples/factory/Cargo.toml - examples/fib/Cargo.toml @@ -847,6 +850,7 @@ jobs: - examples/cis2-nft - examples/cis3-nft-sponsored-txs - examples/cis2-wccd + - examples/cis5-smart-contract-wallet - examples/credential-registry - examples/factory - examples/fib @@ -894,9 +898,9 @@ jobs: if: ${{ matrix.crates == 'examples/smart-contract-upgrade/contract-version1' }} run: cargo concordium build --out "examples/smart-contract-upgrade/contract-version2/concordium-out/module.wasm.v1" -- --manifest-path "examples/smart-contract-upgrade/contract-version2/Cargo.toml" - # The 'sponsored-tx-enabled-auction' example needs the wasm module `cis2-multi` for its tests. + # The 'sponsored-tx-enabled-auction' and 'cis5-smart-contract-walletn' examples need the wasm module `cis2-multi` for its tests. - name: Build cis2-multi module if needed - if: ${{ matrix.crates == 'examples/sponsored-tx-enabled-auction' }} + if: ${{ matrix.crates == 'examples/sponsored-tx-enabled-auction' || matrix.crates == 'examples/cis5-smart-contract-wallet'}} run: cargo concordium build --out "examples/cis2-multi/concordium-out/module.wasm.v1" -- --manifest-path "examples/cis2-multi/Cargo.toml" - name: Run cargo concordium test diff --git a/concordium-rust-sdk b/concordium-rust-sdk index 5e4b93c0a..6eb5f3b76 160000 --- a/concordium-rust-sdk +++ b/concordium-rust-sdk @@ -1 +1 @@ -Subproject commit 5e4b93c0a0ae7aef16d3bb6c0e62c30424d900bc +Subproject commit 6eb5f3b768256b1e77d6d4340e121ead4a40aa20 From d96da066867734799c339d1d278bb9e41c279978 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Wed, 3 Apr 2024 21:21:46 +0300 Subject: [PATCH 21/28] Fix CI pipeline --- .github/workflows/linter.yml | 1 - examples/cis5-smart-contract-wallet/src/lib.rs | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index a5e97fadc..2e8405908 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -631,7 +631,6 @@ jobs: - examples/cis2-multi/Cargo.toml - examples/cis2-dynamic-nft/Cargo.toml - examples/cis2-multi-royalties/Cargo.toml - - examples/cis5-smart-contract-wallet/Cargo.toml - examples/nametoken/Cargo.toml - examples/account-signature-checks/Cargo.toml - examples/bump-alloc-tests/Cargo.toml diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index 4e524901b..f16b95490 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -381,7 +381,7 @@ impl State { std_id: StandardIdentifierOwned, implementors: Vec, ) { - self.implementors.insert(std_id, implementors); + let _ = self.implementors.insert(std_id, implementors); } } @@ -627,7 +627,7 @@ fn validate_signature_and_increase_nonce_withdraw_message( ctx: &ReceiveContext, ) -> ContractResult<()> { // Check that the signature is not expired. - ensure!(message.expiry_time > ctx.metadata().slot_time(), CustomContractError::Expired.into()); + ensure!(message.expiry_time > ctx.metadata().slot_time(), CustomContractError::Expired); // Calculate the message hash. let message_hash: [u8; 32] = @@ -636,13 +636,13 @@ fn validate_signature_and_increase_nonce_withdraw_message( // Check the signature. let valid_signature = crypto_primitives.verify_ed25519_signature(signer, signature, &message_hash); - ensure!(valid_signature, CustomContractError::WrongSignature.into()); + ensure!(valid_signature, CustomContractError::WrongSignature); // Get the nonce. let mut entry = host.state_mut().nonces_registry.entry(signer).or_insert_with(|| 0); // Check the nonce to prevent replay attacks. - ensure_eq!(message.nonce, *entry, CustomContractError::NonceMismatch.into()); + ensure_eq!(message.nonce, *entry, CustomContractError::NonceMismatch); // Bump the nonce. *entry += 1; @@ -1034,7 +1034,7 @@ fn validate_signature_and_increase_nonce_internal_transfer_message( ctx: &ReceiveContext, ) -> ContractResult<()> { // Check that the signature is not expired. - ensure!(message.expiry_time > ctx.metadata().slot_time(), CustomContractError::Expired.into()); + ensure!(message.expiry_time > ctx.metadata().slot_time(), CustomContractError::Expired); // Calculate the message hash. let message_hash = @@ -1043,13 +1043,13 @@ fn validate_signature_and_increase_nonce_internal_transfer_message( // Check the signature. let valid_signature = crypto_primitives.verify_ed25519_signature(signer, signature, &message_hash); - ensure!(valid_signature, CustomContractError::WrongSignature.into()); + ensure!(valid_signature, CustomContractError::WrongSignature); // Get the nonce. let mut entry = host.state_mut().nonces_registry.entry(signer).or_insert_with(|| 0); // Check the nonce to prevent replay attacks. - ensure_eq!(message.nonce, *entry, CustomContractError::NonceMismatch.into()); + ensure_eq!(message.nonce, *entry, CustomContractError::NonceMismatch); // Bump the nonce. *entry += 1; From a97afdea14e87a97dd381f48c2676b2ee2be4838 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 4 Apr 2024 10:37:59 +0300 Subject: [PATCH 22/28] Add overflow checks --- .../cis5-smart-contract-wallet/src/lib.rs | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index f16b95490..b7ecbc537 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -286,18 +286,17 @@ impl State { .entry(from_public_key) .occupied_or(CustomContractError::InsufficientFunds)?; - ensure!( - *from_public_key_native_balance >= ccd_amount, - CustomContractError::InsufficientFunds.into() - ); - *from_public_key_native_balance -= ccd_amount; + *from_public_key_native_balance = (*from_public_key_native_balance) + .checked_sub(ccd_amount) + .ok_or(CustomContractError::InsufficientFunds)?; } let mut to_public_key_native_balance = self.native_balances.entry(to_public_key).or_insert_with(Amount::zero); - // TODO: check if overflow possible - *to_public_key_native_balance += ccd_amount; + *to_public_key_native_balance = (*to_public_key_native_balance) + .checked_add(ccd_amount) + .ok_or(CustomContractError::Overflow)?; } logger.log(&Event::InternalNativeCurrencyTransfer( @@ -351,7 +350,11 @@ impl State { let mut to_cis2_token_balance = token_balances.entry(to_public_key).or_insert_with(|| TokenAmountU256(0.into())); - // CHECK: can overflow happen + // A well designes CIS-2 token contract should not overflow. + ensure!( + *to_cis2_token_balance + token_amount >= *to_cis2_token_balance, + CustomContractError::Overflow.into() + ); *to_cis2_token_balance += token_amount; } @@ -411,6 +414,8 @@ pub enum CustomContractError { UnAuthorized, // -10 /// Failed because the signed amount type is wrong. WrongAmountType, // -11 + /// Failed because of an overflow in the token/CCD amount. + Overflow, // -12 } /// ContractResult type. @@ -447,8 +452,8 @@ fn deposit_native_currency( let mut public_key_balance = host.state_mut().native_balances.entry(to).or_insert_with(Amount::zero); - // CHECK: overflow can happen - *public_key_balance += amount; + *public_key_balance = + (*public_key_balance).checked_add(amount).ok_or(CustomContractError::Overflow)?; logger.log(&Event::DepositNativeCurrency(DepositNativeCurrencyEvent { ccd_amount: amount, @@ -505,7 +510,12 @@ fn deposit_cis2_tokens( .entry(cis2_hook_param.data) .or_insert_with(|| TokenAmountU256(0.into())); - // CHECK: overflow can happen + // A well designes CIS-2 token contract should not overflow. + ensure!( + *cis2_token_balance + cis2_hook_param.amount >= *cis2_token_balance, + CustomContractError::Overflow.into() + ); + *cis2_token_balance += cis2_hook_param.amount; logger.log(&Event::DepositCis2Tokens(DepositCis2TokensEvent { @@ -774,11 +784,9 @@ fn withdraw_native_currency( .entry(signer) .occupied_or(CustomContractError::InsufficientFunds)?; - ensure!( - *from_public_key_native_balance >= ccd_amount, - CustomContractError::InsufficientFunds.into() - ); - *from_public_key_native_balance -= ccd_amount; + *from_public_key_native_balance = (*from_public_key_native_balance) + .checked_sub(ccd_amount) + .ok_or(CustomContractError::InsufficientFunds)?; } // Withdraw CCD out of the contract. From 3bb4276687dfde424052708a3950ddb62ed72fc0 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 4 Apr 2024 10:56:12 +0300 Subject: [PATCH 23/28] Simplify message validation --- .../cis5-smart-contract-wallet/src/lib.rs | 75 +++++++------------ 1 file changed, 28 insertions(+), 47 deletions(-) diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index b7ecbc537..fad140e93 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -421,6 +421,12 @@ pub enum CustomContractError { /// ContractResult type. pub type ContractResult = Result; +/// Implement custom trait `IsMessage`. +trait IsMessage { + fn expiry_time(&self) -> Timestamp; + fn nonce(&self) -> u64; +} + // Contract functions /// Initializes the contract instance with no balances. @@ -578,6 +584,12 @@ pub struct WithdrawMessage { pub simple_withdraws: Vec, } +impl IsMessage for WithdrawMessage { + fn expiry_time(&self) -> Timestamp { self.expiry_time } + + fn nonce(&self) -> u64 { self.nonce } +} + /// A batch of withdrawals signed by a signer. #[derive(Serialize, SchemaType)] pub struct WithdrawBatch { @@ -622,14 +634,15 @@ fn calculate_message_hash_from_bytes( Ok(crypto_primitives.hash_sha2_256(&[&msg_prepend[0..48], &message_bytes].concat()).0) } -/// Validates the withdraw message signature and increases the public key nonce. +/// Validates the message signature and increases the public key nonce. /// /// It rejects if: /// - the message is expired. /// - the signature is invalid. /// - the nonce is wrong. -fn validate_signature_and_increase_nonce_withdraw_message( - message: WithdrawMessage, +/// - the message hash can not be calculated. +fn validate_signature_and_increase_nonce( + message: T, signer: PublicKeyEd25519, signature: SignatureEd25519, host: &mut Host, @@ -637,7 +650,7 @@ fn validate_signature_and_increase_nonce_withdraw_message( ctx: &ReceiveContext, ) -> ContractResult<()> { // Check that the signature is not expired. - ensure!(message.expiry_time > ctx.metadata().slot_time(), CustomContractError::Expired); + ensure!(message.expiry_time() > ctx.metadata().slot_time(), CustomContractError::Expired); // Calculate the message hash. let message_hash: [u8; 32] = @@ -652,7 +665,7 @@ fn validate_signature_and_increase_nonce_withdraw_message( let mut entry = host.state_mut().nonces_registry.entry(signer).or_insert_with(|| 0); // Check the nonce to prevent replay attacks. - ensure_eq!(message.nonce, *entry, CustomContractError::NonceMismatch); + ensure_eq!(message.nonce(), *entry, CustomContractError::NonceMismatch); // Bump the nonce. *entry += 1; @@ -746,7 +759,7 @@ fn withdraw_native_currency( } }; - validate_signature_and_increase_nonce_withdraw_message( + validate_signature_and_increase_nonce( message, signer, signature, @@ -883,7 +896,7 @@ fn withdraw_cis2_tokens( SigningAmount::TokenAmount(ref token_amount) => token_amount, }; - validate_signature_and_increase_nonce_withdraw_message( + validate_signature_and_increase_nonce( message, signer, signature, @@ -1006,6 +1019,12 @@ pub struct InternalTransferMessage { pub simple_transfers: Vec, } +impl IsMessage for InternalTransferMessage { + fn expiry_time(&self) -> Timestamp { self.expiry_time } + + fn nonce(&self) -> u64 { self.nonce } +} + /// A batch of transfers signed by a signer. #[derive(Serialize, SchemaType)] pub struct InternalTransferBatch { @@ -1027,44 +1046,6 @@ pub struct InternalTransferParameter { pub transfers: Vec, } -/// Validates the transfer message signature and increases the public key nonce. -/// -/// It rejects if: -/// - the message is expired. -/// - the signature is invalid. -/// - the nonce is wrong. -fn validate_signature_and_increase_nonce_internal_transfer_message( - message: InternalTransferMessage, - signer: PublicKeyEd25519, - signature: SignatureEd25519, - host: &mut Host, - crypto_primitives: &impl HasCryptoPrimitives, - ctx: &ReceiveContext, -) -> ContractResult<()> { - // Check that the signature is not expired. - ensure!(message.expiry_time > ctx.metadata().slot_time(), CustomContractError::Expired); - - // Calculate the message hash. - let message_hash = - calculate_message_hash_from_bytes(&to_bytes(&message), crypto_primitives, ctx)?; - - // Check the signature. - let valid_signature = - crypto_primitives.verify_ed25519_signature(signer, signature, &message_hash); - ensure!(valid_signature, CustomContractError::WrongSignature); - - // Get the nonce. - let mut entry = host.state_mut().nonces_registry.entry(signer).or_insert_with(|| 0); - - // Check the nonce to prevent replay attacks. - ensure_eq!(message.nonce, *entry, CustomContractError::NonceMismatch); - - // Bump the nonce. - *entry += 1; - - Ok(()) -} - /// Helper function to calculate the `InternalTransferMessageHash`. #[receive( contract = "smart_contract_wallet", @@ -1148,7 +1129,7 @@ fn internal_transfer_native_currency( } }; - validate_signature_and_increase_nonce_internal_transfer_message( + validate_signature_and_increase_nonce( message, signer, signature, @@ -1252,7 +1233,7 @@ fn internal_transfer_cis2_tokens( SigningAmount::TokenAmount(token_amount) => token_amount, }; - validate_signature_and_increase_nonce_internal_transfer_message( + validate_signature_and_increase_nonce( message, signer, signature, From c4130b954d6bdaefeb772ee6b786dd7b2ce7f812 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Wed, 10 Apr 2024 17:41:10 +0300 Subject: [PATCH 24/28] Address comments --- .github/workflows/linter.yml | 2 +- examples/cis2-multi/src/lib.rs | 5 +- .../cis5-smart-contract-wallet/src/lib.rs | 107 ++++++++++-------- .../cis5-smart-contract-wallet/tests/tests.rs | 2 +- 4 files changed, 65 insertions(+), 51 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 2e8405908..8e992fad8 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -897,7 +897,7 @@ jobs: if: ${{ matrix.crates == 'examples/smart-contract-upgrade/contract-version1' }} run: cargo concordium build --out "examples/smart-contract-upgrade/contract-version2/concordium-out/module.wasm.v1" -- --manifest-path "examples/smart-contract-upgrade/contract-version2/Cargo.toml" - # The 'sponsored-tx-enabled-auction' and 'cis5-smart-contract-walletn' examples need the wasm module `cis2-multi` for its tests. + # The 'sponsored-tx-enabled-auction' and 'cis5-smart-contract-wallet' examples need the wasm module `cis2-multi` for its tests. - name: Build cis2-multi module if needed if: ${{ matrix.crates == 'examples/sponsored-tx-enabled-auction' || matrix.crates == 'examples/cis5-smart-contract-wallet'}} run: cargo concordium build --out "examples/cis2-multi/concordium-out/module.wasm.v1" -- --manifest-path "examples/cis2-multi/Cargo.toml" diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index 6cdd53e69..2c0fd4917 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -958,10 +958,7 @@ fn mint( host: &mut Host, logger: &mut impl HasLogger, ) -> ContractResult<()> { - let to_address = match params.to { - Receiver::Account(address) => Address::Account(address), - Receiver::Contract(address, _) => Address::Contract(address), - }; + let to_address = params.to.address(); let is_blacklisted = host.state().blacklist.contains(&get_canonical_address(to_address)?); diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index fad140e93..eca2ddb67 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -1,23 +1,24 @@ //! A smart contract wallet. //! -//! This contract implements the CIS-5 (Concordium standard interface 5) that -//! defines a smart contract wallet that can hold and transfer native currency -//! and CIS-2 tokens. https://proposals.concordium.software/CIS/cis-5.html +//! This contract implements the CIS-5 (Concordium interoperability +//! specification 5) that defines a smart contract wallet that can hold and +//! transfer native currency and CIS-2 tokens. +//! https://proposals.concordium.software/CIS/cis-5.html //! //! Native currency/CIS-2 tokens can be deposited into the smart contract wallet -//! by specifying to which public key (PublicKeyEd25519 schema) the deposit +//! by specifying to which public key ([PublicKeyEd25519] schema) the deposit //! should be assigned. Authorization for token and currency transfers from the //! smart contract's assigned public key balance is exclusively granted to the //! holder of the corresponding private key, ensuring self-custodial control //! over the assets. //! -//! Transfers of native currency and CIS-2 token balances do not require +//! Transfers of native currency and CIS-2 token balances (meaning `withdraw` +//! and `internalTransfer` functions) do not require //! on-chain transaction submissions. Instead, the holder of the corresponding //! private key can generate a valid signature and identify a third party to //! submit the transaction on-chain, potentially incentivizing the third-party -//! involvement through a service fees. The `withdraw` and `internalTransfer` -//! functions transfer (authorized as part of the message that was signed) the -//! amount of service fee to the service fee recipient's public key. +//! involvement through a service fee. The message that was signed specifies the +//! amount of service fee and the service fee recipient's public key. //! //! Any withdrawal (native currency or CIS-2 tokens) to a smart contract will //! invoke a `receiveHook` function on that smart contract. @@ -68,27 +69,27 @@ pub enum Event { /// The event tracks the nonce used in the message that was signed. #[concordium(tag = 250)] Nonce(NonceEvent), - /// The event tracks ever time a CCD amount received by the contract is + /// The event tracks every time a CCD amount received by the contract is /// assigned to a public key. #[concordium(tag = 249)] DepositNativeCurrency(DepositNativeCurrencyEvent), - /// The event tracks ever time a token amount received by the contract is + /// The event tracks every time a token amount received by the contract is /// assigned to a public key. #[concordium(tag = 248)] DepositCis2Tokens(DepositCis2TokensEvent), - /// The event tracks ever time a CCD amount held by a public key is + /// The event tracks every time a CCD amount held by a public key is /// withdrawn to an address. #[concordium(tag = 247)] WithdrawNativeCurrency(WithdrawNativeCurrencyEvent), - /// The event tracks ever time a token amount held by a public key is + /// The event tracks every time a token amount held by a public key is /// withdrawn to an address. #[concordium(tag = 246)] WithdrawCis2Tokens(WithdrawCis2TokensEvent), - /// The event tracks ever time a CCD amount held by a public key is + /// The event tracks every time a CCD amount held by a public key is /// transferred to another public key within the contract. #[concordium(tag = 245)] InternalNativeCurrencyTransfer(InternalNativeCurrencyTransferEvent), - /// The event tracks ever time a token amount held by a public key is + /// The event tracks every time a token amount held by a public key is /// transferred to another public key within the contract. #[concordium(tag = 244)] InternalCis2TokensTransfer(InternalCis2TokensTransferEvent), @@ -200,7 +201,7 @@ struct ContractAddressState { // Implementation of the `ContractAddressState`. impl ContractAddressState { - // Creates an new `ContractAddressState` with emtpy balances. + /// Creates a new `ContractAddressState` with empty balances. fn empty(state_builder: &mut StateBuilder) -> Self { ContractAddressState { balances: state_builder.new_map(), @@ -276,7 +277,7 @@ impl State { from_public_key: PublicKeyEd25519, to_public_key: PublicKeyEd25519, ccd_amount: Amount, - logger: &mut impl HasLogger, + logger: &mut Logger, ) -> ReceiveResult<()> { // A zero transfer does not modify the state. if ccd_amount != Amount::zero() { @@ -321,7 +322,7 @@ impl State { cis2_token_contract_address: ContractAddress, token_id: ContractTokenId, token_amount: ContractTokenAmount, - logger: &mut impl HasLogger, + logger: &mut Logger, ) -> ReceiveResult<()> { // A zero transfer does not modify the state. if token_amount != TokenAmountU256(0.into()) { @@ -335,22 +336,22 @@ impl State { .entry(token_id.clone()) .occupied_or(CustomContractError::InsufficientFunds)?; - let mut from_cis2_token_balance = token_balances - .entry(from_public_key) - .occupied_or(CustomContractError::InsufficientFunds)?; - - ensure!( - *from_cis2_token_balance >= token_amount, - CustomContractError::InsufficientFunds.into() - ); - *from_cis2_token_balance -= token_amount; + { + let mut from_cis2_token_balance = token_balances + .entry(from_public_key) + .occupied_or(CustomContractError::InsufficientFunds)?; - drop(from_cis2_token_balance); + ensure!( + *from_cis2_token_balance >= token_amount, + CustomContractError::InsufficientFunds.into() + ); + *from_cis2_token_balance -= token_amount; + } let mut to_cis2_token_balance = token_balances.entry(to_public_key).or_insert_with(|| TokenAmountU256(0.into())); - // A well designes CIS-2 token contract should not overflow. + // A well designed CIS-2 token contract should not overflow. ensure!( *to_cis2_token_balance + token_amount >= *to_cis2_token_balance, CustomContractError::Overflow.into() @@ -421,7 +422,10 @@ pub enum CustomContractError { /// ContractResult type. pub type ContractResult = Result; -/// Implement custom trait `IsMessage`. +/// Trait definition of `IsMessage`. This trait is implemented for the two +/// types `WithdrawMessage` and `InternalTransferMessage`. The `isMessage` trait +/// is used as an input parameter to the `validate_signature_and_increase_nonce` +/// function so that the function works with both message types. trait IsMessage { fn expiry_time(&self) -> Timestamp; fn nonce(&self) -> u64; @@ -438,6 +442,11 @@ fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitRe /// The function is payable and deposits/assigns the send CCD amount /// (native currency) to a public key. /// Logs a `DepositNativeCurrency` event. +/// +/// It rejects if: +/// - it fails to parse the parameter. +/// - an overflow occurs. +/// - it fails to log the event. #[receive( contract = "smart_contract_wallet", name = "depositNativeCurrency", @@ -451,7 +460,7 @@ fn deposit_native_currency( ctx: &ReceiveContext, host: &mut Host, amount: Amount, - logger: &mut impl HasLogger, + logger: &mut Logger, ) -> ReceiveResult<()> { let to: PublicKeyEd25519 = ctx.parameter_cursor().get()?; @@ -474,7 +483,10 @@ fn deposit_native_currency( /// token contract. The function deposits/assigns the sent CIS-2 tokens to a /// public key. Logs a `DepositCis2Tokens` event. /// It rejects if: +/// - it fails to parse the parameter. /// - the sender is not a contract. +/// - an overflow occurs. +/// - it fails to log the event. #[receive( contract = "smart_contract_wallet", name = "depositCis2Tokens", @@ -486,7 +498,7 @@ fn deposit_native_currency( fn deposit_cis2_tokens( ctx: &ReceiveContext, host: &mut Host, - logger: &mut impl HasLogger, + logger: &mut Logger, ) -> ReceiveResult<()> { let cis2_hook_param: OnReceivingCis2DataParams< ContractTokenId, @@ -516,7 +528,7 @@ fn deposit_cis2_tokens( .entry(cis2_hook_param.data) .or_insert_with(|| TokenAmountU256(0.into())); - // A well designes CIS-2 token contract should not overflow. + // A well designed CIS-2 token contract should not overflow. ensure!( *cis2_token_balance + cis2_hook_param.amount >= *cis2_token_balance, CustomContractError::Overflow.into() @@ -656,11 +668,6 @@ fn validate_signature_and_increase_nonce( let message_hash: [u8; 32] = calculate_message_hash_from_bytes(&to_bytes(&message), crypto_primitives, ctx)?; - // Check the signature. - let valid_signature = - crypto_primitives.verify_ed25519_signature(signer, signature, &message_hash); - ensure!(valid_signature, CustomContractError::WrongSignature); - // Get the nonce. let mut entry = host.state_mut().nonces_registry.entry(signer).or_insert_with(|| 0); @@ -670,6 +677,11 @@ fn validate_signature_and_increase_nonce( // Bump the nonce. *entry += 1; + // Check the signature. + let valid_signature = + crypto_primitives.verify_ed25519_signature(signer, signature, &message_hash); + ensure!(valid_signature, CustomContractError::WrongSignature); + Ok(()) } @@ -712,6 +724,8 @@ fn contract_view_withdraw_message_hash( /// - the nonce is wrong. /// - the `signer` has an insufficient balance. /// - the CCD receive hook function reverts for any withdrawal. +/// - an overflow occurs. +/// - it fails to log any of the events. #[receive( contract = "smart_contract_wallet", name = "withdrawNativeCurrency", @@ -724,7 +738,7 @@ fn contract_view_withdraw_message_hash( fn withdraw_native_currency( ctx: &ReceiveContext, host: &mut Host, - logger: &mut impl HasLogger, + logger: &mut Logger, crypto_primitives: &impl HasCryptoPrimitives, ) -> ReceiveResult<()> { // Parse the parameter. @@ -853,6 +867,8 @@ fn withdraw_native_currency( /// - the nonce is wrong. /// - the `signer` has an insufficient balance. /// - the `transfer` function on the CIS-2 contract reverts for any withdrawal. +/// - an overflow occurs. +/// - it fails to log any of the events. #[receive( contract = "smart_contract_wallet", name = "withdrawCis2Tokens", @@ -865,7 +881,7 @@ fn withdraw_native_currency( fn withdraw_cis2_tokens( ctx: &ReceiveContext, host: &mut Host, - logger: &mut impl HasLogger, + logger: &mut Logger, crypto_primitives: &impl HasCryptoPrimitives, ) -> ReceiveResult<()> { // Parse the parameter. @@ -969,10 +985,7 @@ fn withdraw_cis2_tokens( Amount::zero(), )?; - let to_address = match to { - Receiver::Account(account_address) => Address::Account(account_address), - Receiver::Contract(contract_address, _) => Address::Contract(contract_address), - }; + let to_address = to.address(); logger.log(&Event::WithdrawCis2Tokens(WithdrawCis2TokensEvent { token_amount: single_withdraw.token_amount, @@ -1082,6 +1095,8 @@ fn contract_view_internal_transfer_message_hash( /// - the signature is invalid. /// - the nonce is wrong. /// - the `signer` has an insufficient balance. +/// - an overflow occurs. +/// - it fails to log any of the events. #[receive( contract = "smart_contract_wallet", name = "internalTransferNativeCurrency", @@ -1094,7 +1109,7 @@ fn contract_view_internal_transfer_message_hash( fn internal_transfer_native_currency( ctx: &ReceiveContext, host: &mut Host, - logger: &mut impl HasLogger, + logger: &mut Logger, crypto_primitives: &impl HasCryptoPrimitives, ) -> ReceiveResult<()> { // Parse the parameter. @@ -1186,6 +1201,8 @@ fn internal_transfer_native_currency( /// - the signature is invalid. /// - the nonce is wrong. /// - the `signer` has an insufficient balance. +/// - an overflow occurs. +/// - it fails to log any of the events. #[receive( contract = "smart_contract_wallet", name = "internalTransferCis2Tokens", @@ -1198,7 +1215,7 @@ fn internal_transfer_native_currency( fn internal_transfer_cis2_tokens( ctx: &ReceiveContext, host: &mut Host, - logger: &mut impl HasLogger, + logger: &mut Logger, crypto_primitives: &impl HasCryptoPrimitives, ) -> ReceiveResult<()> { // Parse the parameter. diff --git a/examples/cis5-smart-contract-wallet/tests/tests.rs b/examples/cis5-smart-contract-wallet/tests/tests.rs index a3708cd25..9068be4d5 100644 --- a/examples/cis5-smart-contract-wallet/tests/tests.rs +++ b/examples/cis5-smart-contract-wallet/tests/tests.rs @@ -802,7 +802,7 @@ fn alice_deposits_ccd( })]); } -// /// Deserialize the events from an update. +/// Deserialize the events from an update. fn deserialize_update_events_of_specified_contract( update: &ContractInvokeSuccess, smart_contract_wallet: ContractAddress, From 51955a699c76d2ac3ee05cdc40b7fa7a415be543 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Wed, 10 Apr 2024 19:38:09 +0300 Subject: [PATCH 25/28] Change simplified token state --- .../cis5-smart-contract-wallet/src/lib.rs | 109 +++++------------- 1 file changed, 32 insertions(+), 77 deletions(-) diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index eca2ddb67..864425090 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -33,9 +33,12 @@ //! //! The goal of this standard is to simplify the account creation onboarding //! flow on Concordium. Users can hold/transfer native currency/CIS-2 tokens -//! without a valid account as a starting point (no KYC required). Once users -//! are ready to go through the KYC process to create a valid account on +//! without a native account as a starting point (no KYC required). Once users +//! are ready to go through the KYC process to create a native account on //! Concordium, they can withdraw assets out of the smart contract wallet. +//! The public key accounts in this smart contract wallet can't submit the +//! transactions on chain themselves, but rely on someone with a native account +//! (third-party) to do so. use concordium_cis2::*; use concordium_std::*; @@ -190,31 +193,13 @@ pub struct InternalCis2TokensTransferEvent { pub to: PublicKeyEd25519, } -/// The token balances stored in the state for each token id. -#[derive(Serial, DeserialWithState, Deletable)] -#[concordium(state_parameter = "S")] -#[concordium(transparent)] -struct ContractAddressState { - /// The amount of tokens owned by public keys mapped for each token id. - balances: StateMap, S>, -} - -// Implementation of the `ContractAddressState`. -impl ContractAddressState { - /// Creates a new `ContractAddressState` with empty balances. - fn empty(state_builder: &mut StateBuilder) -> Self { - ContractAddressState { - balances: state_builder.new_map(), - } - } -} - /// The contract state. #[derive(Serial, DeserialWithState)] #[concordium(state_parameter = "S")] struct State { /// The token balances stored in the state. - token_balances: StateMap, S>, + token_balances: + StateMap<(ContractAddress, ContractTokenId, PublicKeyEd25519), ContractTokenAmount, S>, /// The CCD balances stored in the state. native_balances: StateMap, /// A map with contract addresses providing implementations of additional @@ -231,7 +216,7 @@ struct State { // Functions for creating, updating and querying the contract state. impl State { - /// Creates a new state with emtpy balances. + /// Creates a new state with empty balances. fn empty(state_builder: &mut StateBuilder) -> Self { State { native_balances: state_builder.new_map(), @@ -252,21 +237,13 @@ impl State { /// contract, token id or public key does not exist in the state. fn balance_tokens( &self, - token_id: &ContractTokenId, - cis2_token_contract_address: &ContractAddress, - public_key: &PublicKeyEd25519, + token_id: ContractTokenId, + cis2_token_contract_address: ContractAddress, + public_key: PublicKeyEd25519, ) -> ContractTokenAmount { self.token_balances - .get(cis2_token_contract_address) - .map(|a| { - a.balances - .get(token_id) - .map(|b| { - b.get(public_key).map(|c| *c).unwrap_or_else(|| TokenAmountU256(0.into())) - }) - .unwrap_or_else(|| TokenAmountU256(0.into())) - }) - .unwrap_or_else(|| TokenAmountU256(0.into())) + .get(&(cis2_token_contract_address, token_id, public_key)) + .map_or_else(|| TokenAmountU256(0.into()), |x| *x) } /// Updates the state with a transfer of CCD amount and logs an @@ -326,19 +303,10 @@ impl State { ) -> ReceiveResult<()> { // A zero transfer does not modify the state. if token_amount != TokenAmountU256(0.into()) { - let mut contract_balances = self - .token_balances - .entry(cis2_token_contract_address) - .occupied_or(CustomContractError::InsufficientFunds)?; - - let mut token_balances = contract_balances - .balances - .entry(token_id.clone()) - .occupied_or(CustomContractError::InsufficientFunds)?; - { - let mut from_cis2_token_balance = token_balances - .entry(from_public_key) + let mut from_cis2_token_balance = self + .token_balances + .entry((cis2_token_contract_address, token_id.clone(), from_public_key)) .occupied_or(CustomContractError::InsufficientFunds)?; ensure!( @@ -348,8 +316,10 @@ impl State { *from_cis2_token_balance -= token_amount; } - let mut to_cis2_token_balance = - token_balances.entry(to_public_key).or_insert_with(|| TokenAmountU256(0.into())); + let mut to_cis2_token_balance = self + .token_balances + .entry((cis2_token_contract_address, token_id.clone(), to_public_key)) + .or_insert_with(|| TokenAmountU256(0.into())); // A well designed CIS-2 token contract should not overflow. ensure!( @@ -512,20 +482,10 @@ fn deposit_cis2_tokens( Address::Account(_) => bail!(CustomContractError::OnlyContract.into()), }; - let (state, builder) = host.state_and_builder(); - - let mut contract_balances = state + let mut cis2_token_balance = host + .state_mut() .token_balances - .entry(sender_contract_address) - .or_insert_with(|| ContractAddressState::empty(builder)); - - let mut contract_token_balances = contract_balances - .balances - .entry(cis2_hook_param.token_id.clone()) - .or_insert_with(|| builder.new_map()); - - let mut cis2_token_balance = contract_token_balances - .entry(cis2_hook_param.data) + .entry((sender_contract_address, cis2_hook_param.token_id.clone(), cis2_hook_param.data)) .or_insert_with(|| TokenAmountU256(0.into())); // A well designed CIS-2 token contract should not overflow. @@ -946,19 +906,14 @@ fn withdraw_cis2_tokens( // Update the contract state { - let mut contract_balances = host + let mut from_cis2_token_balance = host .state_mut() .token_balances - .entry(single_withdraw.cis2_token_contract_address) - .occupied_or(CustomContractError::InsufficientFunds)?; - - let mut token_balances = contract_balances - .balances - .entry(single_withdraw.token_id.clone()) - .occupied_or(CustomContractError::InsufficientFunds)?; - - let mut from_cis2_token_balance = token_balances - .entry(signer) + .entry(( + single_withdraw.cis2_token_contract_address, + single_withdraw.token_id.clone(), + signer, + )) .occupied_or(CustomContractError::InsufficientFunds)?; ensure!( @@ -1471,9 +1426,9 @@ fn contract_balance_of_cis2_tokens( for query in params.queries { // Query the state for balance. let amount = host.state().balance_tokens( - &query.token_id, - &query.cis2_token_contract_address, - &query.public_key, + query.token_id, + query.cis2_token_contract_address, + query.public_key, ); response.push(amount); } From e928c40904d6da9c5efb64577f6d1a9fe4ac9e76 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 11 Apr 2024 11:11:56 +0300 Subject: [PATCH 26/28] Change to use generics for signingAmount --- .../cis5-smart-contract-wallet/src/lib.rs | 266 +++++++++--------- .../cis5-smart-contract-wallet/tests/tests.rs | 64 ++--- 2 files changed, 161 insertions(+), 169 deletions(-) diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index 864425090..c5d9b465a 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -383,16 +383,14 @@ pub enum CustomContractError { WrongEntryPoint, // -9 /// Failed because the sender is unauthorized to invoke the entry point. UnAuthorized, // -10 - /// Failed because the signed amount type is wrong. - WrongAmountType, // -11 /// Failed because of an overflow in the token/CCD amount. - Overflow, // -12 + Overflow, // -11 } /// ContractResult type. pub type ContractResult = Result; -/// Trait definition of `IsMessage`. This trait is implemented for the two +/// Trait definition of the `IsMessage`. This trait is implemented for the two /// types `WithdrawMessage` and `InternalTransferMessage`. The `isMessage` trait /// is used as an input parameter to the `validate_signature_and_increase_nonce` /// function so that the function works with both message types. @@ -411,7 +409,8 @@ fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitRe /// The function is payable and deposits/assigns the send CCD amount /// (native currency) to a public key. -/// Logs a `DepositNativeCurrency` event. +/// +/// The function logs a `DepositNativeCurrency` event. /// /// It rejects if: /// - it fails to parse the parameter. @@ -451,7 +450,10 @@ fn deposit_native_currency( /// The function should be called through the receive hook mechanism of a CIS-2 /// token contract. The function deposits/assigns the sent CIS-2 tokens to a -/// public key. Logs a `DepositCis2Tokens` event. +/// public key. +/// +/// The function logs a `DepositCis2Tokens` event. +/// /// It rejects if: /// - it fails to parse the parameter. /// - the sender is not a contract. @@ -507,14 +509,18 @@ fn deposit_cis2_tokens( Ok(()) } -/// The native currency or CIS-2 token amount signed in the message. -#[derive(Serialize, Clone, SchemaType)] -pub enum SigningAmount { - /// The CCD amount signed in the message. - CCDAmount(Amount), - /// The token amount signed in the message. - TokenAmount(TokenAmount), -} +/// Trait definition of the `SigningAmount`. This trait is implemented for the +/// two types `Amount` and `TokenAmount`. The `SigningAmount` trait +/// is used as a generic parameter in the `WithdrawParameter` and +/// `InternalTransferParameter` types so that we can use the same parameters in +/// the `withdraw/internalTransfer` functions (no matter if the function +/// tranfsers CCD or CIS-2 tokens). +pub trait SigningAmount: Deserial + Serial {} + +/// `SigningAmount` trait definition for `Amount`. +impl SigningAmount for Amount {} +/// `SigningAmount` trait definition for `TokenAmount`. +impl SigningAmount for TokenAmount {} /// The token amount signed in the message. #[derive(Serialize, Clone, SchemaType)] @@ -529,18 +535,18 @@ pub struct TokenAmount { /// A single withdrawal of native currency or some amount of tokens. #[derive(Serialize, Clone, SchemaType)] -pub struct Withdraw { +pub struct Withdraw { /// The address receiving the native currency or tokens being withdrawn. pub to: Receiver, /// The amount being withdrawn. - pub withdraw_amount: SigningAmount, + pub withdraw_amount: T, /// Some additional data for the receive hook function. pub data: AdditionalData, } /// The withdraw message that is signed by the signer. #[derive(Serialize, Clone, SchemaType)] -pub struct WithdrawMessage { +pub struct WithdrawMessage { /// The entry_point that the signature is intended for. pub entry_point: OwnedEntrypointName, /// A timestamp to make the signatures expire. @@ -550,13 +556,13 @@ pub struct WithdrawMessage { /// The recipient public key of the service fee. pub service_fee_recipient: PublicKeyEd25519, /// The amount of native currency or tokens to be received as a service fee. - pub service_fee_amount: SigningAmount, + pub service_fee_amount: T, /// List of withdrawals. #[concordium(size_length = 2)] - pub simple_withdraws: Vec, + pub simple_withdraws: Vec>, } -impl IsMessage for WithdrawMessage { +impl IsMessage for WithdrawMessage { fn expiry_time(&self) -> Timestamp { self.expiry_time } fn nonce(&self) -> u64 { self.nonce } @@ -564,23 +570,23 @@ impl IsMessage for WithdrawMessage { /// A batch of withdrawals signed by a signer. #[derive(Serialize, SchemaType)] -pub struct WithdrawBatch { +pub struct WithdrawBatch { /// The signer public key. pub signer: PublicKeyEd25519, /// The signature. pub signature: SignatureEd25519, /// The message being signed. - pub message: WithdrawMessage, + pub message: WithdrawMessage, } /// The parameter type for the contract functions /// `withdrawNativeCurrency/withdrawCis2Tokens`. #[derive(Serialize, SchemaType)] #[concordium(transparent)] -pub struct WithdrawParameter { +pub struct WithdrawParameter { /// List of withdraw batches. #[concordium(size_length = 2)] - pub withdraws: Vec, + pub withdraws: Vec>, } /// The `TransferParameter` type for the `transfer` function in the CIS-2 token @@ -645,23 +651,44 @@ fn validate_signature_and_increase_nonce( Ok(()) } -/// Helper function to calculate the `WithdrawMessageHash`. +/// Helper function to calculate the `WithdrawMessageHash` for a CCD amount. #[receive( contract = "smart_contract_wallet", - name = "viewWithdrawMessageHash", - parameter = "WithdrawMessage", + name = "viewWithdrawMessageHashCcdAmount", + parameter = "WithdrawMessage", return_value = "[u8;32]", error = "CustomContractError", crypto_primitives, mutable )] -fn contract_view_withdraw_message_hash( +fn contract_view_withdraw_message_hash_ccd_amount( ctx: &ReceiveContext, _host: &mut Host, crypto_primitives: &impl HasCryptoPrimitives, ) -> ContractResult<[u8; 32]> { // Parse the parameter. - let param: WithdrawMessage = ctx.parameter_cursor().get()?; + let param: WithdrawMessage = ctx.parameter_cursor().get()?; + + calculate_message_hash_from_bytes(&to_bytes(¶m), crypto_primitives, ctx) +} + +/// Helper function to calculate the `WithdrawMessageHash` for a token amount. +#[receive( + contract = "smart_contract_wallet", + name = "viewWithdrawMessageHashTokenAmount", + parameter = "WithdrawMessage", + return_value = "[u8;32]", + error = "CustomContractError", + crypto_primitives, + mutable +)] +fn contract_view_withdraw_message_hash_token_amount( + ctx: &ReceiveContext, + _host: &mut Host, + crypto_primitives: &impl HasCryptoPrimitives, +) -> ContractResult<[u8; 32]> { + // Parse the parameter. + let param: WithdrawMessage = ctx.parameter_cursor().get()?; calculate_message_hash_from_bytes(&to_bytes(¶m), crypto_primitives, ctx) } @@ -689,7 +716,7 @@ fn contract_view_withdraw_message_hash( #[receive( contract = "smart_contract_wallet", name = "withdrawNativeCurrency", - parameter = "WithdrawParameter", + parameter = "WithdrawParameter", error = "CustomContractError", crypto_primitives, enable_logger, @@ -702,7 +729,7 @@ fn withdraw_native_currency( crypto_primitives: &impl HasCryptoPrimitives, ) -> ReceiveResult<()> { // Parse the parameter. - let param: WithdrawParameter = ctx.parameter_cursor().get()?; + let param: WithdrawParameter = ctx.parameter_cursor().get()?; for withdraw_batch in param.withdraws { let WithdrawBatch { @@ -726,13 +753,6 @@ fn withdraw_native_currency( CustomContractError::WrongEntryPoint.into() ); - let service_fee_ccd_amount = match service_fee_amount { - SigningAmount::CCDAmount(ccd_amount) => ccd_amount, - SigningAmount::TokenAmount(_) => { - bail!(CustomContractError::WrongAmountType.into()) - } - }; - validate_signature_and_increase_nonce( message, signer, @@ -746,7 +766,7 @@ fn withdraw_native_currency( host.state_mut().transfer_native_currency( signer, service_fee_recipient, - service_fee_ccd_amount, + service_fee_amount, logger, )?; @@ -756,13 +776,6 @@ fn withdraw_native_currency( data, } in simple_withdraws { - let ccd_amount = match withdraw_amount { - SigningAmount::CCDAmount(ccd_amount) => ccd_amount, - SigningAmount::TokenAmount(_) => { - bail!(CustomContractError::WrongAmountType.into()) - } - }; - // Update the contract state { let mut from_public_key_native_balance = host @@ -772,14 +785,14 @@ fn withdraw_native_currency( .occupied_or(CustomContractError::InsufficientFunds)?; *from_public_key_native_balance = (*from_public_key_native_balance) - .checked_sub(ccd_amount) + .checked_sub(withdraw_amount) .ok_or(CustomContractError::InsufficientFunds)?; } // Withdraw CCD out of the contract. let to_address = match to { Receiver::Account(account_address) => { - host.invoke_transfer(&account_address, ccd_amount)?; + host.invoke_transfer(&account_address, withdraw_amount)?; Address::Account(account_address) } Receiver::Contract(contract_address, function) => { @@ -788,16 +801,16 @@ fn withdraw_native_currency( &contract_address, &data, function.as_entrypoint_name(), - ccd_amount, + withdraw_amount, )?; Address::Contract(contract_address) } }; logger.log(&Event::WithdrawNativeCurrency(WithdrawNativeCurrencyEvent { - ccd_amount, - from: signer, - to: to_address, + ccd_amount: withdraw_amount, + from: signer, + to: to_address, }))?; } @@ -832,7 +845,7 @@ fn withdraw_native_currency( #[receive( contract = "smart_contract_wallet", name = "withdrawCis2Tokens", - parameter = "WithdrawParameter", + parameter = "WithdrawParameter", error = "CustomContractError", crypto_primitives, enable_logger, @@ -845,7 +858,7 @@ fn withdraw_cis2_tokens( crypto_primitives: &impl HasCryptoPrimitives, ) -> ReceiveResult<()> { // Parse the parameter. - let param: WithdrawParameter = ctx.parameter_cursor().get()?; + let param: WithdrawParameter = ctx.parameter_cursor().get()?; for withdraw_batch in param.withdraws { let WithdrawBatch { @@ -865,13 +878,6 @@ fn withdraw_cis2_tokens( ensure_eq!(entry_point, "withdrawCis2Tokens", CustomContractError::WrongEntryPoint.into()); - let service_fee = match service_fee_amount { - SigningAmount::CCDAmount(_) => { - bail!(CustomContractError::WrongAmountType.into()) - } - SigningAmount::TokenAmount(ref token_amount) => token_amount, - }; - validate_signature_and_increase_nonce( message, signer, @@ -885,9 +891,9 @@ fn withdraw_cis2_tokens( host.state_mut().transfer_cis2_tokens( signer, service_fee_recipient, - service_fee.cis2_token_contract_address, - service_fee.token_id.clone(), - service_fee.token_amount, + service_fee_amount.cis2_token_contract_address, + service_fee_amount.token_id.clone(), + service_fee_amount.token_amount, logger, )?; @@ -897,36 +903,29 @@ fn withdraw_cis2_tokens( data, } in simple_withdraws { - let single_withdraw = match withdraw_amount { - SigningAmount::CCDAmount(_) => { - bail!(CustomContractError::WrongAmountType.into()) - } - SigningAmount::TokenAmount(ref single_withdraw) => single_withdraw, - }; - // Update the contract state { let mut from_cis2_token_balance = host .state_mut() .token_balances .entry(( - single_withdraw.cis2_token_contract_address, - single_withdraw.token_id.clone(), + withdraw_amount.cis2_token_contract_address, + withdraw_amount.token_id.clone(), signer, )) .occupied_or(CustomContractError::InsufficientFunds)?; ensure!( - *from_cis2_token_balance >= single_withdraw.token_amount, + *from_cis2_token_balance >= withdraw_amount.token_amount, CustomContractError::InsufficientFunds.into() ); - *from_cis2_token_balance -= single_withdraw.token_amount; + *from_cis2_token_balance -= withdraw_amount.token_amount; } // Create Transfer parameter. let data: TransferParameter = TransferParams(vec![Transfer { - token_id: single_withdraw.token_id.clone(), - amount: single_withdraw.token_amount, + token_id: withdraw_amount.token_id.clone(), + amount: withdraw_amount.token_amount, from: Address::Contract(ctx.self_address()), to: to.clone(), data, @@ -934,7 +933,7 @@ fn withdraw_cis2_tokens( // Invoke the `transfer` function on the CIS-2 token contract. host.invoke_contract( - &single_withdraw.cis2_token_contract_address, + &withdraw_amount.cis2_token_contract_address, &data, EntrypointName::new_unchecked("transfer"), Amount::zero(), @@ -943,9 +942,9 @@ fn withdraw_cis2_tokens( let to_address = to.address(); logger.log(&Event::WithdrawCis2Tokens(WithdrawCis2TokensEvent { - token_amount: single_withdraw.token_amount, - token_id: single_withdraw.token_id.clone(), - cis2_token_contract_address: single_withdraw.cis2_token_contract_address, + token_amount: withdraw_amount.token_amount, + token_id: withdraw_amount.token_id.clone(), + cis2_token_contract_address: withdraw_amount.cis2_token_contract_address, from: signer, to: to_address, }))?; @@ -962,16 +961,16 @@ fn withdraw_cis2_tokens( /// A single transfer of native currency or some amount of tokens. #[derive(Serialize, Clone, SchemaType)] -pub struct InternalTransfer { +pub struct InternalTransfer { /// The public key receiving the tokens being transferred. pub to: PublicKeyEd25519, /// The amount of tokens being transferred. - pub transfer_amount: SigningAmount, + pub transfer_amount: T, } /// The transfer message that is signed by the signer. #[derive(Serialize, Clone, SchemaType)] -pub struct InternalTransferMessage { +pub struct InternalTransferMessage { /// The entry_point that the signature is intended for. pub entry_point: OwnedEntrypointName, /// A timestamp to make the signatures expire. @@ -981,13 +980,13 @@ pub struct InternalTransferMessage { /// The recipient public key of the service fee. pub service_fee_recipient: PublicKeyEd25519, /// The amount of native currency or tokens to be received as a service fee. - pub service_fee_amount: SigningAmount, + pub service_fee_amount: T, /// List of transfers. #[concordium(size_length = 2)] - pub simple_transfers: Vec, + pub simple_transfers: Vec>, } -impl IsMessage for InternalTransferMessage { +impl IsMessage for InternalTransferMessage { fn expiry_time(&self) -> Timestamp { self.expiry_time } fn nonce(&self) -> u64 { self.nonce } @@ -995,42 +994,65 @@ impl IsMessage for InternalTransferMessage { /// A batch of transfers signed by a signer. #[derive(Serialize, SchemaType)] -pub struct InternalTransferBatch { +pub struct InternalTransferBatch { /// The signer public key. pub signer: PublicKeyEd25519, /// The signature. pub signature: SignatureEd25519, /// The message being signed. - pub message: InternalTransferMessage, + pub message: InternalTransferMessage, } /// The parameter type for the contract functions /// `internalTransferNativeCurrency/internalTransferCis2Tokens`. #[derive(Serialize, SchemaType)] #[concordium(transparent)] -pub struct InternalTransferParameter { +pub struct InternalTransferParameter { /// List of transfer batches. #[concordium(size_length = 2)] - pub transfers: Vec, + pub transfers: Vec>, +} + +/// Helper function to calculate the `InternalTransferMessageHash` for a CCD +/// amount. +#[receive( + contract = "smart_contract_wallet", + name = "viewInternalTransferMessageHashCcdAmount", + parameter = "InternalTransferMessage", + return_value = "[u8;32]", + error = "CustomContractError", + crypto_primitives, + mutable +)] +fn contract_view_internal_transfer_message_hash_ccd_amount( + ctx: &ReceiveContext, + _host: &mut Host, + crypto_primitives: &impl HasCryptoPrimitives, +) -> ContractResult<[u8; 32]> { + // Parse the parameter. + let param: InternalTransferMessage = ctx.parameter_cursor().get()?; + + calculate_message_hash_from_bytes(&to_bytes(¶m), crypto_primitives, ctx) } -/// Helper function to calculate the `InternalTransferMessageHash`. +/// Helper function to calculate the `InternalTransferMessageHash` for a token +/// amount. #[receive( contract = "smart_contract_wallet", - name = "viewInternalTransferMessageHash", - parameter = "InternalTransferMessage", + name = "viewInternalTransferMessageHashTokenAmount", + parameter = "InternalTransferMessage", return_value = "[u8;32]", error = "CustomContractError", crypto_primitives, mutable )] -fn contract_view_internal_transfer_message_hash( +fn contract_view_internal_transfer_message_hash_token_amount( ctx: &ReceiveContext, _host: &mut Host, crypto_primitives: &impl HasCryptoPrimitives, ) -> ContractResult<[u8; 32]> { // Parse the parameter. - let param: InternalTransferMessage = ctx.parameter_cursor().get()?; + let param: InternalTransferMessage = ctx.parameter_cursor().get()?; calculate_message_hash_from_bytes(&to_bytes(¶m), crypto_primitives, ctx) } @@ -1055,7 +1077,7 @@ fn contract_view_internal_transfer_message_hash( #[receive( contract = "smart_contract_wallet", name = "internalTransferNativeCurrency", - parameter = "InternalTransferParameter", + parameter = "InternalTransferParameter", error = "CustomContractError", crypto_primitives, enable_logger, @@ -1068,7 +1090,7 @@ fn internal_transfer_native_currency( crypto_primitives: &impl HasCryptoPrimitives, ) -> ReceiveResult<()> { // Parse the parameter. - let param: InternalTransferParameter = ctx.parameter_cursor().get()?; + let param: InternalTransferParameter = ctx.parameter_cursor().get()?; for transfer_batch in param.transfers { let InternalTransferBatch { @@ -1092,13 +1114,6 @@ fn internal_transfer_native_currency( CustomContractError::WrongEntryPoint.into() ); - let service_fee_ccd_amount = match service_fee_amount { - SigningAmount::CCDAmount(ccd_amount) => ccd_amount, - SigningAmount::TokenAmount(_) => { - bail!(CustomContractError::WrongAmountType.into()) - } - }; - validate_signature_and_increase_nonce( message, signer, @@ -1112,7 +1127,7 @@ fn internal_transfer_native_currency( host.state_mut().transfer_native_currency( signer, service_fee_recipient, - service_fee_ccd_amount, + service_fee_amount, logger, )?; @@ -1121,15 +1136,8 @@ fn internal_transfer_native_currency( transfer_amount, } in simple_transfers { - let ccd_amount = match transfer_amount { - SigningAmount::CCDAmount(ccd_amount) => ccd_amount, - SigningAmount::TokenAmount(_) => { - bail!(CustomContractError::WrongAmountType.into()) - } - }; - // Update the contract state - host.state_mut().transfer_native_currency(signer, to, ccd_amount, logger)?; + host.state_mut().transfer_native_currency(signer, to, transfer_amount, logger)?; } logger.log(&Event::Nonce(NonceEvent { @@ -1161,7 +1169,7 @@ fn internal_transfer_native_currency( #[receive( contract = "smart_contract_wallet", name = "internalTransferCis2Tokens", - parameter = "InternalTransferParameter", + parameter = "InternalTransferParameter", error = "CustomContractError", crypto_primitives, enable_logger, @@ -1174,7 +1182,7 @@ fn internal_transfer_cis2_tokens( crypto_primitives: &impl HasCryptoPrimitives, ) -> ReceiveResult<()> { // Parse the parameter. - let param: InternalTransferParameter = ctx.parameter_cursor().get()?; + let param: InternalTransferParameter = ctx.parameter_cursor().get()?; for transfer_batch in param.transfers { let InternalTransferBatch { @@ -1198,13 +1206,6 @@ fn internal_transfer_cis2_tokens( CustomContractError::WrongEntryPoint.into() ); - let service_fee = match service_fee_amount { - SigningAmount::CCDAmount(_) => { - bail!(CustomContractError::WrongAmountType.into()) - } - SigningAmount::TokenAmount(token_amount) => token_amount, - }; - validate_signature_and_increase_nonce( message, signer, @@ -1218,9 +1219,9 @@ fn internal_transfer_cis2_tokens( host.state_mut().transfer_cis2_tokens( signer, service_fee_recipient, - service_fee.cis2_token_contract_address, - service_fee.token_id, - service_fee.token_amount, + service_fee_amount.cis2_token_contract_address, + service_fee_amount.token_id, + service_fee_amount.token_amount, logger, )?; @@ -1229,20 +1230,13 @@ fn internal_transfer_cis2_tokens( transfer_amount, } in simple_transfers { - let single_transfer = match transfer_amount { - SigningAmount::CCDAmount(_) => { - bail!(CustomContractError::WrongAmountType.into()) - } - SigningAmount::TokenAmount(single_transfer) => single_transfer, - }; - // Update the contract state host.state_mut().transfer_cis2_tokens( signer, to, - single_transfer.cis2_token_contract_address, - single_transfer.token_id, - single_transfer.token_amount, + transfer_amount.cis2_token_contract_address, + transfer_amount.token_id, + transfer_amount.token_amount, logger, )?; } diff --git a/examples/cis5-smart-contract-wallet/tests/tests.rs b/examples/cis5-smart-contract-wallet/tests/tests.rs index 9068be4d5..b9c087794 100644 --- a/examples/cis5-smart-contract-wallet/tests/tests.rs +++ b/examples/cis5-smart-contract-wallet/tests/tests.rs @@ -77,18 +77,16 @@ fn test_withdraw_ccd() { let withdraw_amount: Amount = Amount::from_micro_ccd(5); let message = WithdrawMessage { - entry_point: OwnedEntrypointName::new_unchecked( - "withdrawNativeCurrency".to_string(), - ), - expiry_time: Timestamp::now(), - nonce: 0u64, + entry_point: OwnedEntrypointName::new_unchecked("withdrawNativeCurrency".to_string()), + expiry_time: Timestamp::now(), + nonce: 0u64, service_fee_recipient: SERVICE_FEE_RECIPIENT_KEY, - simple_withdraws: vec![Withdraw { - to: Receiver::Account(BOB), - withdraw_amount: SigningAmount::CCDAmount(withdraw_amount), - data: AdditionalData::empty(), + simple_withdraws: vec![Withdraw { + to: Receiver::Account(BOB), + withdraw_amount, + data: AdditionalData::empty(), }], - service_fee_amount: SigningAmount::CCDAmount(service_fee_amount), + service_fee_amount, }; let mut withdraw = WithdrawBatch { @@ -103,12 +101,12 @@ fn test_withdraw_ccd() { amount: Amount::zero(), address: smart_contract_wallet, receive_name: OwnedReceiveName::new_unchecked( - "smart_contract_wallet.viewWithdrawMessageHash".to_string(), + "smart_contract_wallet.viewWithdrawMessageHashCcdAmount".to_string(), ), message: OwnedParameter::from_serial(&message) .expect("Should be a valid inut parameter"), }) - .expect("Should be able to query viewWithdrawMessageHash"); + .expect("Should be able to query viewWithdrawMessageHashCccdAmount"); let signature = signing_key.sign(&invoke.return_value); @@ -211,18 +209,18 @@ fn test_withdraw_cis2_tokens() { service_fee_recipient: SERVICE_FEE_RECIPIENT_KEY, simple_withdraws: vec![Withdraw { to: Receiver::Account(BOB), - withdraw_amount: SigningAmount::TokenAmount(TokenAmount { + withdraw_amount: TokenAmount { token_amount: withdraw_amount, token_id: contract_token_id.clone(), cis2_token_contract_address, - }), + }, data: AdditionalData::empty(), }], - service_fee_amount: SigningAmount::TokenAmount(TokenAmount { + service_fee_amount: TokenAmount { token_amount: service_fee_amount, token_id: contract_token_id.clone(), cis2_token_contract_address, - }), + }, }; let mut withdraw = WithdrawBatch { @@ -237,12 +235,12 @@ fn test_withdraw_cis2_tokens() { amount: Amount::zero(), address: smart_contract_wallet, receive_name: OwnedReceiveName::new_unchecked( - "smart_contract_wallet.viewWithdrawMessageHash".to_string(), + "smart_contract_wallet.viewWithdrawMessageHashTokenAmount".to_string(), ), message: OwnedParameter::from_serial(&message) .expect("Should be a valid inut parameter"), }) - .expect("Should be able to query viewWithdrawMessageHash"); + .expect("Should be able to query viewWithdrawMessageHashTokenAmount"); let signature = signing_key.sign(&invoke.return_value); @@ -339,17 +337,17 @@ fn test_internal_transfer_ccd() { let transfer_amount: Amount = Amount::from_micro_ccd(5); let message = InternalTransferMessage { - entry_point: OwnedEntrypointName::new_unchecked( + entry_point: OwnedEntrypointName::new_unchecked( "internalTransferNativeCurrency".to_string(), ), - expiry_time: Timestamp::now(), - nonce: 0u64, + expiry_time: Timestamp::now(), + nonce: 0u64, service_fee_recipient: SERVICE_FEE_RECIPIENT_KEY, - simple_transfers: vec![InternalTransfer { - to: BOB_PUBLIC_KEY, - transfer_amount: SigningAmount::CCDAmount(transfer_amount), + simple_transfers: vec![InternalTransfer { + to: BOB_PUBLIC_KEY, + transfer_amount, }], - service_fee_amount: SigningAmount::CCDAmount(service_fee_amount), + service_fee_amount, }; let mut internal_transfer = InternalTransferBatch { @@ -364,12 +362,12 @@ fn test_internal_transfer_ccd() { amount: Amount::zero(), address: smart_contract_wallet, receive_name: OwnedReceiveName::new_unchecked( - "smart_contract_wallet.viewInternalTransferMessageHash".to_string(), + "smart_contract_wallet.viewInternalTransferMessageHashCcdAmount".to_string(), ), message: OwnedParameter::from_serial(&message) .expect("Should be a valid inut parameter"), }) - .expect("Should be able to query viewInternalTransferMessageHash"); + .expect("Should be able to query viewInternalTransferMessageHashCcdAmount"); let signature = signing_key.sign(&invoke.return_value); @@ -464,18 +462,18 @@ fn test_internal_transfer_cis2_tokens() { expiry_time: Timestamp::now(), nonce: 0u64, service_fee_recipient: SERVICE_FEE_RECIPIENT_KEY, - service_fee_amount: SigningAmount::TokenAmount(TokenAmount { + service_fee_amount: TokenAmount { token_amount: service_fee_amount, token_id: contract_token_id.clone(), cis2_token_contract_address, - }), + }, simple_transfers: vec![InternalTransfer { to: BOB_PUBLIC_KEY, - transfer_amount: SigningAmount::TokenAmount(TokenAmount { + transfer_amount: TokenAmount { token_amount: transfer_amount, token_id: contract_token_id.clone(), cis2_token_contract_address, - }), + }, }], }; @@ -491,12 +489,12 @@ fn test_internal_transfer_cis2_tokens() { amount: Amount::zero(), address: smart_contract_wallet, receive_name: OwnedReceiveName::new_unchecked( - "smart_contract_wallet.viewInternalTransferMessageHash".to_string(), + "smart_contract_wallet.viewInternalTransferMessageHashTokenAmount".to_string(), ), message: OwnedParameter::from_serial(&message) .expect("Should be a valid inut parameter"), }) - .expect("Should be able to query viewInternalTransferMessageHash"); + .expect("Should be able to query viewInternalTransferMessageHashTokenAmount"); let signature = signing_key.sign(&invoke.return_value); From 8e6af6b90dcb612e5c99ad713bf482d8e6983ecc Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 11 Apr 2024 11:48:14 +0300 Subject: [PATCH 27/28] Change from address in receive hook --- examples/cis2-multi/src/lib.rs | 2 +- examples/cis5-smart-contract-wallet/tests/tests.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs index 2c0fd4917..4a441d636 100644 --- a/examples/cis2-multi/src/lib.rs +++ b/examples/cis2-multi/src/lib.rs @@ -1041,7 +1041,7 @@ fn contract_mint( let parameter = OnReceivingCis2Params { token_id: params.token_id, amount: host.state.mint_airdrop, - from: Address::Contract(ctx.self_address()), + from: Address::from(address), data: params.data, }; host.invoke_contract(&address, ¶meter, function.as_entrypoint_name(), Amount::zero())?; diff --git a/examples/cis5-smart-contract-wallet/tests/tests.rs b/examples/cis5-smart-contract-wallet/tests/tests.rs index b9c087794..55711283f 100644 --- a/examples/cis5-smart-contract-wallet/tests/tests.rs +++ b/examples/cis5-smart-contract-wallet/tests/tests.rs @@ -749,7 +749,7 @@ fn alice_deposits_cis2_tokens( token_amount: TokenAmountU256(AIRDROP_TOKEN_AMOUNT.0.into()), token_id: TokenIdVec(vec![TOKEN_ID.0]), cis2_token_contract_address, - from: Address::Contract(cis2_token_contract_address), + from: Address::Contract(smart_contract_wallet), to: alice_public_key })]); } From 4a67ac94c88078b020e2a716ecb9d0bf2e22a9b1 Mon Sep 17 00:00:00 2001 From: Doris Benda Date: Thu, 11 Apr 2024 14:22:52 +0300 Subject: [PATCH 28/28] Add transparent attribute --- concordium-rust-sdk | 2 +- examples/cis5-smart-contract-wallet/src/lib.rs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/concordium-rust-sdk b/concordium-rust-sdk index 6eb5f3b76..5e4b93c0a 160000 --- a/concordium-rust-sdk +++ b/concordium-rust-sdk @@ -1 +1 @@ -Subproject commit 6eb5f3b768256b1e77d6d4340e121ead4a40aa20 +Subproject commit 5e4b93c0a0ae7aef16d3bb6c0e62c30424d900bc diff --git a/examples/cis5-smart-contract-wallet/src/lib.rs b/examples/cis5-smart-contract-wallet/src/lib.rs index c5d9b465a..18c1f26a1 100644 --- a/examples/cis5-smart-contract-wallet/src/lib.rs +++ b/examples/cis5-smart-contract-wallet/src/lib.rs @@ -583,6 +583,7 @@ pub struct WithdrawBatch { /// `withdrawNativeCurrency/withdrawCis2Tokens`. #[derive(Serialize, SchemaType)] #[concordium(transparent)] +#[repr(transparent)] pub struct WithdrawParameter { /// List of withdraw batches. #[concordium(size_length = 2)] @@ -1007,6 +1008,7 @@ pub struct InternalTransferBatch { /// `internalTransferNativeCurrency/internalTransferCis2Tokens`. #[derive(Serialize, SchemaType)] #[concordium(transparent)] +#[repr(transparent)] pub struct InternalTransferParameter { /// List of transfer batches. #[concordium(size_length = 2)] @@ -1320,6 +1322,7 @@ fn contract_supports( /// The parameter type for the contract function `balanceOfNativeCurrency`. #[derive(Serialize, SchemaType)] #[concordium(transparent)] +#[repr(transparent)] pub struct NativeCurrencyBalanceOfParameter { /// List of balance queries. #[concordium(size_length = 2)] @@ -1331,6 +1334,7 @@ pub struct NativeCurrencyBalanceOfParameter { /// It consists of the list of results corresponding to the list of queries. #[derive(Serialize, SchemaType, PartialEq, Eq)] #[concordium(transparent)] +#[repr(transparent)] pub struct NativeCurrencyBalanceOfResponse(#[concordium(size_length = 2)] pub Vec); /// Conversion helper function. @@ -1380,6 +1384,7 @@ pub struct Cis2TokensBalanceOfQuery { /// The parameter type for the contract function `balanceOfCis2Tokens`. #[derive(Serialize, SchemaType)] #[concordium(transparent)] +#[repr(transparent)] pub struct Cis2TokensBalanceOfParameter { /// List of balance queries. #[concordium(size_length = 2)] @@ -1391,6 +1396,7 @@ pub struct Cis2TokensBalanceOfParameter { /// It consists of the list of results corresponding to the list of queries. #[derive(Serialize, SchemaType, PartialEq, Eq)] #[concordium(transparent)] +#[repr(transparent)] pub struct Cis2TokensBalanceOfResponse(#[concordium(size_length = 2)] pub Vec); /// Conversion helper function.