From 8937c55f954999a3cd36946b35331329312b01ef Mon Sep 17 00:00:00 2001 From: Yan Liu Date: Fri, 31 Jan 2025 09:55:38 +0100 Subject: [PATCH] A few more ICRC-2 tests --- ic-canister/src/canister_backend/icrc2.rs | 19 +- ic-canister/tests/test_icrc2.rs | 373 ++++++++++++++++++++++ 2 files changed, 390 insertions(+), 2 deletions(-) diff --git a/ic-canister/src/canister_backend/icrc2.rs b/ic-canister/src/canister_backend/icrc2.rs index 1a6f021..99d5061 100644 --- a/ic-canister/src/canister_backend/icrc2.rs +++ b/ic-canister/src/canister_backend/icrc2.rs @@ -3,6 +3,7 @@ use candid::Nat; use dcc_common::{ account_balance_get, approval_get, approval_update, get_timestamp_ns, ledger_funds_transfer, nat_to_balance, FundsTransfer, FundsTransferApproval, IcrcCompatibleAccount, TokenAmountE9s, + MEMO_BYTES_MAX, }; use ic_cdk::caller; use icrc_ledger_types::icrc1::account::Account; @@ -31,6 +32,16 @@ pub fn _icrc2_approve(args: ApproveArgs) -> Result { }); } + // Check the size of the memo + if let Some(memo) = &args.memo { + if memo.0.len() > MEMO_BYTES_MAX { + return Err(ApproveError::GenericError { + error_code: 2u32.into(), + message: "Memo too large".to_string(), + }); + } + } + // Check if caller has sufficient balance for fee let balance = account_balance_get(&from); if balance < DC_TOKEN_TRANSFER_FEE_E9S { @@ -92,7 +103,11 @@ pub fn _icrc2_approve(args: ApproveArgs) -> Result { args.amount .min(TokenAmountE9s::MAX.into()) .0 - .to_u64_digits()[0], + .to_u64_digits() + .iter() + .next() + .cloned() + .unwrap_or_default(), args.expires_at, DC_TOKEN_TRANSFER_FEE_E9S, args.memo.map(|m| m.0.to_vec()).unwrap_or_default(), @@ -120,7 +135,7 @@ pub fn _icrc2_transfer_from(args: TransferFromArgs) -> Result + ); + assert!(result.is_ok()); + + // Wait 2s for expiration + ctx.ffwd_to_next_block(ts); + ctx.ffwd_to_next_block(ts); + + // Try to transfer after expiration + let transfer_from_args = TransferFromArgs { + spender_subaccount: None, + from: owner, + to: recipient, + amount: 300_000_000u64.into(), + fee: None, + memo: None, + created_at_time: Some(ctx.get_timestamp_ns()), + }; + + let result = update_check_and_decode!( + ctx.pic, + ctx.canister_id, + spender.owner, + "icrc2_transfer_from", + candid::encode_one(transfer_from_args).unwrap(), + Result + ); + assert!(matches!( + result, + Err(TransferFromError::InsufficientAllowance { allowance: _ }) + )); +} + +#[test] +fn test_transfer_from_with_subaccount() { + let ctx = TestContext::new(); + let owner = create_test_account(18); + let spender = create_test_account(19); + let recipient = create_test_account(20); + let subaccount1 = [1u8; 32]; + let subaccount2 = [2u8; 32]; + + // Mint tokens to owner, and transfer half to subaccount2 + ctx.mint_tokens_for_test(&owner, 2_000_000_000); + ctx.transfer_funds( + &owner, + &Account { + subaccount: Some(subaccount2), + ..owner + }, + 1_000_000_000, + ) + .unwrap(); + + // Get current timestamp and fee + let ts = ctx.get_timestamp_ns(); + let fee = ctx.get_transfer_fee(); + + let icrc2_approve_spending = |from_subaccount: Option<[u8; 32]>| -> Result { + let approve_args_default = ApproveArgs { + from_subaccount, + spender, + amount: 500_000_000u64.into(), + expected_allowance: None, + expires_at: None, + fee: Some(fee.clone()), + memo: None, + created_at_time: Some(ts), + }; + + update_check_and_decode!( + ctx.pic, + ctx.canister_id, + owner.owner, + "icrc2_approve", + candid::encode_one(approve_args_default).unwrap(), + Result + ) + }; + + let icrc2_transfer = |spender_subaccount: Option<[u8; 32]>, + from_subaccount: Option<[u8; 32]>| + -> Result { + let transfer_from_args = TransferFromArgs { + spender_subaccount, + from: Account { + subaccount: from_subaccount, + ..owner + }, + to: recipient, + amount: 300_000_000u64.into(), + fee: Some(fee.clone()), + memo: None, + created_at_time: Some(ts), + }; + + update_check_and_decode!( + ctx.pic, + ctx.canister_id, + spender.owner, + "icrc2_transfer_from", + candid::encode_one(transfer_from_args).unwrap(), + Result + ) + }; + + // Approve spending from main account, no subaccount + let result = icrc2_approve_spending(None); + assert!(result.is_ok()); + + assert_eq!( + icrc2_transfer(None, Some(subaccount1)) + .unwrap_err() + .to_string(), + "the spender account does not have sufficient allowance, current allowance is 0" + ); + assert_eq!( + icrc2_transfer(Some(subaccount1), Some(subaccount1)) + .unwrap_err() + .to_string(), + "the spender account does not have sufficient allowance, current allowance is 0" + ); + + // Now approve spending from subaccount2 + let result = icrc2_approve_spending(Some(subaccount2)); + println!("result: {:#?}", result.unwrap()); + + // Spending from subaccount1 still fails + assert_eq!( + icrc2_transfer(None, Some(subaccount1)) + .unwrap_err() + .to_string(), + "the spender account does not have sufficient allowance, current allowance is 0" + ); + assert_eq!( + icrc2_transfer(Some(subaccount1), Some(subaccount1)) + .unwrap_err() + .to_string(), + "the spender account does not have sufficient allowance, current allowance is 0" + ); + + // Spender subaccount1 fails + assert_eq!( + icrc2_transfer(Some(subaccount1), Some(subaccount1)) + .unwrap_err() + .to_string(), + "the spender account does not have sufficient allowance, current allowance is 0" + ); + // Spender subaccount2 fails + assert_eq!( + icrc2_transfer(Some(subaccount2), Some(subaccount1)) + .unwrap_err() + .to_string(), + "the spender account does not have sufficient allowance, current allowance is 0" + ); + // Spender no subaccount FROM subaccount2 succeeds + let result = icrc2_transfer(None, Some(subaccount2)); + println!("result: {:#?}", result.unwrap()); +} + +#[test] +fn test_zero_amount_approval() { + let ctx = TestContext::new(); + let owner = create_test_account(21); + let spender = create_test_account(22); + + // Mint tokens to owner (enough for fee) + ctx.mint_tokens_for_test(&owner, 1_000_000); + + // Get current timestamp and fee + let ts = ctx.get_timestamp_ns(); + let fee = ctx.get_transfer_fee(); + + // Approve zero amount + let approve_args = ApproveArgs { + from_subaccount: None, + spender, + amount: 0u64.into(), + expected_allowance: None, + expires_at: None, + fee: Some(fee), + memo: None, + created_at_time: Some(ts), + }; + + let result = update_check_and_decode!( + ctx.pic, + ctx.canister_id, + owner.owner, + "icrc2_approve", + candid::encode_one(approve_args).unwrap(), + Result + ); + assert!(result.is_ok()); + + // Check allowance is zero + let allowance_args = AllowanceArgs { + account: owner, + spender, + }; + + let allowance = query_check_and_decode!( + ctx.pic, + ctx.canister_id, + "icrc2_allowance", + candid::encode_one(allowance_args).unwrap(), + icrc_ledger_types::icrc2::allowance::Allowance + ); + + assert_eq!(allowance.allowance, Nat::from(0u64)); +} + +#[test] +fn test_approve_with_memo() { + let ctx = TestContext::new(); + let owner = create_test_account(23); + let spender = create_test_account(24); + + // Mint tokens to owner + ctx.mint_tokens_for_test(&owner, 1_000_000_000); + + // Get current timestamp and fee + let ts = ctx.get_timestamp_ns(); + let fee = ctx.get_transfer_fee(); + + // Approve with memo + let approve_args = ApproveArgs { + from_subaccount: None, + spender, + amount: 500_000_000u64.into(), + expected_allowance: None, + expires_at: None, + fee: Some(fee), + memo: Some(icrc_ledger_types::icrc1::transfer::Memo( + vec![1, 2, 3, 4].into(), + )), + created_at_time: Some(ts), + }; + + let result = update_check_and_decode!( + ctx.pic, + ctx.canister_id, + owner.owner, + "icrc2_approve", + candid::encode_one(approve_args).unwrap(), + Result + ); + assert!(result.is_ok()); +} + +#[test] +fn test_multiple_approvals() { + let ctx = TestContext::new(); + let owner = create_test_account(25); + let spender1 = create_test_account(26); + let spender2 = create_test_account(27); + + // Mint tokens to owner + ctx.mint_tokens_for_test(&owner, 1_000_000_000); + + // Get current timestamp and fee + let ts = ctx.get_timestamp_ns(); + let fee = ctx.get_transfer_fee(); + + // Approve first spender + let approve_args = ApproveArgs { + from_subaccount: None, + spender: spender1, + amount: 300_000_000u64.into(), + expected_allowance: None, + expires_at: None, + fee: Some(fee.clone()), + memo: None, + created_at_time: Some(ts), + }; + + let result = update_check_and_decode!( + ctx.pic, + ctx.canister_id, + owner.owner, + "icrc2_approve", + candid::encode_one(approve_args).unwrap(), + Result + ); + assert!(result.is_ok()); + + // Approve second spender + let approve_args = ApproveArgs { + from_subaccount: None, + spender: spender2, + amount: 200_000_000u64.into(), + expected_allowance: None, + expires_at: None, + fee: Some(fee), + memo: None, + created_at_time: Some(ts), + }; + + let result = update_check_and_decode!( + ctx.pic, + ctx.canister_id, + owner.owner, + "icrc2_approve", + candid::encode_one(approve_args).unwrap(), + Result + ); + assert!(result.is_ok()); + + // Check both allowances + let allowance_args = AllowanceArgs { + account: owner, + spender: spender1, + }; + + let allowance1 = query_check_and_decode!( + ctx.pic, + ctx.canister_id, + "icrc2_allowance", + candid::encode_one(allowance_args).unwrap(), + icrc_ledger_types::icrc2::allowance::Allowance + ); + + let allowance_args = AllowanceArgs { + account: owner, + spender: spender2, + }; + + let allowance2 = query_check_and_decode!( + ctx.pic, + ctx.canister_id, + "icrc2_allowance", + candid::encode_one(allowance_args).unwrap(), + icrc_ledger_types::icrc2::allowance::Allowance + ); + + assert_eq!(allowance1.allowance, Nat::from(300_000_000u64)); + assert_eq!(allowance2.allowance, Nat::from(200_000_000u64)); +}