From 9b545559b07dcdb6e2f7e13cb13c6f916a243306 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Wed, 19 Jun 2024 06:16:53 +0300 Subject: [PATCH 1/6] Add new tests for coin-based contract opcodes --- fuel-vm/src/interpreter/contract.rs | 3 - fuel-vm/src/interpreter/contract/tests.rs | 426 ----------- fuel-vm/src/tests/coins.rs | 879 ++++++++++++++++++++++ fuel-vm/src/tests/contract.rs | 495 ------------ fuel-vm/src/tests/mod.rs | 1 + fuel-vm/src/tests/outputs.rs | 537 ------------- 6 files changed, 880 insertions(+), 1461 deletions(-) delete mode 100644 fuel-vm/src/interpreter/contract/tests.rs create mode 100644 fuel-vm/src/tests/coins.rs diff --git a/fuel-vm/src/interpreter/contract.rs b/fuel-vm/src/interpreter/contract.rs index 7588a7839b..d27e246932 100644 --- a/fuel-vm/src/interpreter/contract.rs +++ b/fuel-vm/src/interpreter/contract.rs @@ -57,9 +57,6 @@ use fuel_types::{ use alloc::borrow::Cow; -#[cfg(test)] -mod tests; - impl Interpreter where M: Memory, diff --git a/fuel-vm/src/interpreter/contract/tests.rs b/fuel-vm/src/interpreter/contract/tests.rs deleted file mode 100644 index 8c2fce185b..0000000000 --- a/fuel-vm/src/interpreter/contract/tests.rs +++ /dev/null @@ -1,426 +0,0 @@ -#![allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] - -use core::convert::Infallible; - -use alloc::vec; - -use super::*; -use crate::{ - interpreter::{ - internal::absolute_output_mem_range, - PanicContext, - }, - storage::MemoryStorage, -}; -use fuel_tx::{ - field::{ - Inputs, - Outputs, - }, - Input, - Script, -}; -use fuel_types::canonical::Deserialize; -use test_case::test_case; - -#[test_case(0, 32 => Ok(()); "Can read contract balance")] -fn test_contract_balance(b: Word, c: Word) -> IoResult<(), Infallible> { - let mut memory: MemoryInstance = vec![1u8; MEM_SIZE].try_into().unwrap(); - memory[b as usize..(b as usize + AssetId::LEN)] - .copy_from_slice(&[2u8; AssetId::LEN][..]); - memory[c as usize..(c as usize + ContractId::LEN)] - .copy_from_slice(&[3u8; ContractId::LEN][..]); - let contract_id = ContractId::from([3u8; 32]); - let mut storage = MemoryStorage::default(); - let old_balance = storage - .contract_asset_id_balance_insert(&contract_id, &AssetId::from([2u8; 32]), 33) - .unwrap(); - assert!(old_balance.is_none()); - let mut pc = 4; - - let mut panic_context = PanicContext::None; - let input_contracts = [contract_id].into_iter().collect(); - let input = ContractBalanceCtx { - storage: &mut storage, - memory: &mut memory, - pc: RegMut::new(&mut pc), - input_contracts: InputContracts::new(&input_contracts, &mut panic_context), - }; - let mut result = 0; - - input.contract_balance(&mut result, b, c)?; - - assert_eq!(pc, 8); - assert_eq!(result, 33); - - Ok(()) -} - -#[test_case(true, 0, 0, 50, 32, 32 => Ok(()); "Can transfer from external balance")] -#[test_case(false, 0, 0, 50, 32, 32 => Ok(()); "Can transfer from internal balance")] -#[test_case(true, 32, 32, 50, 0, 0 => Ok(()); "Can transfer from external balance with flipped offsets")] -#[test_case(false, 32, 32, 50, 0, 0 => Ok(()); "Can transfer from internal balance with flipped offsets")] -#[test_case(true, 0, 0, 70, 32, 32 => Err(RuntimeError::Recoverable(PanicReason::NotEnoughBalance)); "Cannot transfer from external balance insufficient funds")] -#[test_case(false, 0, 0, 70, 32, 32 => Err(RuntimeError::Recoverable(PanicReason::NotEnoughBalance)); "Cannot transfer from internal balance insufficient funds")] -#[test_case(true, MEM_SIZE as u64 - 1, 0, 50, 32, 32 => Err(RuntimeError::Recoverable(PanicReason::MemoryOverflow)); "External transfer errors if contract_id lookup overflows")] -#[test_case(false, MEM_SIZE as u64 - 1, 0, 50, 32, 32 => Err(RuntimeError::Recoverable(PanicReason::MemoryOverflow)); "Internal transfer errors if contract_id lookup overflows")] -#[test_case(true, 0, 0, 50, MEM_SIZE as u64 - 1, 32 => Err(RuntimeError::Recoverable(PanicReason::MemoryOverflow)); "External transfer errors if asset_id lookup overflows")] -#[test_case(false, 0, 0, 50, MEM_SIZE as u64 - 1, 32 => Err(RuntimeError::Recoverable(PanicReason::MemoryOverflow)); "Internal transfer errors if asset_id lookup overflows")] -fn test_transfer( - external: bool, - requested_contract_id_offset: Word, - real_contract_id_offset: Word, - transfer_amount: Word, - requested_asset_id_offset: Word, - real_asset_id_offset: Word, -) -> IoResult<(), Infallible> { - // Given - - const ASSET_ID: AssetId = AssetId::new([2u8; AssetId::LEN]); - const RECIPIENT_CONTRACT_ID: ContractId = ContractId::new([3u8; ContractId::LEN]); - const SOURCE_CONTRACT_ID: ContractId = ContractId::new([5u8; ContractId::LEN]); - - let mut pc = 4; - - // Arbitrary value - let fp = 2048; - let is = 0; - let mut cgas = 10_000; - let mut ggas = 10_000; - - let mut memory: MemoryInstance = vec![1u8; MEM_SIZE].try_into().unwrap(); - memory[real_contract_id_offset as usize - ..(real_contract_id_offset as usize + ContractId::LEN)] - .copy_from_slice(RECIPIENT_CONTRACT_ID.as_ref()); - memory[real_asset_id_offset as usize..(real_asset_id_offset as usize + AssetId::LEN)] - .copy_from_slice(ASSET_ID.as_ref()); - memory[fp as usize..(fp as usize + ContractId::LEN)] - .copy_from_slice(SOURCE_CONTRACT_ID.as_ref()); - - let mut storage = MemoryStorage::default(); - - let initial_recipient_contract_balance = 0; - let initial_source_contract_balance = 60; - let old_balance = storage - .contract_asset_id_balance_insert( - &SOURCE_CONTRACT_ID, - &ASSET_ID, - initial_source_contract_balance, - ) - .unwrap(); - assert!(old_balance.is_none()); - - let context = if external { - Context::Script { - block_height: Default::default(), - } - } else { - Context::Call { - block_height: Default::default(), - } - }; - - let mut balances = RuntimeBalances::try_from_iter([(ASSET_ID, 50)]).unwrap(); - let start_balance = balances.balance(&ASSET_ID).unwrap(); - - let mut receipts = Default::default(); - let mut panic_context = PanicContext::None; - let mut tx = Script::default(); - *tx.inputs_mut() = vec![Input::contract( - Default::default(), - Default::default(), - Default::default(), - Default::default(), - RECIPIENT_CONTRACT_ID, - )]; - - let input_contracts = [RECIPIENT_CONTRACT_ID].into_iter().collect(); - let transfer_ctx = TransferCtx { - storage: &mut storage, - memory: &mut memory, - pc: RegMut::new(&mut pc), - context: &context, - balances: &mut balances, - receipts: &mut receipts, - profiler: &mut Default::default(), - new_storage_gas_per_byte: 1, - tx: &mut tx, - input_contracts: InputContracts::new(&input_contracts, &mut panic_context), - tx_offset: 0, - cgas: RegMut::new(&mut cgas), - ggas: RegMut::new(&mut ggas), - fp: Reg::new(&fp), - is: Reg::new(&is), - }; - - // When - - transfer_ctx.transfer( - requested_contract_id_offset, - transfer_amount, - requested_asset_id_offset, - )?; - - // Then - - let final_recipient_contract_balance = storage - .contract_asset_id_balance(&RECIPIENT_CONTRACT_ID, &ASSET_ID) - .unwrap() - .unwrap(); - - let final_source_contract_balance = storage - .contract_asset_id_balance(&SOURCE_CONTRACT_ID, &ASSET_ID) - .unwrap() - .unwrap(); - - assert_eq!(pc, 8); - assert_eq!( - final_recipient_contract_balance, - initial_recipient_contract_balance + transfer_amount - ); - if external { - assert_eq!( - balances.balance(&ASSET_ID).unwrap(), - start_balance - transfer_amount - ); - assert_eq!( - final_source_contract_balance, - initial_source_contract_balance - ); - } else { - assert_eq!(balances.balance(&ASSET_ID).unwrap(), start_balance); - assert_eq!( - final_source_contract_balance, - initial_source_contract_balance - transfer_amount - ); - } - - Ok(()) -} - -#[test_case(true, 0, 0, 0, 50, 32, 32 => Ok(()); "Can transfer from external balance")] -#[test_case(false, 0, 0, 0, 50, 32, 32 => Ok(()); "Can transfer from internal balance")] -#[test_case(true, 32, 32, 0, 50, 0, 0 => Ok(()); "Can transfer from external balance with flipped offsets")] -#[test_case(false,32, 32, 0, 50, 0, 0 => Ok(()); "Can transfer from internal balance with flipped offsets")] -#[test_case(false, 0, 0, 0, 70, 32, 32 => Err(RuntimeError::Recoverable(PanicReason::NotEnoughBalance)); "Cannot transfer from external balance insufficient funds")] -#[test_case(false, 0, 0, 0, 70, 32, 32 => Err(RuntimeError::Recoverable(PanicReason::NotEnoughBalance)); "Cannot transfer from internal balance insufficient funds")] -#[test_case(true, MEM_SIZE as u64 - 1, 0, 0, 50, 32, 32 => Err(RuntimeError::Recoverable(PanicReason::MemoryOverflow)); "External transfer errors if contract_id lookup overflows")] -#[test_case(false, MEM_SIZE as u64 - 1, 0, 0, 50, 32, 32 => Err(RuntimeError::Recoverable(PanicReason::MemoryOverflow)); "Internal transfer errors if contract_id lookup overflows")] -#[test_case(true, 0, 0, 0, 50, MEM_SIZE as u64 - 1, 32 => Err(RuntimeError::Recoverable(PanicReason::MemoryOverflow)); "External transfer errors if asset_id lookup overflows")] -#[test_case(false, 0, 0, 0, 50, MEM_SIZE as u64 - 1, 32 => Err(RuntimeError::Recoverable(PanicReason::MemoryOverflow)); "Internal transfer errors if asset_id lookup overflows")] -fn test_transfer_output( - external: bool, - requested_recipient_offset: Word, - real_recipient_offset: Word, - output_index: Word, - transfer_amount: Word, - requested_asset_id_offset: Word, - real_asset_id_offset: Word, -) -> IoResult<(), Infallible> { - // Given - - const ASSET_ID: AssetId = AssetId::new([2u8; AssetId::LEN]); - const SOURCE_CONTRACT_ID: ContractId = ContractId::new([3u8; ContractId::LEN]); - const RECIPIENT_ADDRESS: Address = Address::new([4u8; Address::LEN]); - - let mut pc = 4; - - // Arbitrary value - let fp = 2048; - let is = 0; - let mut cgas = 10_000; - let mut ggas = 10_000; - - let mut memory: MemoryInstance = vec![1u8; MEM_SIZE].try_into().unwrap(); - - memory - [real_recipient_offset as usize..(real_recipient_offset as usize + Address::LEN)] - .copy_from_slice(RECIPIENT_ADDRESS.as_ref()); - memory[real_asset_id_offset as usize..(real_asset_id_offset as usize + AssetId::LEN)] - .copy_from_slice(ASSET_ID.as_ref()); - memory[fp as usize..(fp as usize + ContractId::LEN)] - .copy_from_slice(SOURCE_CONTRACT_ID.as_ref()); - - let mut storage = MemoryStorage::default(); - - let initial_contract_balance = 60; - - let old_balance = storage - .contract_asset_id_balance_insert( - &SOURCE_CONTRACT_ID, - &ASSET_ID, - initial_contract_balance, - ) - .unwrap(); - assert!(old_balance.is_none()); - - let context = if external { - Context::Script { - block_height: Default::default(), - } - } else { - Context::Call { - block_height: Default::default(), - } - }; - - let balance_of_start = transfer_amount; - - let mut balances = - RuntimeBalances::try_from_iter([(ASSET_ID, balance_of_start)]).unwrap(); - let mut receipts = Default::default(); - let mut tx = Script::default(); - *tx.inputs_mut() = vec![Input::contract( - Default::default(), - Default::default(), - Default::default(), - Default::default(), - SOURCE_CONTRACT_ID, - )]; - - *tx.outputs_mut() = vec![Output::variable( - RECIPIENT_ADDRESS, - Default::default(), - Default::default(), - )]; - - let tx_offset = 512; - - let output_range = - absolute_output_mem_range(&tx, tx_offset, output_index as usize).unwrap(); - - let input_contracts = [SOURCE_CONTRACT_ID].into_iter().collect(); - let mut panic_context = PanicContext::None; - let transfer_ctx = TransferCtx { - storage: &mut storage, - memory: &mut memory, - pc: RegMut::new(&mut pc), - context: &context, - balances: &mut balances, - receipts: &mut receipts, - profiler: &mut Default::default(), - new_storage_gas_per_byte: 1, - tx: &mut tx, - input_contracts: InputContracts::new(&input_contracts, &mut panic_context), - tx_offset, - cgas: RegMut::new(&mut cgas), - ggas: RegMut::new(&mut ggas), - fp: Reg::new(&fp), - is: Reg::new(&is), - }; - - // When - - transfer_ctx.transfer_output( - requested_recipient_offset, - output_index, - transfer_amount, - requested_asset_id_offset, - )?; - - // Then - - let final_contract_balance = storage - .contract_asset_id_balance(&SOURCE_CONTRACT_ID, &ASSET_ID) - .unwrap() - .unwrap(); - - assert_eq!(pc, 8); - - let output_bytes: &[u8] = &memory[output_range.start..output_range.end]; - let output = Output::from_bytes(output_bytes).unwrap(); - let output_amount = output.amount().unwrap(); - assert_eq!(output_amount, transfer_amount); - - if external { - // In an external context, decrease MEM[balanceOfStart(MEM[$rD, 32]), 8] by $rC. - assert_eq!( - balances.balance(&ASSET_ID).unwrap(), - balance_of_start - transfer_amount - ); - assert_eq!(final_contract_balance, initial_contract_balance); - } else { - assert_eq!(balances.balance(&ASSET_ID).unwrap(), balance_of_start); - assert_eq!( - final_contract_balance, - initial_contract_balance - transfer_amount - ); - } - - Ok(()) -} - -#[test_case(0, 0 => Ok(()); "Can increase balance by zero")] -#[test_case(None, 0 => Ok(()); "Can initialize balance to zero")] -#[test_case(None, Word::MAX => Ok(()); "Can initialize balance to max")] -#[test_case(0, Word::MAX => Ok(()); "Can add max to zero")] -#[test_case(Word::MAX, 0 => Ok(()); "Can add zero to max")] -#[test_case(1, Word::MAX => Err(RuntimeError::Recoverable(PanicReason::BalanceOverflow)); "Overflowing add")] -#[test_case(Word::MAX, 1 => Err(RuntimeError::Recoverable(PanicReason::BalanceOverflow)); "Overflowing 1 add")] -fn test_balance_increase( - initial: impl Into>, - amount: Word, -) -> IoResult<(), Infallible> { - let contract_id = ContractId::from([3u8; 32]); - let asset_id = AssetId::from([2u8; 32]); - let mut storage = MemoryStorage::default(); - let initial = initial.into(); - if let Some(initial) = initial { - let old_balance = storage - .contract_asset_id_balance_insert(&contract_id, &asset_id, initial) - .unwrap(); - assert!(old_balance.is_none()); - } - - let (result, created_new_entry) = - balance_increase(&mut storage, &contract_id, &asset_id, amount)?; - - assert!(initial.is_none() == created_new_entry); - let initial = initial.unwrap_or(0); - assert_eq!(result, initial + amount); - - let result = storage - .contract_asset_id_balance(&contract_id, &asset_id) - .unwrap() - .unwrap(); - - assert_eq!(result, initial + amount); - - Ok(()) -} - -#[test_case(0, 0 => Ok(()); "Can increase balance by zero")] -#[test_case(None, 0 => Ok(()); "Can initialize balance to zero")] -#[test_case(Word::MAX, 0 => Ok(()); "Can initialize balance to max")] -#[test_case(10, 10 => Ok(()); "Can subtract to zero")] -#[test_case(1, Word::MAX => Err(RuntimeError::Recoverable(PanicReason::NotEnoughBalance)); "Overflowing subtract")] -#[test_case(1, 2 => Err(RuntimeError::Recoverable(PanicReason::NotEnoughBalance)); "Overflowing 1 subtract")] -fn test_balance_decrease( - initial: impl Into>, - amount: Word, -) -> IoResult<(), Infallible> { - let contract_id = ContractId::from([3u8; 32]); - let asset_id = AssetId::from([2u8; 32]); - let mut storage = MemoryStorage::default(); - let initial = initial.into(); - if let Some(initial) = initial { - let old_balance = storage - .contract_asset_id_balance_insert(&contract_id, &asset_id, initial) - .unwrap(); - assert!(old_balance.is_none()); - } - - let result = balance_decrease(&mut storage, &contract_id, &asset_id, amount)?; - let initial = initial.unwrap_or(0); - - assert_eq!(result, initial - amount); - - let result = storage - .contract_asset_id_balance(&contract_id, &asset_id) - .unwrap() - .unwrap(); - - assert_eq!(result, initial - amount); - - Ok(()) -} diff --git a/fuel-vm/src/tests/coins.rs b/fuel-vm/src/tests/coins.rs new file mode 100644 index 0000000000..573d64bf61 --- /dev/null +++ b/fuel-vm/src/tests/coins.rs @@ -0,0 +1,879 @@ +use rstest::rstest; +use test_case::test_case; + +use fuel_asm::{ + op, + GTFArgs, + Instruction, + RegId, + Word, +}; +use fuel_tx::{ + field::Outputs, + Address, + AssetId, + Bytes32, + ContractId, + ContractIdExt, + Output, + PanicReason, + Receipt, + ScriptExecutionResult, +}; +use fuel_types::canonical::Serialize; + +use crate::{ + call::Call, + consts::VM_MAX_RAM, + prelude::TestBuilder, + tests::test_helpers::set_full_word, + util::test_helpers::find_change, +}; + +fn run(mut test_context: TestBuilder, call_contract_id: ContractId) -> Vec { + let script_ops = vec![ + op::gtf_args(0x10, RegId::ZERO, GTFArgs::ScriptData), + op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS), + op::ret(RegId::ONE), + ]; + let script_data: Vec = [Call::new(call_contract_id, 0, 0).to_bytes().as_slice()] + .into_iter() + .flatten() + .copied() + .collect(); + + test_context + .start_script(script_ops, script_data) + .script_gas_limit(1_000_000) + .contract_input(call_contract_id) + .fee_input() + .contract_output(&call_contract_id) + .execute() + .receipts() + .to_vec() +} + +/// `value_extractor` is called only on success +fn extract_result( + receipts: &[Receipt], + value_extractor: fn(&[Receipt]) -> Option, +) -> RunResult { + let Receipt::ScriptResult { result, .. } = receipts.last().unwrap() else { + unreachable!("No script result"); + }; + + match *result { + ScriptExecutionResult::Success => match value_extractor(receipts) { + Some(v) => RunResult::Success(v), + None => RunResult::UnableToExtractValue, + }, + ScriptExecutionResult::Revert => RunResult::Revert, + ScriptExecutionResult::Panic => RunResult::Panic({ + let Receipt::Panic { reason, .. } = receipts[receipts.len() - 2] else { + unreachable!("No panic receipt"); + }; + *reason.reason() + }), + ScriptExecutionResult::GenericFailure(value) => RunResult::GenericFailure(value), + } +} + +fn extract_novalue(receipts: &[Receipt]) -> RunResult<()> { + extract_result(receipts, |_| Some(())) +} + +fn first_log(receipts: &[Receipt]) -> Option { + receipts + .iter() + .filter_map(|receipt| match receipt { + Receipt::Log { ra, .. } => Some(*ra), + _ => None, + }) + .next() +} + +fn first_tro(receipts: &[Receipt]) -> Option { + receipts + .iter() + .filter_map(|receipt| match receipt { + Receipt::TransferOut { amount, .. } => Some(*amount), + _ => None, + }) + .next() +} + +#[derive(Debug, PartialEq)] +enum RunResult { + Success(T), + UnableToExtractValue, + Revert, + Panic(PanicReason), + GenericFailure(u64), +} +impl RunResult { + fn is_ok(&self) -> bool { + matches!(self, RunResult::Success(_)) + } + + fn map R, R>(self, f: F) -> RunResult { + match self { + RunResult::Success(v) => RunResult::Success(f(v)), + RunResult::UnableToExtractValue => RunResult::UnableToExtractValue, + RunResult::Revert => RunResult::Revert, + RunResult::Panic(r) => RunResult::Panic(r), + RunResult::GenericFailure(v) => RunResult::GenericFailure(v), + } + } +} + +const REG_DATA_PTR: u8 = 0x3f; +const REG_HELPER: u8 = 0x3e; + +#[test_case(0, 0, op::bal(0x20, RegId::HP, REG_DATA_PTR) => RunResult::Success(0); "Works correctly with balance 0")] +#[test_case(1234, 0, op::bal(0x20, RegId::HP, REG_DATA_PTR) => RunResult::Success(1234); "Works correctly with balance 1234")] +#[test_case(Word::MAX, 0, op::bal(0x20, RegId::HP, REG_DATA_PTR) => RunResult::Success(Word::MAX); "Works correctly with balance Word::MAX")] +#[test_case(0, Word::MAX - 31, op::bal(0x20, REG_HELPER, REG_DATA_PTR) => RunResult::Panic(PanicReason::MemoryOverflow); "$rB + 32 overflows")] +#[test_case(0, VM_MAX_RAM - 31, op::bal(0x20, REG_HELPER, REG_DATA_PTR) => RunResult::Panic(PanicReason::MemoryOverflow); "$rB + 32 > VM_MAX_RAM")] +#[test_case(0, Word::MAX - 31, op::bal(0x20, RegId::HP, REG_HELPER) => RunResult::Panic(PanicReason::MemoryOverflow); "$rC + 32 overflows")] +#[test_case(0, VM_MAX_RAM - 31, op::bal(0x20, RegId::HP, REG_HELPER) => RunResult::Panic(PanicReason::MemoryOverflow); "$rC + 32 > VM_MAX_RAM")] +#[test_case(0, 0, op::bal(0x20, RegId::HP, RegId::ZERO) => RunResult::Panic(PanicReason::ContractNotInInputs); "Contract not in inputs")] +fn bal_external(amount: Word, helper: Word, bal_instr: Instruction) -> RunResult { + let reg_len: u8 = 0x10; + + let mut ops = set_full_word(REG_HELPER.into(), helper); + ops.extend(set_full_word( + reg_len.into(), + (ContractId::LEN + AssetId::LEN) as Word, + )); + ops.extend([ + // Compute asset id from contract id and sub asset id + op::aloc(reg_len), + op::gtf_args(REG_DATA_PTR, RegId::ZERO, GTFArgs::ScriptData), + bal_instr, + op::log(0x20, RegId::ZERO, RegId::ZERO, RegId::ZERO), + op::ret(RegId::ONE), + ]); + + let mut test_context = TestBuilder::new(1234u64); + let contract_id = test_context + .setup_contract( + vec![op::ret(RegId::ONE)], + Some((AssetId::zeroed(), amount)), + None, + ) + .contract_id; + + let result = test_context + .start_script(ops, contract_id.to_bytes()) + .script_gas_limit(1_000_000) + .contract_input(contract_id) + .fee_input() + .contract_output(&contract_id) + .execute(); + + extract_result(result.receipts(), first_log) +} + +#[test_case(0, 0, op::bal(0x20, RegId::HP, REG_DATA_PTR) => RunResult::Success(0); "Works correctly with balance 0")] +#[test_case(1234, 0, op::bal(0x20, RegId::HP, REG_DATA_PTR) => RunResult::Success(1234); "Works correctly with balance 1234")] +#[test_case(Word::MAX, 0, op::bal(0x20, RegId::HP, REG_DATA_PTR) => RunResult::Success(Word::MAX); "Works correctly with balance Word::MAX")] +#[test_case(0, Word::MAX - 31, op::bal(0x20, REG_HELPER, REG_DATA_PTR) => RunResult::Panic(PanicReason::MemoryOverflow); "$rB + 32 overflows")] +#[test_case(0, VM_MAX_RAM - 31, op::bal(0x20, REG_HELPER, REG_DATA_PTR) => RunResult::Panic(PanicReason::MemoryOverflow); "$rB + 32 > VM_MAX_RAM")] +#[test_case(0, Word::MAX - 31, op::bal(0x20, RegId::HP, REG_HELPER) => RunResult::Panic(PanicReason::MemoryOverflow); "$rC + 32 overflows")] +#[test_case(0, VM_MAX_RAM - 31, op::bal(0x20, RegId::HP, REG_HELPER) => RunResult::Panic(PanicReason::MemoryOverflow); "$rC + 32 > VM_MAX_RAM")] +#[test_case(0, 0, op::bal(0x20, RegId::HP, RegId::ZERO) => RunResult::Panic(PanicReason::ContractNotInInputs); "Contract not in inputs")] +fn mint_and_bal( + mint_amount: Word, + helper: Word, + bal_instr: Instruction, +) -> RunResult { + let reg_len: u8 = 0x10; + let reg_mint_amount: u8 = 0x11; + + let mut ops = set_full_word(reg_mint_amount.into(), mint_amount); + ops.extend(set_full_word(REG_HELPER.into(), helper)); + ops.extend(set_full_word( + reg_len.into(), + (ContractId::LEN + AssetId::LEN) as Word, + )); + ops.extend([ + // Compute asset id from contract id and sub asset id + op::aloc(reg_len), + op::mint(reg_mint_amount, RegId::HP), // Mint using the zero subid. + op::gtf_args(REG_DATA_PTR, RegId::ZERO, GTFArgs::ScriptData), + op::mcpi(RegId::HP, REG_DATA_PTR, ContractId::LEN.try_into().unwrap()), + op::s256(RegId::HP, RegId::HP, reg_len), + bal_instr, + op::log(0x20, RegId::ZERO, RegId::ZERO, RegId::ZERO), + op::ret(RegId::ONE), + ]); + + let mut test_context = TestBuilder::new(1234u64); + let contract_id = test_context.setup_contract(ops, None, None).contract_id; + extract_result(&run(test_context, contract_id), first_log) +} + +#[rstest] +#[case(0, RegId::HP, RunResult::Success(()))] +#[case(Word::MAX, RegId::HP, RunResult::Success(()))] +#[case(Word::MAX - 31, REG_HELPER, RunResult::Panic(PanicReason::MemoryOverflow))] +#[case(VM_MAX_RAM - 31, REG_HELPER, RunResult::Panic(PanicReason::MemoryOverflow))] +fn mint_burn_bounds>( + #[values(op::mint, op::burn)] instr: fn(RegId, R) -> Instruction, + #[case] helper: Word, + #[case] sub_id_ptr_reg: R, + #[case] result: RunResult<()>, +) { + let reg_len: u8 = 0x10; + + let mut ops = set_full_word(REG_HELPER.into(), helper); + ops.extend(set_full_word( + reg_len.into(), + (ContractId::LEN + AssetId::LEN) as Word, + )); + ops.extend([ + // Compute asset id from contract id and sub asset id + op::gtf_args(REG_DATA_PTR, RegId::ZERO, GTFArgs::ScriptData), + op::aloc(reg_len), + instr(RegId::ZERO, sub_id_ptr_reg), + op::log(RegId::ZERO, RegId::ZERO, RegId::ZERO, RegId::ZERO), + op::ret(RegId::ONE), + ]); + + let mut test_context = TestBuilder::new(1234u64); + let contract_id = test_context + .setup_contract(ops, Some((AssetId::zeroed(), Word::MAX - helper)), None) // Ensure enough but not too much balance + .contract_id; + assert_eq!(extract_novalue(&run(test_context, contract_id)), result); +} + +#[test_case(vec![(op::mint, 0, 0)] => RunResult::Success(()); "Mint 0")] +#[test_case(vec![(op::burn, 0, 0)] => RunResult::Success(()); "Burn 0")] +#[test_case(vec![(op::mint, 100, 0)] => RunResult::Success(()); "Mint 100")] +#[test_case(vec![(op::mint, Word::MAX, 0)] => RunResult::Success(()); "Mint Word::MAX")] +#[test_case(vec![(op::mint, 100, 0), (op::burn, 100, 0)] => RunResult::Success(()); "Mint 100, Burn all")] +#[test_case(vec![(op::mint, Word::MAX, 0), (op::burn, Word::MAX, 0)] => RunResult::Success(()); "Mint Word::MAX, Burn all")] +#[test_case(vec![(op::mint, 100, 0), (op::mint, 10, 0), (op::burn, 20, 0)] => RunResult::Success(()); "Mint 10 and 10, Burn all")] +#[test_case(vec![(op::mint, 2, 0), (op::mint, 3, 1), (op::burn, 2, 0), (op::burn, 3, 1)] => RunResult::Success(()); "Mint multiple assets, Burn all")] +#[test_case(vec![(op::burn, 1, 0)] => RunResult::Panic(PanicReason::NotEnoughBalance); "Burn nonexisting 1")] +#[test_case(vec![(op::burn, Word::MAX, 0)] => RunResult::Panic(PanicReason::NotEnoughBalance); "Burn nonexisting Word::MAX")] +#[test_case(vec![(op::mint, Word::MAX, 0), (op::mint, 1, 0)] => RunResult::Panic(PanicReason::BalanceOverflow); "Mint overflow")] +#[test_case(vec![(op::mint, Word::MAX, 0), (op::burn, 1, 0), (op::mint, 2, 0)] => RunResult::Panic(PanicReason::BalanceOverflow); "Mint,Burn,Mint overflow")] +fn mint_burn_single_sequence( + seq: Vec<(fn(RegId, RegId) -> Instruction, Word, u8)>, +) -> RunResult<()> { + let reg_len: u8 = 0x10; + let reg_mint_amount: u8 = 0x11; + + let mut ops = vec![ + // Allocate space for sub asset id + op::movi(reg_len, 32), + op::aloc(reg_len), + ]; + + for (mint_or_burn, amount, sub_id) in seq { + ops.push(op::sb(RegId::HP, sub_id, 0)); + ops.extend(set_full_word(reg_mint_amount.into(), amount)); + ops.push(mint_or_burn(reg_mint_amount.into(), RegId::HP)); + } + ops.push(op::ret(RegId::ONE)); + + let mut test_context = TestBuilder::new(1234u64); + let contract_id = test_context.setup_contract(ops, None, None).contract_id; + extract_novalue(&run(test_context, contract_id)) +} + +#[test_case(vec![false] => RunResult::Panic(PanicReason::NotEnoughBalance); "Burn")] +#[test_case(vec![true, false, false] => RunResult::Panic(PanicReason::NotEnoughBalance); "Mint,Burn,Burn")] +#[test_case(vec![true, true, false] => RunResult::Success(1); "Mint,Mint,Burn")] +fn mint_burn_many_calls_sequence(seq: Vec) -> RunResult { + let reg_len: u8 = 0x10; + let reg_jump: u8 = 0x11; + + let ops = vec![ + // Allocate space for zero sub asset id + op::movi(reg_len, 32), + op::aloc(reg_len), + op::jmpf(reg_jump, 0), + op::mint(RegId::ONE, RegId::HP), // Jump of 0 - mint 1 + op::ret(RegId::ONE), // Jump of 1 - do nothing + op::burn(RegId::ONE, RegId::HP), // Jump of 2 - burn 1 + op::ret(RegId::ONE), // Jump of 3 - do nothing + ]; + + let mut test_context = TestBuilder::new(1234u64); + let contract_id = test_context.setup_contract(ops, None, None).contract_id; + + for instr in seq { + let script_ops = vec![ + op::movi(reg_jump, if instr { 0 } else { 2 }), + op::gtf_args(0x10, RegId::ZERO, GTFArgs::ScriptData), + op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS), + op::ret(RegId::ONE), + ]; + let script_data: Vec = [Call::new(contract_id, 0, 0).to_bytes().as_slice()] + .into_iter() + .flatten() + .copied() + .collect(); + + let receipts = test_context + .start_script(script_ops, script_data) + .script_gas_limit(1_000_000) + .contract_input(contract_id) + .fee_input() + .contract_output(&contract_id) + .execute() + .receipts() + .to_vec(); + + let result = extract_novalue(&receipts); + if !result.is_ok() { + return result.map(|_| unreachable!()); + } + } + + RunResult::Success( + test_context.get_contract_balance( + &contract_id, + &contract_id.asset_id(&Bytes32::zeroed()), + ), + ) +} + +#[test_case(0, 10, 0 => RunResult::Panic(PanicReason::TransferZeroCoins); "Cannot transfer 0 coins")] +#[test_case(1, 10, 0 => RunResult::Success((9, 1)); "Can transfer 1 coins to empty")] +#[test_case(1, 10, 5 => RunResult::Success((9, 6)); "Can transfer 1 coins to non-empty")] +#[test_case(11, 10, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "Cannot transfer just over balance coins")] +#[test_case(Word::MAX, 0, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "Cannot transfer max over balance coins")] +#[test_case(1, 1, Word::MAX => RunResult::Panic(PanicReason::BalanceOverflow); "Cannot overflow balance of contract")] +#[test_case(Word::MAX, Word::MAX, 0 => RunResult::Success((0, Word::MAX)); "Can transfer Word::MAX coins to empty contract")] +fn transfer_to_contract_external( + amount: Word, + balance: Word, + other_balance: Word, +) -> RunResult<(Word, Word)> { + let contract_id_ptr = 0x11; + let asset_id_ptr = 0x12; + let reg_amount = 0x13; + + let mut ops = set_full_word(reg_amount.into(), amount); + + ops.extend(&[ + op::gtf_args(contract_id_ptr, RegId::ZERO, GTFArgs::ScriptData), + op::addi( + asset_id_ptr, + contract_id_ptr, + ContractId::LEN.try_into().unwrap(), + ), + op::tr(contract_id_ptr, reg_amount, asset_id_ptr), + op::ret(RegId::ONE), + ]); + + let mut test_context = TestBuilder::new(1234u64); + let asset_id = AssetId::new([1; 32]); + + let contract = test_context + .setup_contract( + vec![op::ret(RegId::ONE)], + Some((asset_id, other_balance)), + None, + ) + .contract_id; + + let script_data: Vec = contract + .to_bytes() + .into_iter() + .chain(asset_id.to_bytes()) + .collect(); + + let (_, tx, receipts) = test_context + .start_script(ops, script_data) + .script_gas_limit(1_000_000) + .contract_input(contract) + .coin_input(asset_id, balance) + .fee_input() + .contract_output(&contract) + .change_output(asset_id) + .execute() + .into_inner(); + + let change = find_change(tx.outputs().to_vec(), asset_id); + let result = extract_novalue(&receipts); + if !result.is_ok() { + assert_eq!(change, balance, "Revert should not change balance") + } + result.map(|()| { + ( + change, + test_context.get_contract_balance(&contract, &asset_id), + ) + }) +} + +#[test_case(1, 0, 10, 0 => RunResult::Panic(PanicReason::TransferZeroCoins); "Cannot transfer 0 coins to empty other")] +#[test_case(1, 0, 10, 5 => RunResult::Panic(PanicReason::TransferZeroCoins); "Cannot transfer 0 coins to non-empty other")] +#[test_case(0, 0, 10, 0 => RunResult::Panic(PanicReason::TransferZeroCoins); "Cannot transfer 0 coins to self")] +#[test_case(1, 1, 10, 0 => RunResult::Success((9, 1)); "Can transfer 1 coins to other")] +#[test_case(0, 1, 10, 0 => RunResult::Success((10, 0)); "Can transfer 1 coins to self")] +#[test_case(1, 11, 10, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "Cannot transfer just over balance coins to other")] +#[test_case(0, 11, 10, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "Cannot transfer just over balance coins to self")] +#[test_case(1, Word::MAX, 0, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "Cannot transfer max over balance coins to other")] +#[test_case(0, Word::MAX, 0, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "Cannot transfer max over balance coins to self")] +#[test_case(1, 1, 1, Word::MAX => RunResult::Panic(PanicReason::BalanceOverflow); "Cannot overflow balance of other contract")] +#[test_case(0, Word::MAX, Word::MAX, 0 => RunResult::Success((Word::MAX, 0)); "Can transfer Word::MAX coins to self")] +#[test_case(1, Word::MAX, Word::MAX, 0 => RunResult::Success((0, Word::MAX)); "Can transfer Word::MAX coins to empty other")] +#[test_case(2, 1, 1, 0 => RunResult::Panic(PanicReason::ContractNotInInputs); "Transfer target not in inputs")] +fn transfer_to_contract_internal( + to: usize, // 0 = self, 1 = other, 2 = non-existing + amount: Word, + balance: Word, + other_balance: Word, +) -> RunResult<(Word, Word)> { + let reg_tmp = 0x10; + let contract_id_ptr = 0x11; + let asset_id_ptr = 0x12; + let reg_amount = 0x13; + + let mut ops = set_full_word(reg_amount.into(), amount); + + ops.extend(&[ + op::gtf_args(reg_tmp, RegId::ZERO, GTFArgs::ScriptData), + op::addi(contract_id_ptr, reg_tmp, Call::LEN.try_into().unwrap()), + op::addi( + asset_id_ptr, + contract_id_ptr, + ContractId::LEN.try_into().unwrap(), + ), + op::tr(contract_id_ptr, reg_amount, asset_id_ptr), + op::ret(RegId::ONE), + ]); + + let mut test_context = TestBuilder::new(1234u64); + let asset_id = AssetId::new([1; 32]); + + let this_contract = test_context + .setup_contract(ops, Some((asset_id, balance)), None) + .contract_id; + + let other_contract = test_context + .setup_contract( + vec![op::ret(RegId::ONE)], + Some((asset_id, other_balance)), + None, + ) + .contract_id; + + let script_ops = vec![ + op::gtf_args(0x10, RegId::ZERO, GTFArgs::ScriptData), + op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS), + op::ret(RegId::ONE), + ]; + let script_data: Vec = [Call::new(this_contract, 0, 0).to_bytes().as_slice()] + .into_iter() + .flatten() + .copied() + .chain(match to { + 0 => this_contract.to_bytes(), + 1 => other_contract.to_bytes(), + _ => vec![1u8; 32], // Non-existing contract + }) + .chain(asset_id.to_bytes()) + .collect(); + + let result = test_context + .start_script(script_ops, script_data) + .script_gas_limit(1_000_000) + .contract_input(this_contract) + .contract_input(other_contract) + .fee_input() + .contract_output(&this_contract) + .contract_output(&other_contract) + .execute(); + + extract_novalue(result.receipts()).map(|()| { + ( + test_context.get_contract_balance(&this_contract, &asset_id), + test_context.get_contract_balance(&other_contract, &asset_id), + ) + }) +} + +#[test_case(None, None => RunResult::Success(()); "Normal case works")] +#[test_case(Some(Word::MAX - 31), None => RunResult::Panic(PanicReason::MemoryOverflow); "$rA + 32 overflows")] +#[test_case(Some(VM_MAX_RAM - 31), None => RunResult::Panic(PanicReason::MemoryOverflow); "$rA + 32 > VM_MAX_RAM")] +#[test_case(None, Some(Word::MAX - 31) => RunResult::Panic(PanicReason::MemoryOverflow); "$rC + 32 overflows")] +#[test_case(None, Some(VM_MAX_RAM - 31) => RunResult::Panic(PanicReason::MemoryOverflow); "$rC + 32 > VM_MAX_RAM")] +fn transfer_to_contract_bounds( + overwrite_contract_id_ptr: Option, + overwrite_asset_id_ptr: Option, +) -> RunResult<()> { + let reg_tmp = 0x10; + let contract_id_ptr = 0x11; + let asset_id_ptr = 0x12; + + let mut ops = vec![ + op::gtf_args(reg_tmp, RegId::ZERO, GTFArgs::ScriptData), + op::addi(contract_id_ptr, reg_tmp, Call::LEN.try_into().unwrap()), + op::addi( + asset_id_ptr, + contract_id_ptr, + ContractId::LEN.try_into().unwrap(), + ), + ]; + + if let Some(value) = overwrite_contract_id_ptr { + ops.extend(set_full_word(contract_id_ptr.into(), value)); + } + + if let Some(value) = overwrite_asset_id_ptr { + ops.extend(set_full_word(asset_id_ptr.into(), value)); + } + + ops.extend(&[ + op::tr(contract_id_ptr, RegId::ONE, asset_id_ptr), + op::ret(RegId::ONE), + ]); + + let mut test_context = TestBuilder::new(1234u64); + let asset_id = AssetId::new([1; 32]); + + let this_contract = test_context + .setup_contract(ops, Some((asset_id, Word::MAX)), None) + .contract_id; + + let script_ops = vec![ + op::gtf_args(0x10, RegId::ZERO, GTFArgs::ScriptData), + op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS), + op::ret(RegId::ONE), + ]; + let script_data: Vec = [Call::new(this_contract, 0, 0).to_bytes().as_slice()] + .into_iter() + .flatten() + .copied() + .chain(this_contract.to_bytes()) + .chain(asset_id.to_bytes()) + .collect(); + + let result = test_context + .start_script(script_ops, script_data) + .script_gas_limit(1_000_000) + .contract_input(this_contract) + .fee_input() + .contract_output(&this_contract) + .execute(); + + extract_novalue(result.receipts()) +} + +#[test_case(false, 0, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(external) Cannot transfer 0 coins to non-Variable output")] +#[test_case(false, 1, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(external) Cannot transfer 0 coins to valid output")] +#[test_case(false, 9, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(external) Cannot transfer 0 coins to non-existing output")] +#[test_case(false, 1, 1, 10 => RunResult::Success((1, 9)); "(external) Can transfer 1 coins")] +#[test_case(false, 1, 11, 10 => RunResult::Panic(PanicReason::NotEnoughBalance); "(external) Cannot transfer just over balance coins")] +#[test_case(false, 1, Word::MAX, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "(external) Cannot transfer max over balance coins")] +#[test_case(false, 1, Word::MAX, Word::MAX => RunResult::Success((Word::MAX, 0)); "(external) Can transfer Word::MAX coins")] +#[test_case(false, 0, 1, 10 => RunResult::Panic(PanicReason::OutputNotFound); "(external) Target output is not Variable")] +#[test_case(false, 9, 1, 1 => RunResult::Panic(PanicReason::OutputNotFound); "(external) Target output doesn't exist")] +#[test_case(true, 0, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(internal) Cannot transfer 0 coins to non-Variable output")] +#[test_case(true, 1, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(internal) Cannot transfer 0 coins to valid output")] +#[test_case(true, 9, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(internal) Cannot transfer 0 coins to non-existing output")] +#[test_case(true, 1, 1, 10 => RunResult::Success((1, 9)); "(internal) Can transfer 1 coins")] +#[test_case(true, 1, 11, 10 => RunResult::Panic(PanicReason::NotEnoughBalance); "(internal) Cannot transfer just over balance coins")] +#[test_case(true, 1, Word::MAX, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "(internal) Cannot transfer max over balance coins")] +#[test_case(true, 1, Word::MAX, Word::MAX => RunResult::Success((Word::MAX, 0)); "(internal) Can transfer Word::MAX coins")] +#[test_case(true, 0, 1, 10 => RunResult::Panic(PanicReason::OutputNotFound); "(internal) Target output is not Variable")] +#[test_case(true, 9, 1, 1 => RunResult::Panic(PanicReason::OutputNotFound); "(internal) Target output doesn't exist")] +fn transfer_to_output( + internal: bool, + to_index: Word, // 1 = the variable output + amount: Word, + balance: Word, +) -> RunResult<(Word, Word)> { + let reg_tmp = 0x10; + let asset_id_ptr = 0x12; + let reg_amount = 0x13; + let reg_index = 0x14; + + let mut ops = set_full_word(reg_amount.into(), amount); + ops.extend(set_full_word(reg_index.into(), to_index)); + ops.extend(&[ + op::gtf_args(reg_tmp, RegId::ZERO, GTFArgs::ScriptData), + op::addi(asset_id_ptr, reg_tmp, Call::LEN.try_into().unwrap()), + op::tro(reg_tmp, reg_index, reg_amount, asset_id_ptr), + op::ret(RegId::ONE), + ]); + + let mut test_context = TestBuilder::new(1234u64); + let asset_id = AssetId::new([1; 32]); + + let contract_id = test_context + .setup_contract(ops.clone(), Some((asset_id, balance)), None) + .contract_id; + + let script_ops = if internal { + vec![ + op::gtf_args(0x10, RegId::ZERO, GTFArgs::ScriptData), + op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS), + op::ret(RegId::ONE), + ] + } else { + ops + }; + + let script_data: Vec = [Call::new(contract_id, 0, 0).to_bytes().as_slice()] + .into_iter() + .flatten() + .copied() + .chain(asset_id.to_bytes()) + .collect(); + + let mut builder = test_context + .start_script(script_ops, script_data) + .script_gas_limit(1_000_000) + .contract_input(contract_id) + .fee_input() + .contract_output(&contract_id) + .variable_output(asset_id); + + if !internal { + builder = builder + .coin_input(asset_id, balance) + .change_output(asset_id); + } + + let (_, tx, receipts) = builder.execute().into_inner(); + let result = extract_result(&receipts, first_tro); + + if let Some(Output::Variable { + to, + amount, + asset_id, + }) = tx.outputs().get(to_index as usize).copied() + { + if result.is_ok() { + assert_eq!(amount, amount, "Transfer amount is wrong"); + assert_eq!(asset_id, asset_id, "Transfer asset id is wrong"); + } else { + assert_eq!( + to, + Address::zeroed(), + "Transfer target should be zeroed on failure" + ); + assert_eq!(amount, 0, "Transfer amount should be 0 on failure"); + assert_eq!( + asset_id, + AssetId::zeroed(), + "Transfer asset id should be zeroed on failure" + ); + } + } + + if !result.is_ok() && !internal { + assert_eq!( + find_change(tx.outputs().to_vec(), asset_id), + balance, + "Revert should not change balance" + ) + } + + result.map(|tr| { + ( + tr, + if internal { + test_context.get_contract_balance(&contract_id, &asset_id) + } else { + find_change(tx.outputs().to_vec(), asset_id) + }, + ) + }) +} + +#[test_case(None, None => RunResult::Success(()); "Normal case works")] +#[test_case(Some(Word::MAX - 31), None => RunResult::Panic(PanicReason::MemoryOverflow); "$rA + 32 overflows")] +#[test_case(Some(VM_MAX_RAM - 31), None => RunResult::Panic(PanicReason::MemoryOverflow); "$rA + 32 > VM_MAX_RAM")] +#[test_case(None, Some(Word::MAX - 31) => RunResult::Panic(PanicReason::MemoryOverflow); "$rC + 32 overflows")] +#[test_case(None, Some(VM_MAX_RAM - 31) => RunResult::Panic(PanicReason::MemoryOverflow); "$rC + 32 > VM_MAX_RAM")] +fn transfer_to_output_bounds( + overwrite_contract_id_ptr: Option, + overwrite_asset_id_ptr: Option, +) -> RunResult<()> { + let reg_tmp = 0x10; + let contract_id_ptr = 0x11; + let asset_id_ptr = 0x12; + + let mut ops = vec![ + op::gtf_args(reg_tmp, RegId::ZERO, GTFArgs::ScriptData), + op::addi(contract_id_ptr, reg_tmp, Call::LEN.try_into().unwrap()), + op::addi( + asset_id_ptr, + contract_id_ptr, + ContractId::LEN.try_into().unwrap(), + ), + ]; + + if let Some(value) = overwrite_contract_id_ptr { + ops.extend(set_full_word(contract_id_ptr.into(), value)); + } + + if let Some(value) = overwrite_asset_id_ptr { + ops.extend(set_full_word(asset_id_ptr.into(), value)); + } + + ops.extend(&[ + op::tro(contract_id_ptr, RegId::ONE, RegId::ONE, asset_id_ptr), + op::ret(RegId::ONE), + ]); + + let mut test_context = TestBuilder::new(1234u64); + let asset_id = AssetId::new([1; 32]); + + let this_contract = test_context + .setup_contract(ops, Some((asset_id, Word::MAX)), None) + .contract_id; + + let script_ops = vec![ + op::gtf_args(0x10, RegId::ZERO, GTFArgs::ScriptData), + op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS), + op::ret(RegId::ONE), + ]; + let script_data: Vec = [Call::new(this_contract, 0, 0).to_bytes().as_slice()] + .into_iter() + .flatten() + .copied() + .chain(this_contract.to_bytes()) + .chain(asset_id.to_bytes()) + .collect(); + + let result = test_context + .start_script(script_ops, script_data) + .script_gas_limit(1_000_000) + .contract_input(this_contract) + .fee_input() + .contract_output(&this_contract) + .variable_output(asset_id) + .execute(); + + extract_novalue(result.receipts()) +} + +const M: Word = Word::MAX; + +// Calls script -> src -> dst +#[test_case(0, 0, 0, 0, 0 => ((0, 0, 0), RunResult::Success(())); "No coins moving, zero balances")] +#[test_case(1, 1, 1, 0, 0 => ((1, 1, 1), RunResult::Success(())); "No coins moving, nonzero balances")] +#[test_case(1, 0, 0, 1, 0 => ((0, 1, 0), RunResult::Success(())); "Fwd 1 from script to src")] +#[test_case(0, 1, 0, 0, 1 => ((0, 0, 1), RunResult::Success(())); "Fwd 1 from src to dst")] +#[test_case(1, 0, 0, 1, 1 => ((0, 0, 1), RunResult::Success(())); "Fwd 1 from script to dst")] +#[test_case(1, 2, 3, 1, 3 => ((0, 0, 6), RunResult::Success(())); "Fwd combination full")] +#[test_case(5, 5, 1, 3, 2 => ((2, 6, 3), RunResult::Success(())); "Fwd combination partial")] +#[test_case(M, 0, 0, M, 0 => ((0, M, 0), RunResult::Success(())); "Fwd Word::MAX from script to src")] +#[test_case(0, M, 0, 0, M => ((0, 0, M), RunResult::Success(())); "Fwd Word::MAX from src to dst")] +#[test_case(M, 0, 0, M, M => ((0, 0, M), RunResult::Success(())); "Fwd Word::MAX from script to dst")] +#[test_case(1, M, 0, 1, 0 => ((1, M, 0), RunResult::Panic(PanicReason::BalanceOverflow)); "Fwd 1 overflow on src")] +#[test_case(0, 1, M, 0, 1 => ((0, 1, M), RunResult::Panic(PanicReason::BalanceOverflow)); "Fwd 1 overflow on dst")] +#[test_case(M, 1, 0, M, 0 => ((M, 1, 0), RunResult::Panic(PanicReason::BalanceOverflow)); "Fwd Word::MAX overflow on src")] +#[test_case(0, M, 1, 0, M => ((0, M, 1), RunResult::Panic(PanicReason::BalanceOverflow)); "Fwd Word::MAX overflow on dst")] +#[test_case(M, M, M, M, M => ((M, M, M), RunResult::Panic(PanicReason::BalanceOverflow)); "Fwd Word::MAX both")] +#[test_case(0, 0, 0, 1, 0 => ((0, 0, 0), RunResult::Panic(PanicReason::NotEnoughBalance)); "Fwd 1 over empty script balance")] +#[test_case(1, 0, 0, 2, 0 => ((1, 0, 0), RunResult::Panic(PanicReason::NotEnoughBalance)); "Fwd 1 over script balance")] +#[test_case(0, 0, 0, M, 0 => ((0, 0, 0), RunResult::Panic(PanicReason::NotEnoughBalance)); "Fwd max over empty script balance")] +#[test_case(1, 0, 0, M, 0 => ((1, 0, 0), RunResult::Panic(PanicReason::NotEnoughBalance)); "Fwd max over script balance")] +#[test_case(0, 0, 0, 0, 1 => ((0, 0, 0), RunResult::Panic(PanicReason::NotEnoughBalance)); "Fwd 1 over empty src balance")] +#[test_case(0, 1, 0, 0, 2 => ((0, 1, 0), RunResult::Panic(PanicReason::NotEnoughBalance)); "Fwd 1 over src balance")] +#[test_case(0, 0, 0, 0, M => ((0, 0, 0), RunResult::Panic(PanicReason::NotEnoughBalance)); "Fwd max over empty src balance")] +#[test_case(0, 1, 0, 0, M => ((0, 1, 0), RunResult::Panic(PanicReason::NotEnoughBalance)); "Fwd max over src balance")] +fn call_forwarding_internal( + balance_in: Word, + balance_src: Word, + balance_dst: Word, + fwd_to_src: Word, + fwd_to_dst: Word, +) -> ((Word, Word, Word), RunResult<()>) { + let reg_tmp = 0x10; + let reg_dst_contract_ptr = 0x11; + let reg_fwd_to_src: u8 = 0x12; + let reg_fwd_to_dst: u8 = 0x13; + let reg_asset_id_ptr: u8 = 0x14; + + let mut test_context = TestBuilder::new(1234u64); + let asset_id = AssetId::new([1; 32]); + + // Setup the dst contract. This does nothing, just holds/receives the balance. + let dst_contract = test_context + .setup_contract( + vec![op::ret(RegId::ONE)], + Some((asset_id, balance_dst)), + None, + ) + .contract_id; + + // Setup the src contract. This just calls the dst to forward the coins. + let src_contract = test_context + .setup_contract( + vec![ + op::call(reg_dst_contract_ptr, reg_fwd_to_dst, RegId::HP, RegId::CGAS), + op::ret(RegId::ONE), + ], + Some((asset_id, balance_src)), + None, + ) + .contract_id; + + // Setup the script that does the call. + let mut script_ops = Vec::new(); + script_ops.extend(set_full_word(reg_fwd_to_src.into(), fwd_to_src)); + script_ops.extend(set_full_word(reg_fwd_to_dst.into(), fwd_to_dst)); + script_ops.extend(&[ + op::movi(reg_tmp, AssetId::LEN.try_into().unwrap()), + op::aloc(reg_tmp), + op::gtf_args(reg_tmp, RegId::ZERO, GTFArgs::ScriptData), + op::addi( + reg_asset_id_ptr, + reg_tmp, + (Call::LEN * 2).try_into().unwrap(), + ), + op::mcpi( + RegId::HP, + reg_asset_id_ptr, + AssetId::LEN.try_into().unwrap(), + ), + op::addi(reg_dst_contract_ptr, reg_tmp, Call::LEN.try_into().unwrap()), + op::call(reg_tmp, reg_fwd_to_src, RegId::HP, RegId::CGAS), + op::ret(RegId::ONE), + ]); + + let script_data: Vec = [ + Call::new(src_contract, 0, 0).to_bytes().as_slice(), + Call::new(dst_contract, 0, 0).to_bytes().as_slice(), + ] + .into_iter() + .flatten() + .copied() + .chain(asset_id.to_bytes()) + .collect(); + + let (_, tx, receipts) = test_context + .start_script(script_ops, script_data) + .script_gas_limit(1_000_000) + .contract_input(src_contract) + .contract_input(dst_contract) + .coin_input(asset_id, balance_in) + .fee_input() + .contract_output(&src_contract) + .contract_output(&dst_contract) + .change_output(asset_id) + .execute() + .into_inner(); + + let result = extract_novalue(&receipts); + let change = find_change(tx.outputs().to_vec(), asset_id); + ( + ( + change, + test_context.get_contract_balance(&src_contract, &asset_id), + test_context.get_contract_balance(&dst_contract, &asset_id), + ), + result, + ) +} diff --git a/fuel-vm/src/tests/contract.rs b/fuel-vm/src/tests/contract.rs index 7e1aada095..c53e8d612b 100644 --- a/fuel-vm/src/tests/contract.rs +++ b/fuel-vm/src/tests/contract.rs @@ -90,175 +90,6 @@ fn prevent_contract_id_redeployment() { ); } -#[test] -fn mint_burn() { - let mut test_context = TestBuilder::new(2322u64); - - let mut balance = 1000; - let gas_limit = 1_000_000; - - let program = vec![ - op::addi(0x10, RegId::FP, CallFrame::a_offset() as Immediate12), - op::lw(0x10, 0x10, 0), - op::addi(0x11, RegId::FP, CallFrame::b_offset() as Immediate12), - op::lw(0x11, 0x11, 0), - // Allocate 32 bytes for the zeroed `sub_id`. - op::movi(0x15, Bytes32::LEN as u32), - op::aloc(0x15), - op::jnei(0x10, RegId::ZERO, 9), - // Mint `0x11` amount of an assets created from zeroed `sub_id` - op::mint(0x11, RegId::HP), - op::ji(10), - op::burn(0x11, RegId::HP), - op::ret(RegId::ONE), - ]; - - let contract_id = test_context.setup_contract(program, None, None).contract_id; - - let asset_id = contract_id.asset_id(&Bytes32::zeroed()); - - let (script_call, _) = script_with_data_offset!( - data_offset, - vec![ - op::movi(0x10, data_offset as Immediate18), - op::call(0x10, RegId::ZERO, 0x10, RegId::CGAS), - op::ret(RegId::ONE), - ], - test_context.get_tx_params().tx_offset() - ); - let script_call_data = Call::new(contract_id, 0, balance).to_bytes(); - - let script_data_check_balance: Vec = asset_id - .as_ref() - .iter() - .chain(contract_id.as_ref().iter()) - .copied() - .collect(); - - let (script_check_balance, _) = script_with_data_offset!( - data_offset, - vec![ - op::movi(0x10, data_offset as Immediate18), - op::move_(0x11, 0x10), - op::addi(0x12, 0x10, AssetId::LEN as Immediate12), - op::bal(0x10, 0x11, 0x12), - op::log(0x10, RegId::ZERO, RegId::ZERO, RegId::ZERO), - op::ret(RegId::ONE), - ], - test_context.get_tx_params().tx_offset() - ); - - let result = test_context - .start_script( - script_check_balance.clone(), - script_data_check_balance.clone(), - ) - .script_gas_limit(gas_limit) - .contract_input(contract_id) - .fee_input() - .contract_output(&contract_id) - .execute(); - - let storage_balance = result.receipts()[0].ra().expect("Balance expected"); - assert_eq!(0, storage_balance); - - test_context - .start_script(script_call.clone(), script_call_data) - .script_gas_limit(gas_limit) - .contract_input(contract_id) - .fee_input() - .contract_output(&contract_id) - .execute(); - - let result = test_context - .start_script( - script_check_balance.clone(), - script_data_check_balance.clone(), - ) - .script_gas_limit(gas_limit) - .contract_input(contract_id) - .fee_input() - .contract_output(&contract_id) - .execute(); - - let storage_balance = result.receipts()[0].ra().expect("Balance expected"); - assert_eq!(balance as Word, storage_balance); - - // Try to burn more than the available balance - let script_call_data = Call::new(contract_id, 1, balance + 1).to_bytes(); - - let result = test_context - .start_script(script_call.clone(), script_call_data) - .script_gas_limit(gas_limit) - .contract_input(contract_id) - .fee_input() - .contract_output(&contract_id) - .execute(); - assert!(result.should_revert()); - - let result = test_context - .start_script( - script_check_balance.clone(), - script_data_check_balance.clone(), - ) - .script_gas_limit(gas_limit) - .contract_input(contract_id) - .fee_input() - .contract_output(&contract_id) - .execute(); - - let storage_balance = result.receipts()[0].ra().expect("Balance expected"); - assert_eq!(balance as Word, storage_balance); - - // Burn some of the balance - let burn = 100; - - let script_call_data = Call::new(contract_id, 1, burn).to_bytes(); - test_context - .start_script(script_call.clone(), script_call_data) - .script_gas_limit(gas_limit) - .contract_input(contract_id) - .fee_input() - .contract_output(&contract_id) - .execute(); - balance -= burn; - - let result = test_context - .start_script( - script_check_balance.clone(), - script_data_check_balance.clone(), - ) - .script_gas_limit(gas_limit) - .contract_input(contract_id) - .fee_input() - .contract_output(&contract_id) - .execute(); - - let storage_balance = result.receipts()[0].ra().expect("Balance expected"); - assert_eq!(balance as Word, storage_balance); - - // Burn the remainder balance - let script_call_data = Call::new(contract_id, 1, balance).to_bytes(); - test_context - .start_script(script_call, script_call_data) - .script_gas_limit(gas_limit) - .contract_input(contract_id) - .fee_input() - .contract_output(&contract_id) - .execute(); - - let result = test_context - .start_script(script_check_balance, script_data_check_balance) - .script_gas_limit(gas_limit) - .contract_input(contract_id) - .fee_input() - .contract_output(&contract_id) - .execute(); - - let storage_balance = result.receipts()[0].ra().expect("Balance expected"); - assert_eq!(0, storage_balance); -} - #[test] fn mint_consumes_gas_for_new_assets() { let mut test_context = TestBuilder::new(2322u64); @@ -323,329 +154,3 @@ fn mint_consumes_gas_for_new_assets() { assert!(new_asset > existing_asset); } - -#[test] -fn call_increases_contract_asset_balance_and_balance_register() { - let rng = &mut StdRng::seed_from_u64(2322u64); - - let gas_limit = 1_000_000; - let asset_id: AssetId = rng.gen(); - let call_amount = 500u64; - - let mut test_context = TestBuilder::new(2322u64); - let contract_id = test_context - .setup_contract(vec![op::ret(RegId::BAL)], None, None) - .contract_id; - - let (script_ops, offset) = script_with_data_offset!( - data_offset, - vec![ - // load call data to 0x10 - op::movi(0x10, data_offset + 32), - // load balance to forward to 0x12 - op::movi(0x11, call_amount as Immediate18), - // load the asset id to use to 0x13 - op::movi(0x12, data_offset), - // call the transfer contract - op::call(0x10, 0x11, 0x12, RegId::CGAS), - op::ret(RegId::ONE), - ], - test_context.get_tx_params().tx_offset() - ); - let script_data: Vec = [ - asset_id.as_ref(), - Call::new(contract_id, 0, offset as Word) - .to_bytes() - .as_slice(), - ] - .into_iter() - .flatten() - .copied() - .collect(); - - // starting contract balance - let start_balance = test_context.get_contract_balance(&contract_id, &asset_id); - assert_eq!(start_balance, 0); - - // call contract with some amount of coins to forward - let transfer_tx = test_context - .start_script(script_ops, script_data) - .script_gas_limit(gas_limit) - .gas_price(0) - .coin_input(asset_id, call_amount) - .contract_input(contract_id) - .contract_output(&contract_id) - .change_output(asset_id) - .execute(); - - // Ensure transfer tx processed correctly - assert!(!transfer_tx.should_revert()); - - // verify balance transfer occurred - let end_balance = test_context.get_contract_balance(&contract_id, &asset_id); - assert_eq!(end_balance, call_amount); - - // verify balance register was set - assert_eq!(transfer_tx.receipts()[1].val().unwrap(), call_amount); -} - -#[test] -fn call_decreases_internal_balance_and_increases_destination_contract_balance() { - let rng = &mut StdRng::seed_from_u64(2322u64); - - let gas_limit = 1_000_000; - let asset_id: AssetId = rng.gen(); - let call_amount = 500; - let initial_internal_balance = 1_000_000; - - let mut test_context = TestBuilder::new(2322u64); - let dest_contract_id = test_context - .setup_contract( - vec![ - // log the balance register - op::ret(RegId::BAL), - ], - None, - None, - ) - .contract_id; - - let program = vec![ - // load amount of tokens - op::addi(0x10, RegId::FP, CallFrame::a_offset() as Immediate12), - op::lw(0x10, 0x10, 0), - // load asset id - op::addi(0x11, RegId::FP, CallFrame::b_offset() as Immediate12), - op::lw(0x11, 0x11, 0), - // load contract id - op::addi(0x12, 0x11, 32 as Immediate12), - op::call(0x12, 0x10, 0x11, RegId::CGAS), - op::ret(RegId::BAL), - ]; - let sender_contract_id = test_context - .setup_contract(program, Some((asset_id, initial_internal_balance)), None) - .contract_id; - - let (script_ops, offset) = script_with_data_offset!( - data_offset, - vec![ - // load call data to 0x10 - op::movi(0x10, data_offset + 64), - // call the transfer contract - op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS), - op::ret(RegId::ONE), - ], - test_context.get_tx_params().tx_offset() - ); - let script_data: Vec = [ - asset_id.as_ref(), - dest_contract_id.as_ref(), - Call::new(sender_contract_id, call_amount, offset as Word) - .to_bytes() - .as_slice(), - ] - .into_iter() - .flatten() - .copied() - .collect(); - - // assert initial balance state - let dest_balance = test_context.get_contract_balance(&dest_contract_id, &asset_id); - assert_eq!(dest_balance, 0); - let source_balance = - test_context.get_contract_balance(&sender_contract_id, &asset_id); - assert_eq!(source_balance, initial_internal_balance); - - // initiate the call between contracts - let transfer_tx = test_context - .start_script(script_ops, script_data) - .script_gas_limit(gas_limit) - .gas_price(0) - .contract_input(sender_contract_id) - .contract_input(dest_contract_id) - .fee_input() - .contract_output(&sender_contract_id) - .contract_output(&dest_contract_id) - .execute(); - - // Ensure transfer tx processed correctly - assert!(!transfer_tx.should_revert()); - - // verify balance transfer occurred - let dest_balance = test_context.get_contract_balance(&dest_contract_id, &asset_id); - assert_eq!(dest_balance, call_amount); - let source_balance = - test_context.get_contract_balance(&sender_contract_id, &asset_id); - assert_eq!(source_balance, initial_internal_balance - call_amount); - - // verify balance register of source contract - // should be zero because external call transferred nothing - assert_eq!(transfer_tx.receipts()[3].val().unwrap(), 0); - - // verify balance register of destination contract - assert_eq!(transfer_tx.receipts()[2].val().unwrap(), call_amount); -} - -#[test] -fn internal_transfer_reduces_source_contract_balance_and_increases_destination_contract_balance( -) { - let rng = &mut StdRng::seed_from_u64(2322u64); - - let gas_limit = 1_000_000; - let asset_id: AssetId = rng.gen(); - let transfer_amount = 500; - let initial_internal_balance = 1_000_000; - - let mut test_context = TestBuilder::new(2322u64); - let dest_contract_id = test_context.setup_contract(vec![], None, None).contract_id; - - let program = vec![ - // load amount of tokens - op::addi(0x10, RegId::FP, CallFrame::a_offset() as Immediate12), - op::lw(0x10, 0x10, 0), - // load asset id - op::addi(0x11, RegId::FP, CallFrame::b_offset() as Immediate12), - op::lw(0x11, 0x11, 0), - // load contract id - op::addi(0x12, 0x11, 32 as Immediate12), - op::tr(0x12, 0x10, 0x11), - op::ret(RegId::ONE), - ]; - let sender_contract_id = test_context - .setup_contract(program, Some((asset_id, initial_internal_balance)), None) - .contract_id; - - let (script_ops, offset) = script_with_data_offset!( - data_offset, - vec![ - // load call data to 0x10 - op::movi(0x10, data_offset + 64), - // call the transfer contract - op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS), - op::ret(RegId::ONE), - ], - test_context.get_tx_params().tx_offset() - ); - let script_data: Vec = [ - asset_id.as_ref(), - dest_contract_id.as_ref(), - Call::new(sender_contract_id, transfer_amount, offset as Word) - .to_bytes() - .as_slice(), - ] - .into_iter() - .flatten() - .copied() - .collect(); - - // assert initial balance state - let dest_balance = test_context.get_contract_balance(&dest_contract_id, &asset_id); - assert_eq!(dest_balance, 0); - let source_balance = - test_context.get_contract_balance(&sender_contract_id, &asset_id); - assert_eq!(source_balance, initial_internal_balance); - - // initiate the transfer between contracts - let transfer_tx = test_context - .start_script(script_ops, script_data) - .script_gas_limit(gas_limit) - .gas_price(0) - .contract_input(sender_contract_id) - .contract_input(dest_contract_id) - .fee_input() - .contract_output(&sender_contract_id) - .contract_output(&dest_contract_id) - .execute(); - - // Ensure transfer tx processed correctly - assert!(!transfer_tx.should_revert()); - - // verify balance transfer occurred - let dest_balance = test_context.get_contract_balance(&dest_contract_id, &asset_id); - assert_eq!(dest_balance, transfer_amount); - let source_balance = - test_context.get_contract_balance(&sender_contract_id, &asset_id); - assert_eq!(source_balance, initial_internal_balance - transfer_amount); -} - -#[test] -fn internal_transfer_cant_exceed_more_than_source_contract_balance() { - let rng = &mut StdRng::seed_from_u64(2322u64); - - let gas_limit = 1_000_000; - let asset_id: AssetId = rng.gen(); - let transfer_amount = 500; - // set initial internal balance to < transfer amount - let initial_internal_balance = 100; - - let mut test_context = TestBuilder::new(2322u64); - let dest_contract_id = test_context.setup_contract(vec![], None, None).contract_id; - - let program = vec![ - // load amount of tokens - op::addi(0x10, RegId::FP, CallFrame::a_offset() as Immediate12), - op::lw(0x10, 0x10, 0), - // load asset id - op::addi(0x11, RegId::FP, CallFrame::b_offset() as Immediate12), - op::lw(0x11, 0x11, 0), - // load contract id - op::addi(0x12, 0x11, 32 as Immediate12), - op::tr(0x12, 0x10, 0x11), - op::ret(RegId::ONE), - ]; - - let sender_contract_id = test_context - .setup_contract(program, Some((asset_id, initial_internal_balance)), None) - .contract_id; - - let (script_ops, offset) = script_with_data_offset!( - data_offset, - vec![ - // load call data to 0x10 - op::movi(0x10, data_offset + 64), - // call the transfer contract - op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS), - op::ret(RegId::ONE), - ], - test_context.get_tx_params().tx_offset() - ); - let script_data: Vec = [ - asset_id.as_ref(), - dest_contract_id.as_ref(), - Call::new(sender_contract_id, transfer_amount, offset as Word) - .to_bytes() - .as_slice(), - ] - .into_iter() - .flatten() - .copied() - .collect(); - - // assert initial balance state - let dest_balance = test_context.get_contract_balance(&dest_contract_id, &asset_id); - assert_eq!(dest_balance, 0); - let source_balance = - test_context.get_contract_balance(&sender_contract_id, &asset_id); - assert_eq!(source_balance, initial_internal_balance); - - let transfer_tx = test_context - .start_script(script_ops, script_data) - .script_gas_limit(gas_limit) - .gas_price(0) - .contract_input(sender_contract_id) - .contract_input(dest_contract_id) - .fee_input() - .contract_output(&sender_contract_id) - .contract_output(&dest_contract_id) - .execute(); - - // Ensure transfer tx reverts since transfer amount is too large - assert!(transfer_tx.should_revert()); - - // verify balance transfer did not occur - let dest_balance = test_context.get_contract_balance(&dest_contract_id, &asset_id); - assert_eq!(dest_balance, 0); - let source_balance = - test_context.get_contract_balance(&sender_contract_id, &asset_id); - assert_eq!(source_balance, initial_internal_balance); -} diff --git a/fuel-vm/src/tests/mod.rs b/fuel-vm/src/tests/mod.rs index ddd6e5fca5..c9f9862870 100644 --- a/fuel-vm/src/tests/mod.rs +++ b/fuel-vm/src/tests/mod.rs @@ -12,6 +12,7 @@ mod backtrace; mod blockchain; mod cgas; mod code_coverage; +mod coins; mod contract; mod crypto; mod encoding; diff --git a/fuel-vm/src/tests/outputs.rs b/fuel-vm/src/tests/outputs.rs index 6f6da24540..e1c40aa372 100644 --- a/fuel-vm/src/tests/outputs.rs +++ b/fuel-vm/src/tests/outputs.rs @@ -1,7 +1,6 @@ use alloc::{ borrow::ToOwned, vec, - vec::Vec, }; use crate::{ @@ -9,7 +8,6 @@ use crate::{ field::Outputs, *, }, - script_with_data_offset, util::test_helpers::{ find_change, TestBuilder, @@ -24,7 +22,6 @@ use fuel_tx::{ ConsensusParameters, Witness, }; -use fuel_types::canonical::Serialize; use rand::{ rngs::StdRng, Rng, @@ -180,537 +177,3 @@ fn correct_change_is_provided_for_coin_outputs_create() { assert_eq!(change, input_amount - spend_amount); } - -#[test] -fn change_is_reduced_by_external_transfer() { - let mut rng = StdRng::seed_from_u64(2322u64); - let input_amount = 1000; - let transfer_amount: Word = 400; - let gas_price = 0; - let gas_limit = 1_000_000; - let asset_id: AssetId = rng.gen(); - - // simple dummy contract for transferring value to - let contract_code = vec![op::ret(RegId::ONE)]; - - let mut test_context = TestBuilder::new(2322u64); - let contract_id = test_context - .setup_contract(contract_code, None, None) - .contract_id; - - // setup script for transfer - let (script, _) = script_with_data_offset!( - data_offset, - vec![ - // set reg 0x10 to contract id - op::movi(0x10, data_offset as Immediate18), - // set reg 0x11 to transfer amount - op::movi(0x11, transfer_amount as Immediate18), - // set reg 0x12 to asset id - op::movi(0x12, (data_offset + 32) as Immediate18), - // transfer to contract ID at 0x10, the amount of coins at 0x11, of the asset - // id at 0x12 - op::tr(0x10, 0x11, 0x12), - op::ret(RegId::ONE), - ], - test_context.get_tx_params().tx_offset() - ); - - let script_data = [contract_id.as_ref(), asset_id.as_ref()] - .into_iter() - .flatten() - .copied() - .collect(); - - // execute and get change - let change = test_context - .start_script(script, script_data) - .gas_price(gas_price) - .script_gas_limit(gas_limit) - .coin_input(asset_id, input_amount) - .contract_input(contract_id) - .change_output(asset_id) - .contract_output(&contract_id) - .execute_get_change(asset_id); - - assert_eq!(change, input_amount - transfer_amount); -} - -#[test] -fn change_is_not_reduced_by_external_transfer_on_revert() { - let mut rng = StdRng::seed_from_u64(2322u64); - let input_amount = 1000; - // attempt overspend to cause a revert - let transfer_amount: Word = input_amount + 100; - let gas_price = 0; - let gas_limit = 1_000_000; - let asset_id: AssetId = rng.gen(); - - // setup state for test - // simple dummy contract for transferring value to - let contract_code = vec![op::ret(RegId::ONE)]; - - let mut test_context = TestBuilder::new(2322u64); - let contract_id = test_context - .setup_contract(contract_code, None, None) - .contract_id; - - // setup script for transfer - let (script, _) = script_with_data_offset!( - data_offset, - vec![ - // set reg 0x10 to contract id - op::movi(0x10, data_offset), - // set reg 0x11 to transfer amount - op::movi(0x11, transfer_amount as Immediate18), - // set reg 0x12 to asset id - op::movi(0x12, data_offset + 32), - // transfer to contract ID at 0x10, the amount of coins at 0x11, of the asset - // id at 0x12 - op::tr(0x10, 0x11, 0x12), - op::ret(RegId::ONE), - ], - test_context.get_tx_params().tx_offset() - ); - - let script_data = [contract_id.as_ref(), asset_id.as_ref()] - .into_iter() - .flatten() - .copied() - .collect(); - - // execute and get change - let change = test_context - .start_script(script, script_data) - .gas_price(gas_price) - .script_gas_limit(gas_limit) - .coin_input(asset_id, input_amount) - .contract_input(contract_id) - .change_output(asset_id) - .contract_output(&contract_id) - .execute_get_change(asset_id); - - assert_eq!(change, input_amount); -} - -#[test] -fn zero_amount_transfer_reverts() { - let mut rng = StdRng::seed_from_u64(2322u64); - let gas_price = 0; - let gas_limit = 1_000_000; - let asset_id: AssetId = rng.gen(); - - // setup state for test - // simple dummy contract for transferring value to - let contract_code = vec![op::ret(RegId::ONE)]; - - let mut test_context = TestBuilder::new(2322u64); - let contract_id = test_context - .setup_contract(contract_code, None, None) - .contract_id; - - // setup script for transfer - let (script, _) = script_with_data_offset!( - data_offset, - vec![ - // set reg 0x10 to contract id - op::movi(0x10, data_offset), - // set reg 0x12 to asset id - op::movi(0x11, data_offset + ContractId::LEN as u32), - // transfer to contract id at 0x10, amount of coins is zero, asset id at 0x11 - op::tr(0x10, RegId::ZERO, 0x11), - op::ret(RegId::ONE), - ], - test_context.get_tx_params().tx_offset() - ); - - let script_data = [contract_id.as_ref(), asset_id.as_ref()] - .into_iter() - .flatten() - .copied() - .collect(); - - // execute and get receipts - let result = test_context - .start_script(script, script_data) - .gas_price(gas_price) - .script_gas_limit(gas_limit) - .coin_input(asset_id, 0) - .contract_input(contract_id) - .change_output(asset_id) - .contract_output(&contract_id) - .execute(); - - let receipts = result.receipts(); - - if let Some(Receipt::Panic { reason, .. }) = receipts.first() { - assert_eq!(reason.reason(), &PanicReason::TransferZeroCoins); - } else { - panic!("Expected a panic receipt"); - } -} - -#[test] -fn zero_amount_transfer_out_reverts() { - let rng = &mut StdRng::seed_from_u64(2322u64); - - // the initial external (coin) balance - let external_balance = 1_000_000; - // the amount to transfer out from external balance - let gas_price = 0; - let gas_limit = 1_000_000; - let asset_id: AssetId = rng.gen(); - let owner: Address = rng.gen(); - - let (script, _) = script_with_data_offset!( - data_offset, - vec![ - // load amount of coins to 0x10 - op::movi(0x10, data_offset), - op::lw(0x10, 0x10, 0), - // load asset id to 0x11 - op::movi(0x11, data_offset), - // load address to 0x12 - op::movi(0x12, data_offset + 32), - // call contract without any tokens to transfer in or out - op::tro(0x12, RegId::ZERO, RegId::ZERO, 0x11), - op::ret(RegId::ONE), - ], - TxParameters::DEFAULT.tx_offset() - ); - - let script_data: Vec = [asset_id.as_ref(), owner.as_ref()] - .into_iter() - .flatten() - .copied() - .collect(); - - // execute and get receipts - let result = TestBuilder::new(2322u64) - .start_script(script, script_data) - .gas_price(gas_price) - .script_gas_limit(gas_limit) - .coin_input(asset_id, external_balance) - .variable_output(asset_id) - .change_output(asset_id) - .execute(); - - let receipts = result.receipts(); - - if let Some(Receipt::Panic { reason, .. }) = receipts.first() { - assert_eq!(reason.reason(), &PanicReason::TransferZeroCoins); - } else { - panic!("Expected a panic receipt"); - } -} - -#[test] -fn variable_output_set_by_external_transfer_out() { - let rng = &mut StdRng::seed_from_u64(2322u64); - - // the initial external (coin) balance - let external_balance = 1_000_000; - // the amount to transfer out from external balance - let transfer_amount: Word = 600; - let gas_price = 0; - let gas_limit = 1_000_000; - let asset_id: AssetId = rng.gen(); - let owner: Address = rng.gen(); - - let (script, _) = script_with_data_offset!( - data_offset, - vec![ - // load amount of coins to 0x10 - op::movi(0x10, data_offset), - op::lw(0x10, 0x10, 0), - // load asset id to 0x11 - op::movi(0x11, data_offset + 8), - // load address to 0x12 - op::movi(0x12, data_offset + 40), - // load output index (0) to 0x13 - op::move_(0x13, RegId::ZERO), - // call contract without any tokens to transfer in - op::tro(0x12, 0x13, 0x10, 0x11), - op::ret(RegId::ONE), - ], - TxParameters::DEFAULT.tx_offset() - ); - - let script_data: Vec = [ - transfer_amount.to_be_bytes().as_ref(), - asset_id.as_ref(), - owner.as_ref(), - ] - .into_iter() - .flatten() - .copied() - .collect(); - - // create and run the tx - let result = TestBuilder::new(2322u64) - .start_script(script, script_data) - .gas_price(gas_price) - .script_gas_limit(gas_limit) - .coin_input(asset_id, external_balance) - .variable_output(asset_id) - .change_output(asset_id) - .execute(); - - let outputs = result.tx().outputs(); - let receipts = result.receipts(); - - assert!(matches!( - outputs[0], Output::Variable { amount, to, asset_id } - if amount == transfer_amount - && to == owner - && asset_id == asset_id - )); - - assert!(matches!( - outputs[1], Output::Change {amount, asset_id, .. } - if amount == external_balance - transfer_amount - && asset_id == asset_id - )); - - assert!(receipts - .iter() - .any(|r| matches!(r, Receipt::TransferOut { .. }))); -} - -#[test] -fn variable_output_not_set_by_external_transfer_out_on_revert() { - let rng = &mut StdRng::seed_from_u64(2322u64); - - // the initial external (coin) balance (set to less than transfer amount to - // cause a revert) - let external_balance = 100; - // the amount to transfer out from external balance - let transfer_amount: Word = 600; - let gas_price = 0; - let gas_limit = 1_000_000; - let asset_id: AssetId = rng.gen(); - let owner: Address = rng.gen(); - let tx_params = TxParameters::default(); - - let (script, _) = script_with_data_offset!( - data_offset, - vec![ - // load amount of coins to 0x10 - op::movi(0x10, data_offset), - op::lw(0x10, 0x10, 0), - // load asset id to 0x11 - op::movi(0x11, data_offset + 8), - // load address to 0x12 - op::movi(0x12, data_offset + 40), - // load output index (0) to 0x13 - op::move_(0x13, RegId::ZERO), - // call contract without any tokens to transfer in - op::tro(0x12, 0x13, 0x10, 0x11), - op::ret(RegId::ONE), - ], - tx_params.tx_offset() - ); - - let script_data: Vec = [ - transfer_amount.to_be_bytes().as_ref(), - asset_id.as_ref(), - owner.as_ref(), - ] - .into_iter() - .flatten() - .copied() - .collect(); - - // create and run the tx - let result = TestBuilder::new(2322u64) - .start_script(script, script_data) - .gas_price(gas_price) - .script_gas_limit(gas_limit) - .coin_input(asset_id, external_balance) - .variable_output(asset_id) - .change_output(asset_id) - .execute(); - - let outputs = result.tx().outputs(); - let receipts = result.receipts(); - - assert!(matches!( - outputs[0], Output::Variable { amount, .. } if amount == 0 - )); - - // full input amount is converted into change - assert!(matches!( - outputs[1], Output::Change {amount, asset_id, .. } - if amount == external_balance - && asset_id == asset_id - )); - - // TransferOut receipt should not be present - assert!(!receipts - .iter() - .any(|r| matches!(r, Receipt::TransferOut { .. }))); -} - -#[test] -fn variable_output_set_by_internal_contract_transfer_out() { - let rng = &mut StdRng::seed_from_u64(2322u64); - - // the initial contract balance - let internal_balance = 1_000_000; - // the amount to transfer out of a contract - let transfer_amount: Word = 600; - let gas_price = 0; - let gas_limit = 1_000_000; - let asset_id: AssetId = rng.gen(); - let owner: Address = rng.gen(); - - // setup state for test - let contract_code = vec![ - // load amount of coins to 0x10 - op::addi(0x10, RegId::FP, CallFrame::a_offset() as Immediate12), - op::lw(0x10, 0x10, 0), - // load asset id to 0x11 - op::addi(0x11, RegId::FP, CallFrame::b_offset() as Immediate12), - op::lw(0x11, 0x11, 0), - // load address to 0x12 - op::addi(0x12, 0x11, 32 as Immediate12), - // load output index (0) to 0x13 - op::move_(0x13, RegId::ZERO), - op::tro(0x12, 0x13, 0x10, 0x11), - op::ret(RegId::ONE), - ]; - let mut test_context = TestBuilder::new(2322u64); - let contract_id = test_context - .setup_contract(contract_code, Some((asset_id, internal_balance)), None) - .contract_id; - - let (script, data_offset) = script_with_data_offset!( - data_offset, - vec![ - // set reg 0x10 to call data - op::movi(0x10, (data_offset + 64) as Immediate18), - // set reg 0x11 to transfer amount - op::move_(0x11, RegId::CGAS), - // call contract without any tokens to transfer in (3rd arg arbitrary when - // 2nd is zero) - op::call(0x10, RegId::ZERO, RegId::ZERO, 0x11), - op::ret(RegId::ONE), - ], - test_context.get_tx_params().tx_offset() - ); - - let script_data: Vec = [ - asset_id.as_ref(), - owner.as_ref(), - Call::new(contract_id, transfer_amount, data_offset as Word) - .to_bytes() - .as_ref(), - ] - .into_iter() - .flatten() - .copied() - .collect(); - - // create and run the tx - let result = test_context - .start_script(script, script_data) - .gas_price(gas_price) - .script_gas_limit(gas_limit) - .fee_input() - .contract_input(contract_id) - .variable_output(asset_id) - .contract_output(&contract_id) - .execute(); - - let outputs = result.tx().outputs(); - let receipts = result.receipts(); - - let output = Output::variable(owner, transfer_amount, asset_id); - - assert_eq!(output, outputs[0]); - assert!(receipts - .iter() - .any(|r| matches!(r, Receipt::TransferOut { .. }))); -} - -#[test] -fn variable_output_not_increased_by_contract_transfer_out_on_revert() { - let rng = &mut StdRng::seed_from_u64(2322u64); - - // the initial contract balance (set to zero so TRO will intentionally fail) - let internal_balance = 0; - // the amount to transfer out of a contract - let transfer_amount: Word = 600; - let gas_price = 0; - let gas_limit = 1_000_000; - let asset_id: AssetId = rng.gen(); - let owner: Address = rng.gen(); - - // setup state for test - let contract_code = vec![ - // load amount of coins to 0x10 - op::addi(0x10, RegId::FP, CallFrame::a_offset() as Immediate12), - op::lw(0x10, 0x10, 0), - // load asset id to 0x11 - op::addi(0x11, RegId::FP, CallFrame::b_offset() as Immediate12), - op::lw(0x11, 0x11, 0), - // load to address to 0x12 - op::addi(0x12, 0x11, 32 as Immediate12), - // load output index (0) to 0x13 - op::move_(0x13, RegId::ZERO), - op::tro(0x12, 0x13, 0x10, 0x11), - op::ret(RegId::ONE), - ]; - - let mut test_context = TestBuilder::new(2322u64); - let contract_id = test_context - .setup_contract(contract_code, Some((asset_id, internal_balance)), None) - .contract_id; - - let (script, data_offset) = script_with_data_offset!( - data_offset, - vec![ - // set reg 0x10 to call data - op::movi(0x10, data_offset + 64), - // call contract without any tokens to transfer in - op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS), - op::ret(RegId::ONE), - ], - test_context.get_tx_params().tx_offset() - ); - - let script_data: Vec = [ - asset_id.as_ref(), - owner.as_ref(), - Call::new(contract_id, transfer_amount, data_offset as Word) - .to_bytes() - .as_ref(), - ] - .into_iter() - .flatten() - .copied() - .collect(); - - // create and run the tx - let result = test_context - .start_script(script, script_data) - .gas_price(gas_price) - .script_gas_limit(gas_limit) - .fee_input() - .contract_input(contract_id) - .variable_output(asset_id) - .contract_output(&contract_id) - .execute(); - - let outputs = result.tx().outputs(); - let receipts = result.receipts(); - - assert!(matches!( - outputs[0], Output::Variable { amount, .. } if amount == 0 - )); - - // TransferOut receipt should not be present - assert!(!receipts - .iter() - .any(|r| matches!(r, Receipt::TransferOut { .. }))); -} From ca3b1fa0cebfd95f7d489c129af6959e1e134e61 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Wed, 19 Jun 2024 12:51:41 +0300 Subject: [PATCH 2/6] Fix no_std+alloc --- fuel-vm/src/tests/coins.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fuel-vm/src/tests/coins.rs b/fuel-vm/src/tests/coins.rs index 573d64bf61..7439d1a520 100644 --- a/fuel-vm/src/tests/coins.rs +++ b/fuel-vm/src/tests/coins.rs @@ -1,3 +1,8 @@ +use alloc::{ + vec, + vec::Vec, +}; + use rstest::rstest; use test_case::test_case; From 7dce12afc670dbb0cea6ff3e7af91c92d4bc7374 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Wed, 19 Jun 2024 13:02:13 +0300 Subject: [PATCH 3/6] Clippy --- fuel-vm/src/tests/coins.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/fuel-vm/src/tests/coins.rs b/fuel-vm/src/tests/coins.rs index 7439d1a520..e8699b27aa 100644 --- a/fuel-vm/src/tests/coins.rs +++ b/fuel-vm/src/tests/coins.rs @@ -252,6 +252,8 @@ fn mint_burn_bounds>( assert_eq!(extract_novalue(&run(test_context, contract_id)), result); } +type MintOrBurnOpcode = fn(RegId, RegId) -> Instruction; + #[test_case(vec![(op::mint, 0, 0)] => RunResult::Success(()); "Mint 0")] #[test_case(vec![(op::burn, 0, 0)] => RunResult::Success(()); "Burn 0")] #[test_case(vec![(op::mint, 100, 0)] => RunResult::Success(()); "Mint 100")] @@ -264,9 +266,7 @@ fn mint_burn_bounds>( #[test_case(vec![(op::burn, Word::MAX, 0)] => RunResult::Panic(PanicReason::NotEnoughBalance); "Burn nonexisting Word::MAX")] #[test_case(vec![(op::mint, Word::MAX, 0), (op::mint, 1, 0)] => RunResult::Panic(PanicReason::BalanceOverflow); "Mint overflow")] #[test_case(vec![(op::mint, Word::MAX, 0), (op::burn, 1, 0), (op::mint, 2, 0)] => RunResult::Panic(PanicReason::BalanceOverflow); "Mint,Burn,Mint overflow")] -fn mint_burn_single_sequence( - seq: Vec<(fn(RegId, RegId) -> Instruction, Word, u8)>, -) -> RunResult<()> { +fn mint_burn_single_sequence(seq: Vec<(MintOrBurnOpcode, Word, u8)>) -> RunResult<()> { let reg_len: u8 = 0x10; let reg_mint_amount: u8 = 0x11; @@ -651,23 +651,23 @@ fn transfer_to_output( let result = extract_result(&receipts, first_tro); if let Some(Output::Variable { - to, - amount, - asset_id, + to: var_to, + amount: var_amount, + asset_id: var_asset_id, }) = tx.outputs().get(to_index as usize).copied() { if result.is_ok() { - assert_eq!(amount, amount, "Transfer amount is wrong"); - assert_eq!(asset_id, asset_id, "Transfer asset id is wrong"); + assert_eq!(var_amount, amount, "Transfer amount is wrong"); + assert_eq!(var_asset_id, asset_id, "Transfer asset id is wrong"); } else { assert_eq!( - to, + var_to, Address::zeroed(), "Transfer target should be zeroed on failure" ); - assert_eq!(amount, 0, "Transfer amount should be 0 on failure"); + assert_eq!(var_amount, 0, "Transfer amount should be 0 on failure"); assert_eq!( - asset_id, + var_asset_id, AssetId::zeroed(), "Transfer asset id should be zeroed on failure" ); From e97e3b80f0fc955fd8695af186ac43b73c55f42c Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Wed, 19 Jun 2024 13:02:39 +0300 Subject: [PATCH 4/6] Correct a test name --- fuel-vm/src/tests/coins.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuel-vm/src/tests/coins.rs b/fuel-vm/src/tests/coins.rs index e8699b27aa..6261fb70c7 100644 --- a/fuel-vm/src/tests/coins.rs +++ b/fuel-vm/src/tests/coins.rs @@ -788,7 +788,7 @@ const M: Word = Word::MAX; #[test_case(0, 1, 0, 0, 2 => ((0, 1, 0), RunResult::Panic(PanicReason::NotEnoughBalance)); "Fwd 1 over src balance")] #[test_case(0, 0, 0, 0, M => ((0, 0, 0), RunResult::Panic(PanicReason::NotEnoughBalance)); "Fwd max over empty src balance")] #[test_case(0, 1, 0, 0, M => ((0, 1, 0), RunResult::Panic(PanicReason::NotEnoughBalance)); "Fwd max over src balance")] -fn call_forwarding_internal( +fn call_forwarding( balance_in: Word, balance_src: Word, balance_dst: Word, From 350508506b9b2339ebaf03e6d1a07854e3f5fc47 Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Mon, 24 Jun 2024 13:57:56 +0300 Subject: [PATCH 5/6] Address PR comment --- fuel-vm/src/tests/coins.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/fuel-vm/src/tests/coins.rs b/fuel-vm/src/tests/coins.rs index 6261fb70c7..127dc78f5d 100644 --- a/fuel-vm/src/tests/coins.rs +++ b/fuel-vm/src/tests/coins.rs @@ -288,10 +288,15 @@ fn mint_burn_single_sequence(seq: Vec<(MintOrBurnOpcode, Word, u8)>) -> RunResul extract_novalue(&run(test_context, contract_id)) } -#[test_case(vec![false] => RunResult::Panic(PanicReason::NotEnoughBalance); "Burn")] -#[test_case(vec![true, false, false] => RunResult::Panic(PanicReason::NotEnoughBalance); "Mint,Burn,Burn")] -#[test_case(vec![true, true, false] => RunResult::Success(1); "Mint,Mint,Burn")] -fn mint_burn_many_calls_sequence(seq: Vec) -> RunResult { +enum MintOrBurn { + Mint, + Burn, +} + +#[test_case(vec![MintOrBurn::Burn] => RunResult::Panic(PanicReason::NotEnoughBalance); "Burn")] +#[test_case(vec![MintOrBurn::Mint, MintOrBurn::Burn, MintOrBurn::Burn] => RunResult::Panic(PanicReason::NotEnoughBalance); "Mint,Burn,Burn")] +#[test_case(vec![MintOrBurn::Mint, MintOrBurn::Mint, MintOrBurn::Burn] => RunResult::Success(1); "Mint,Mint,Burn")] +fn mint_burn_many_calls_sequence(seq: Vec) -> RunResult { let reg_len: u8 = 0x10; let reg_jump: u8 = 0x11; @@ -311,7 +316,13 @@ fn mint_burn_many_calls_sequence(seq: Vec) -> RunResult { for instr in seq { let script_ops = vec![ - op::movi(reg_jump, if instr { 0 } else { 2 }), + op::movi( + reg_jump, + match instr { + MintOrBurn::Mint => 0, + MintOrBurn::Burn => 2, + }, + ), op::gtf_args(0x10, RegId::ZERO, GTFArgs::ScriptData), op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS), op::ret(RegId::ONE), From 1e93295cfdc29661c8cdedf9def3f4fd9db9577d Mon Sep 17 00:00:00 2001 From: Hannes Karppila Date: Mon, 24 Jun 2024 14:13:43 +0300 Subject: [PATCH 6/6] Address more PR feedback --- fuel-vm/src/tests/coins.rs | 121 +++++++++++++++++++++---------------- fuel-vm/src/util.rs | 2 +- 2 files changed, 70 insertions(+), 53 deletions(-) diff --git a/fuel-vm/src/tests/coins.rs b/fuel-vm/src/tests/coins.rs index 127dc78f5d..afdf9553ac 100644 --- a/fuel-vm/src/tests/coins.rs +++ b/fuel-vm/src/tests/coins.rs @@ -3,6 +3,7 @@ use alloc::{ vec::Vec, }; +use rand::Rng; use rstest::rstest; use test_case::test_case; @@ -387,7 +388,7 @@ fn transfer_to_contract_external( ]); let mut test_context = TestBuilder::new(1234u64); - let asset_id = AssetId::new([1; 32]); + let asset_id: AssetId = test_context.rng.gen(); let contract = test_context .setup_contract( @@ -427,21 +428,30 @@ fn transfer_to_contract_external( }) } -#[test_case(1, 0, 10, 0 => RunResult::Panic(PanicReason::TransferZeroCoins); "Cannot transfer 0 coins to empty other")] -#[test_case(1, 0, 10, 5 => RunResult::Panic(PanicReason::TransferZeroCoins); "Cannot transfer 0 coins to non-empty other")] -#[test_case(0, 0, 10, 0 => RunResult::Panic(PanicReason::TransferZeroCoins); "Cannot transfer 0 coins to self")] -#[test_case(1, 1, 10, 0 => RunResult::Success((9, 1)); "Can transfer 1 coins to other")] -#[test_case(0, 1, 10, 0 => RunResult::Success((10, 0)); "Can transfer 1 coins to self")] -#[test_case(1, 11, 10, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "Cannot transfer just over balance coins to other")] -#[test_case(0, 11, 10, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "Cannot transfer just over balance coins to self")] -#[test_case(1, Word::MAX, 0, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "Cannot transfer max over balance coins to other")] -#[test_case(0, Word::MAX, 0, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "Cannot transfer max over balance coins to self")] -#[test_case(1, 1, 1, Word::MAX => RunResult::Panic(PanicReason::BalanceOverflow); "Cannot overflow balance of other contract")] -#[test_case(0, Word::MAX, Word::MAX, 0 => RunResult::Success((Word::MAX, 0)); "Can transfer Word::MAX coins to self")] -#[test_case(1, Word::MAX, Word::MAX, 0 => RunResult::Success((0, Word::MAX)); "Can transfer Word::MAX coins to empty other")] -#[test_case(2, 1, 1, 0 => RunResult::Panic(PanicReason::ContractNotInInputs); "Transfer target not in inputs")] +enum TrTo { + /// Transfer to self + This, + /// Transfer to other contract + Other, + /// Transfer to non-existing contract + NonExisting, +} + +#[test_case(TrTo::Other, 0, 10, 0 => RunResult::Panic(PanicReason::TransferZeroCoins); "Cannot transfer 0 coins to empty other")] +#[test_case(TrTo::Other, 0, 10, 5 => RunResult::Panic(PanicReason::TransferZeroCoins); "Cannot transfer 0 coins to non-empty other")] +#[test_case(TrTo::This, 0, 10, 0 => RunResult::Panic(PanicReason::TransferZeroCoins); "Cannot transfer 0 coins to self")] +#[test_case(TrTo::Other, 1, 10, 0 => RunResult::Success((9, 1)); "Can transfer 1 coins to other")] +#[test_case(TrTo::This, 1, 10, 0 => RunResult::Success((10, 0)); "Can transfer 1 coins to self")] +#[test_case(TrTo::Other, 11, 10, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "Cannot transfer just over balance coins to other")] +#[test_case(TrTo::This, 11, 10, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "Cannot transfer just over balance coins to self")] +#[test_case(TrTo::Other, Word::MAX, 0, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "Cannot transfer max over balance coins to other")] +#[test_case(TrTo::This, Word::MAX, 0, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "Cannot transfer max over balance coins to self")] +#[test_case(TrTo::Other, 1, 1, Word::MAX => RunResult::Panic(PanicReason::BalanceOverflow); "Cannot overflow balance of other contract")] +#[test_case(TrTo::This, Word::MAX, Word::MAX, 0 => RunResult::Success((Word::MAX, 0)); "Can transfer Word::MAX coins to self")] +#[test_case(TrTo::Other, Word::MAX, Word::MAX, 0 => RunResult::Success((0, Word::MAX)); "Can transfer Word::MAX coins to empty other")] +#[test_case(TrTo::NonExisting, 1, 1, 0 => RunResult::Panic(PanicReason::ContractNotInInputs); "Transfer target not in inputs")] fn transfer_to_contract_internal( - to: usize, // 0 = self, 1 = other, 2 = non-existing + to: TrTo, amount: Word, balance: Word, other_balance: Word, @@ -466,7 +476,7 @@ fn transfer_to_contract_internal( ]); let mut test_context = TestBuilder::new(1234u64); - let asset_id = AssetId::new([1; 32]); + let asset_id: AssetId = test_context.rng.gen(); let this_contract = test_context .setup_contract(ops, Some((asset_id, balance)), None) @@ -490,9 +500,9 @@ fn transfer_to_contract_internal( .flatten() .copied() .chain(match to { - 0 => this_contract.to_bytes(), - 1 => other_contract.to_bytes(), - _ => vec![1u8; 32], // Non-existing contract + TrTo::This => this_contract.to_bytes(), + TrTo::Other => other_contract.to_bytes(), + TrTo::NonExisting => vec![1u8; 32], // Non-existing contract }) .chain(asset_id.to_bytes()) .collect(); @@ -552,7 +562,7 @@ fn transfer_to_contract_bounds( ]); let mut test_context = TestBuilder::new(1234u64); - let asset_id = AssetId::new([1; 32]); + let asset_id: AssetId = test_context.rng.gen(); let this_contract = test_context .setup_contract(ops, Some((asset_id, Word::MAX)), None) @@ -582,26 +592,36 @@ fn transfer_to_contract_bounds( extract_novalue(result.receipts()) } -#[test_case(false, 0, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(external) Cannot transfer 0 coins to non-Variable output")] -#[test_case(false, 1, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(external) Cannot transfer 0 coins to valid output")] -#[test_case(false, 9, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(external) Cannot transfer 0 coins to non-existing output")] -#[test_case(false, 1, 1, 10 => RunResult::Success((1, 9)); "(external) Can transfer 1 coins")] -#[test_case(false, 1, 11, 10 => RunResult::Panic(PanicReason::NotEnoughBalance); "(external) Cannot transfer just over balance coins")] -#[test_case(false, 1, Word::MAX, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "(external) Cannot transfer max over balance coins")] -#[test_case(false, 1, Word::MAX, Word::MAX => RunResult::Success((Word::MAX, 0)); "(external) Can transfer Word::MAX coins")] -#[test_case(false, 0, 1, 10 => RunResult::Panic(PanicReason::OutputNotFound); "(external) Target output is not Variable")] -#[test_case(false, 9, 1, 1 => RunResult::Panic(PanicReason::OutputNotFound); "(external) Target output doesn't exist")] -#[test_case(true, 0, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(internal) Cannot transfer 0 coins to non-Variable output")] -#[test_case(true, 1, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(internal) Cannot transfer 0 coins to valid output")] -#[test_case(true, 9, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(internal) Cannot transfer 0 coins to non-existing output")] -#[test_case(true, 1, 1, 10 => RunResult::Success((1, 9)); "(internal) Can transfer 1 coins")] -#[test_case(true, 1, 11, 10 => RunResult::Panic(PanicReason::NotEnoughBalance); "(internal) Cannot transfer just over balance coins")] -#[test_case(true, 1, Word::MAX, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "(internal) Cannot transfer max over balance coins")] -#[test_case(true, 1, Word::MAX, Word::MAX => RunResult::Success((Word::MAX, 0)); "(internal) Can transfer Word::MAX coins")] -#[test_case(true, 0, 1, 10 => RunResult::Panic(PanicReason::OutputNotFound); "(internal) Target output is not Variable")] -#[test_case(true, 9, 1, 1 => RunResult::Panic(PanicReason::OutputNotFound); "(internal) Target output doesn't exist")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Ctx { + Internal, + External, +} + +const M: Word = Word::MAX; + +#[test_case(Ctx::External, 0, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(external) Cannot transfer 0 coins to non-Variable output")] +#[test_case(Ctx::External, 1, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(external) Cannot transfer 0 coins to valid output")] +#[test_case(Ctx::External, 9, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(external) Cannot transfer 0 coins to non-existing output")] +#[test_case(Ctx::External, 1, 1, 10 => RunResult::Success((1, 9)); "(external) Can transfer 1 coins")] +#[test_case(Ctx::External, 1, 11, 10 => RunResult::Panic(PanicReason::NotEnoughBalance); "(external) Cannot transfer just over balance coins")] +#[test_case(Ctx::External, 1, M, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "(external) Cannot transfer max over balance coins")] +#[test_case(Ctx::External, 1, M, M => RunResult::Success((Word::MAX, 0)); "(external) Can transfer Word::MAX coins")] +#[test_case(Ctx::External, 0, 1, 10 => RunResult::Panic(PanicReason::OutputNotFound); "(external) Target output is not Variable")] +#[test_case(Ctx::External, 9, 1, 1 => RunResult::Panic(PanicReason::OutputNotFound); "(external) Target output doesn't exist")] +#[test_case(Ctx::External, M, 1, 1 => RunResult::Panic(PanicReason::OutputNotFound); "(external) Target output is Word::MAX")] +#[test_case(Ctx::Internal, 0, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(internal) Cannot transfer 0 coins to non-Variable output")] +#[test_case(Ctx::Internal, 1, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(internal) Cannot transfer 0 coins to valid output")] +#[test_case(Ctx::Internal, 9, 0, 10 => RunResult::Panic(PanicReason::TransferZeroCoins); "(internal) Cannot transfer 0 coins to non-existing output")] +#[test_case(Ctx::Internal, 1, 1, 10 => RunResult::Success((1, 9)); "(internal) Can transfer 1 coins")] +#[test_case(Ctx::Internal, 1, 11, 10 => RunResult::Panic(PanicReason::NotEnoughBalance); "(internal) Cannot transfer just over balance coins")] +#[test_case(Ctx::Internal, 1, M, 0 => RunResult::Panic(PanicReason::NotEnoughBalance); "(internal) Cannot transfer max over balance coins")] +#[test_case(Ctx::Internal, 1, M, M => RunResult::Success((Word::MAX, 0)); "(internal) Can transfer Word::MAX coins")] +#[test_case(Ctx::Internal, 0, 1, 10 => RunResult::Panic(PanicReason::OutputNotFound); "(internal) Target output is not Variable")] +#[test_case(Ctx::Internal, 9, 1, 1 => RunResult::Panic(PanicReason::OutputNotFound); "(internal) Target output doesn't exist")] +#[test_case(Ctx::Internal, M, 1, 1 => RunResult::Panic(PanicReason::OutputNotFound); "(internal) Target output is Word::MAX")] fn transfer_to_output( - internal: bool, + ctx: Ctx, to_index: Word, // 1 = the variable output amount: Word, balance: Word, @@ -621,20 +641,19 @@ fn transfer_to_output( ]); let mut test_context = TestBuilder::new(1234u64); - let asset_id = AssetId::new([1; 32]); + let asset_id: AssetId = test_context.rng.gen(); let contract_id = test_context .setup_contract(ops.clone(), Some((asset_id, balance)), None) .contract_id; - let script_ops = if internal { - vec![ + let script_ops = match ctx { + Ctx::Internal => vec![ op::gtf_args(0x10, RegId::ZERO, GTFArgs::ScriptData), op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS), op::ret(RegId::ONE), - ] - } else { - ops + ], + Ctx::External => ops, }; let script_data: Vec = [Call::new(contract_id, 0, 0).to_bytes().as_slice()] @@ -652,7 +671,7 @@ fn transfer_to_output( .contract_output(&contract_id) .variable_output(asset_id); - if !internal { + if ctx == Ctx::External { builder = builder .coin_input(asset_id, balance) .change_output(asset_id); @@ -685,7 +704,7 @@ fn transfer_to_output( } } - if !result.is_ok() && !internal { + if !result.is_ok() && ctx == Ctx::External { assert_eq!( find_change(tx.outputs().to_vec(), asset_id), balance, @@ -696,7 +715,7 @@ fn transfer_to_output( result.map(|tr| { ( tr, - if internal { + if ctx == Ctx::Internal { test_context.get_contract_balance(&contract_id, &asset_id) } else { find_change(tx.outputs().to_vec(), asset_id) @@ -742,7 +761,7 @@ fn transfer_to_output_bounds( ]); let mut test_context = TestBuilder::new(1234u64); - let asset_id = AssetId::new([1; 32]); + let asset_id: AssetId = test_context.rng.gen(); let this_contract = test_context .setup_contract(ops, Some((asset_id, Word::MAX)), None) @@ -773,8 +792,6 @@ fn transfer_to_output_bounds( extract_novalue(result.receipts()) } -const M: Word = Word::MAX; - // Calls script -> src -> dst #[test_case(0, 0, 0, 0, 0 => ((0, 0, 0), RunResult::Success(())); "No coins moving, zero balances")] #[test_case(1, 1, 1, 0, 0 => ((1, 1, 1), RunResult::Success(())); "No coins moving, nonzero balances")] @@ -813,7 +830,7 @@ fn call_forwarding( let reg_asset_id_ptr: u8 = 0x14; let mut test_context = TestBuilder::new(1234u64); - let asset_id = AssetId::new([1; 32]); + let asset_id: AssetId = test_context.rng.gen(); // Setup the dst contract. This does nothing, just holds/receives the balance. let dst_contract = test_context diff --git a/fuel-vm/src/util.rs b/fuel-vm/src/util.rs index 788765ca1a..64d3ee18e9 100644 --- a/fuel-vm/src/util.rs +++ b/fuel-vm/src/util.rs @@ -176,7 +176,7 @@ pub mod test_helpers { } pub struct TestBuilder { - rng: StdRng, + pub rng: StdRng, gas_price: Word, max_fee_limit: Word, script_gas_limit: Word,