diff --git a/crates/sui-adapter-transactional-tests/tests/transfer_object/does_not_have_store.exp b/crates/sui-adapter-transactional-tests/tests/transfer_object/does_not_have_store.exp new file mode 100644 index 0000000000000..27453a65425c5 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/transfer_object/does_not_have_store.exp @@ -0,0 +1,40 @@ +processed 10 tasks + +init: +A: object(100), B: object(101) + +task 1 'publish'. lines 8-29: +created: object(105) +written: object(104) + +task 2 'run'. lines 31-31: +created: object(107) +written: object(106) + +task 3 'view-object'. lines 33-33: +Owner: Account Address ( A ) +Contents: test::m::S {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(107)}}, version: 1u64}} + +task 4 'transfer-object'. lines 35-35: +Error: Transaction Effects Status: MiscellaneousError +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: TransferObjectWithoutPublicTransfer, source: None } } + +task 5 'view-object'. lines 37-40: +Owner: Account Address ( A ) +Contents: test::m::S {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(107)}}, version: 2u64}} + +task 6 'run'. lines 42-42: +created: object(110) +written: object(109) + +task 7 'view-object'. lines 44-44: +Owner: Account Address ( A ) +Contents: test::m::Cup {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(110)}}, version: 1u64}} + +task 8 'transfer-object'. lines 46-46: +Error: Transaction Effects Status: MiscellaneousError +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: TransferObjectWithoutPublicTransfer, source: None } } + +task 9 'view-object'. lines 48-48: +Owner: Account Address ( A ) +Contents: test::m::Cup {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(110)}}, version: 2u64}} diff --git a/crates/sui-adapter-transactional-tests/tests/transfer_object/does_not_have_store.move b/crates/sui-adapter-transactional-tests/tests/transfer_object/does_not_have_store.move new file mode 100644 index 0000000000000..884226bb61204 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/transfer_object/does_not_have_store.move @@ -0,0 +1,48 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// tests TransferObject should fail for an object _without_ public transfer + +//# init --accounts A B --addresses test=0x0 + +//# publish + +module test::m { + use sui::transfer; + use sui::tx_context::{Self, TxContext}; + use sui::id::VersionedID; + + struct S has key { id: VersionedID } + struct Cup has key { id: VersionedID } + + public entry fun mint_s(ctx: &mut TxContext) { + let id = tx_context::new_id(ctx); + transfer::transfer(S { id }, tx_context::sender(ctx)) + } + + public entry fun mint_cup(ctx: &mut TxContext) { + let id = tx_context::new_id(ctx); + transfer::transfer(Cup { id }, tx_context::sender(ctx)) + } +} + +// Mint S to A. Fail to transfer S from A to B, which should fail + +//# run test::m::mint_s --sender A + +//# view-object 107 + +//# transfer-object 107 --sender A --recipient B + +//# view-object 107 + + +// Mint Cup to A. Fail to transfer Cup from A to B + +//# run test::m::mint_cup --type-args test::m::S --sender A + +//# view-object 110 + +//# transfer-object 110 --sender A --recipient B + +//# view-object 110 diff --git a/crates/sui-adapter-transactional-tests/tests/transfer_object/has_store.exp b/crates/sui-adapter-transactional-tests/tests/transfer_object/has_store.exp new file mode 100644 index 0000000000000..ed79753f2d698 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/transfer_object/has_store.exp @@ -0,0 +1,38 @@ +processed 10 tasks + +init: +A: object(100), B: object(101) + +task 1 'publish'. lines 8-29: +created: object(105) +written: object(104) + +task 2 'run'. lines 31-31: +created: object(107) +written: object(106) + +task 3 'view-object'. lines 33-33: +Owner: Account Address ( A ) +Contents: test::m::S {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(107)}}, version: 1u64}} + +task 4 'transfer-object'. lines 35-35: +written: object(107), object(108) + +task 5 'view-object'. lines 37-40: +Owner: Account Address ( B ) +Contents: test::m::S {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(107)}}, version: 2u64}} + +task 6 'run'. lines 42-42: +created: object(110) +written: object(109) + +task 7 'view-object'. lines 44-44: +Owner: Account Address ( A ) +Contents: test::m::Cup {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(110)}}, version: 1u64}} + +task 8 'transfer-object'. lines 46-46: +written: object(110), object(111) + +task 9 'view-object'. lines 48-48: +Owner: Account Address ( B ) +Contents: test::m::Cup {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(110)}}, version: 2u64}} diff --git a/crates/sui-adapter-transactional-tests/tests/transfer_object/has_store.move b/crates/sui-adapter-transactional-tests/tests/transfer_object/has_store.move new file mode 100644 index 0000000000000..2898ece475126 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/transfer_object/has_store.move @@ -0,0 +1,48 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// tests TransferObject with an object with public transfer + +//# init --accounts A B --addresses test=0x0 + +//# publish + +module test::m { + use sui::transfer; + use sui::tx_context::{Self, TxContext}; + use sui::id::VersionedID; + + struct S has store, key { id: VersionedID } + struct Cup has store, key { id: VersionedID } + + public entry fun mint_s(ctx: &mut TxContext) { + let id = tx_context::new_id(ctx); + transfer::transfer(S { id }, tx_context::sender(ctx)) + } + + public entry fun mint_cup(ctx: &mut TxContext) { + let id = tx_context::new_id(ctx); + transfer::transfer(Cup { id }, tx_context::sender(ctx)) + } +} + +// Mint S to A. Transfer S from A to B + +//# run test::m::mint_s --sender A + +//# view-object 107 + +//# transfer-object 107 --sender A --recipient B + +//# view-object 107 + + +// Mint Cup to A. Transfer Cup from A to B + +//# run test::m::mint_cup --type-args test::m::S --sender A + +//# view-object 110 + +//# transfer-object 110 --sender A --recipient B + +//# view-object 110 diff --git a/crates/sui-adapter-transactional-tests/tests/transfer_object/immutable.exp b/crates/sui-adapter-transactional-tests/tests/transfer_object/immutable.exp new file mode 100644 index 0000000000000..4f6fbca1fc36f --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/transfer_object/immutable.exp @@ -0,0 +1,24 @@ +processed 6 tasks + +init: +A: object(100), B: object(101) + +task 1 'publish'. lines 8-22: +created: object(105) +written: object(104) + +task 2 'run'. lines 24-24: +created: object(107) +written: object(106) + +task 3 'view-object'. lines 26-26: +Owner: Immutable +Contents: test::m::S {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(107)}}, version: 1u64}} + +task 4 'transfer-object'. lines 28-28: +Error: Transaction Effects Status: MiscellaneousError +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: TransferUnowned, source: None } } + +task 5 'view-object'. lines 30-30: +Owner: Immutable +Contents: test::m::S {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(107)}}, version: 1u64}} diff --git a/crates/sui-adapter-transactional-tests/tests/transfer_object/immutable.move b/crates/sui-adapter-transactional-tests/tests/transfer_object/immutable.move new file mode 100644 index 0000000000000..afd2666d3b7fa --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/transfer_object/immutable.move @@ -0,0 +1,30 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// tests TransferObject should fail for an immutable object + +//# init --accounts A B --addresses test=0x0 + +//# publish + +module test::m { + use sui::transfer; + use sui::tx_context::{Self, TxContext}; + use sui::id::VersionedID; + + struct S has store, key { id: VersionedID } + struct Cup has store, key { id: VersionedID } + + public entry fun mint_s(ctx: &mut TxContext) { + let id = tx_context::new_id(ctx); + transfer::freeze_object(S { id }) + } +} + +//# run test::m::mint_s --sender A + +//# view-object 107 + +//# transfer-object 107 --sender A --recipient B + +//# view-object 107 diff --git a/crates/sui-adapter-transactional-tests/tests/transfer_object/package.exp b/crates/sui-adapter-transactional-tests/tests/transfer_object/package.exp new file mode 100644 index 0000000000000..57ed1431c3e23 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/transfer_object/package.exp @@ -0,0 +1,18 @@ +processed 5 tasks + +init: +A: object(100), B: object(101) + +task 1 'publish'. lines 8-10: +created: object(105) +written: object(104) + +task 2 'view-object'. lines 13-13: +105::m + +task 3 'transfer-object'. lines 15-15: +Error: Transaction Effects Status: MiscellaneousError +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: TransferUnowned, source: None } } + +task 4 'view-object'. lines 17-17: +105::m diff --git a/crates/sui-adapter-transactional-tests/tests/transfer_object/package.move b/crates/sui-adapter-transactional-tests/tests/transfer_object/package.move new file mode 100644 index 0000000000000..12f79a05efe18 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/transfer_object/package.move @@ -0,0 +1,17 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// tests TransferObject should fail for a package + +//# init --accounts A B --addresses test=0x0 + +//# publish --sender A + +module test::m {} + + +//# view-object 105 + +//# transfer-object 105 --sender A --recipient B + +//# view-object 105 diff --git a/crates/sui-adapter-transactional-tests/tests/transfer_object/quasi_shared.exp b/crates/sui-adapter-transactional-tests/tests/transfer_object/quasi_shared.exp new file mode 100644 index 0000000000000..2c43f50f3f3b8 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/transfer_object/quasi_shared.exp @@ -0,0 +1,28 @@ +processed 7 tasks + +init: +A: object(100), B: object(101) + +task 1 'publish'. lines 8-28: +created: object(105) +written: object(104) + +task 2 'run'. lines 30-30: +created: object(107) +written: object(106) + +task 3 'run'. lines 32-32: +created: object(109) +written: object(107), object(108) + +task 4 'view-object'. lines 34-34: +Owner: Object ID: ( fake(107) ) +Contents: test::m::Child {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(109)}}, version: 1u64}} + +task 5 'transfer-object'. lines 36-36: +Error: Transaction Effects Status: MiscellaneousError +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: TransferUnowned, source: None } } + +task 6 'view-object'. lines 38-38: +Owner: Object ID: ( fake(107) ) +Contents: test::m::Child {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(109)}}, version: 2u64}} diff --git a/crates/sui-adapter-transactional-tests/tests/transfer_object/quasi_shared.move b/crates/sui-adapter-transactional-tests/tests/transfer_object/quasi_shared.move new file mode 100644 index 0000000000000..0c31e147d0841 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/transfer_object/quasi_shared.move @@ -0,0 +1,38 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// tests TransferObject should fail for a quasi-shared object + +//# init --accounts A B --addresses test=0x0 + +//# publish + +module test::m { + use sui::transfer::{Self, ChildRef}; + use sui::tx_context::{Self, TxContext}; + use sui::id::VersionedID; + + struct S has key { id: VersionedID, children: vector> } + struct Child has key { id: VersionedID } + + public entry fun mint_s(ctx: &mut TxContext) { + let id = tx_context::new_id(ctx); + transfer::share_object(S { id, children: vector[] }) + } + + public entry fun mint_child(s: &mut S, ctx: &mut TxContext) { + let id = tx_context::new_id(ctx); + let child = transfer::transfer_to_object(Child { id }, s); + std::vector::push_back(&mut s.children, child) + } +} + +//# run test::m::mint_s + +//# run test::m::mint_child --args object(107) + +//# view-object 109 + +//# transfer-object 109 --sender A --recipient B + +//# view-object 109 diff --git a/crates/sui-adapter-transactional-tests/tests/transfer_object/shared.exp b/crates/sui-adapter-transactional-tests/tests/transfer_object/shared.exp new file mode 100644 index 0000000000000..55c8a6c565c1e --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/transfer_object/shared.exp @@ -0,0 +1,24 @@ +processed 6 tasks + +init: +A: object(100), B: object(101) + +task 1 'publish'. lines 8-21: +created: object(105) +written: object(104) + +task 2 'run'. lines 23-23: +created: object(107) +written: object(106) + +task 3 'view-object'. lines 25-25: +Owner: Shared +Contents: test::m::S {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(107)}}, version: 1u64}} + +task 4 'transfer-object'. lines 27-27: +Error: Transaction Effects Status: MiscellaneousError +Execution Error: ExecutionError: ExecutionError { inner: ExecutionErrorInner { kind: TransferUnowned, source: None } } + +task 5 'view-object'. lines 29-29: +Owner: Shared +Contents: test::m::S {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(107)}}, version: 2u64}} diff --git a/crates/sui-adapter-transactional-tests/tests/transfer_object/shared.move b/crates/sui-adapter-transactional-tests/tests/transfer_object/shared.move new file mode 100644 index 0000000000000..e03859236cdff --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/transfer_object/shared.move @@ -0,0 +1,29 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// tests TransferObject should fail for a shared object + +//# init --accounts A B --addresses test=0x0 + +//# publish + +module test::m { + use sui::transfer; + use sui::tx_context::{Self, TxContext}; + use sui::id::VersionedID; + + struct S has key { id: VersionedID } + + public entry fun mint_s(ctx: &mut TxContext) { + let id = tx_context::new_id(ctx); + transfer::share_object(S { id }) + } +} + +//# run test::m::mint_s + +//# view-object 107 + +//# transfer-object 107 --sender A --recipient B + +//# view-object 107 diff --git a/crates/sui-adapter-transactional-tests/tests/transfer_object/transfer_coin.exp b/crates/sui-adapter-transactional-tests/tests/transfer_object/transfer_coin.exp new file mode 100644 index 0000000000000..177fdbede5449 --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/transfer_object/transfer_coin.exp @@ -0,0 +1,15 @@ +processed 4 tasks + +init: +A: object(100), B: object(101) + +task 1 'view-object'. lines 8-8: +Owner: Account Address ( A ) +Contents: sui::coin::Coin {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(100)}}, version: 0u64}, balance: sui::balance::Balance {value: 100000u64}} + +task 2 'transfer-object'. lines 10-10: +written: object(100), object(104) + +task 3 'view-object'. lines 12-12: +Owner: Account Address ( B ) +Contents: sui::coin::Coin {id: sui::id::VersionedID {id: sui::id::UniqueID {id: sui::id::ID {bytes: fake(100)}}, version: 1u64}, balance: sui::balance::Balance {value: 100000u64}} diff --git a/crates/sui-adapter-transactional-tests/tests/transfer_object/transfer_coin.move b/crates/sui-adapter-transactional-tests/tests/transfer_object/transfer_coin.move new file mode 100644 index 0000000000000..ac419e0949f3a --- /dev/null +++ b/crates/sui-adapter-transactional-tests/tests/transfer_object/transfer_coin.move @@ -0,0 +1,12 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// tests TransferObject with a SUI coin + +//# init --accounts A B + +//# view-object 100 + +//# transfer-object 100 --sender A --recipient B + +//# view-object 100 diff --git a/crates/sui-transactional-test-runner/src/args.rs b/crates/sui-transactional-test-runner/src/args.rs index 7f470f882a209..3b36826218f28 100644 --- a/crates/sui-transactional-test-runner/src/args.rs +++ b/crates/sui-transactional-test-runner/src/args.rs @@ -39,10 +39,23 @@ pub struct ViewObjectCommand { pub id: u64, } +#[derive(Debug, clap::Parser)] +pub struct TransferObjectCommand { + pub id: u64, + #[clap(long = "recipient")] + pub recipient: String, + #[clap(long = "sender")] + pub sender: Option, + #[clap(long = "gas-budget")] + pub gas_budget: Option, +} + #[derive(Debug, clap::Parser)] pub enum SuiSubcommand { #[clap(name = "view-object")] ViewObject(ViewObjectCommand), + #[clap(name = "transfer-object")] + TransferObject(TransferObjectCommand), } #[derive(Debug)] diff --git a/crates/sui-transactional-test-runner/src/test_adapter.rs b/crates/sui-transactional-test-runner/src/test_adapter.rs index bb79b99584e74..cfef5b869503a 100644 --- a/crates/sui-transactional-test-runner/src/test_adapter.rs +++ b/crates/sui-transactional-test-runner/src/test_adapter.rs @@ -347,19 +347,24 @@ impl<'a> MoveTestAdapter<'a> for SuiTestAdapter<'a> { stop_line: _, data: _, } = task; - match command { - SuiSubcommand::ViewObject(ViewObjectCommand { id: fake_id }) => { - let id = match self.fake_to_real_object_id(fake_id) { + macro_rules! get_obj { + ($fake_id:ident) => {{ + let id = match self.fake_to_real_object_id($fake_id) { None => panic!( "task {}, lines {}-{}. Unbound fake id {}", - number, start_line, command_lines_stop, fake_id + number, start_line, command_lines_stop, $fake_id ), Some(res) => res, }; - let obj = match self.storage.get_object(&id) { - None => return Ok(Some(format!("No object at id {}", fake_id))), + match self.storage.get_object(&id) { + None => return Ok(Some(format!("No object at id {}", $fake_id))), Some(obj) => obj, - }; + } + }}; + } + match command { + SuiSubcommand::ViewObject(ViewObjectCommand { id: fake_id }) => { + let obj = get_obj!(fake_id); Ok(Some(match &obj.data { object::Data::Move(move_obj) => { let layout = move_obj @@ -389,6 +394,26 @@ impl<'a> MoveTestAdapter<'a> for SuiTestAdapter<'a> { } })) } + SuiSubcommand::TransferObject(TransferObjectCommand { + id: fake_id, + recipient, + sender, + gas_budget, + }) => { + let obj = get_obj!(fake_id); + let obj_ref = obj.compute_object_reference(); + let recipient = match self.accounts.get(&recipient) { + Some((recipient, _)) => *recipient, + None => panic!("Unbound account {}", recipient), + }; + let gas_budget = gas_budget.unwrap_or(GAS_VALUE_FOR_TESTING); + let transaction = self.sign_txn(sender, |sender, gas| { + TransactionData::new_transfer(recipient, obj_ref, sender, gas, gas_budget) + }); + let summary = self.execute_txn(transaction, gas_budget)?; + let output = self.object_summary_output(&summary, false); + Ok(output) + } } } } diff --git a/crates/sui-types/src/error.rs b/crates/sui-types/src/error.rs index 93412b801275f..42f489256f249 100644 --- a/crates/sui-types/src/error.rs +++ b/crates/sui-types/src/error.rs @@ -447,6 +447,7 @@ pub enum ExecutionErrorKind { // Naitive Transfer errors TransferUnowned, TransferNonCoin, + TransferObjectWithoutPublicTransfer, TransferInsufficientBalance, InvalidTransactionUpdate, @@ -522,6 +523,7 @@ impl ExecutionError { ExecutionErrorKind::InsufficientGas => ExecutionFailureStatus::InsufficientGas, ExecutionErrorKind::TransferUnowned | ExecutionErrorKind::TransferNonCoin + | ExecutionErrorKind::TransferObjectWithoutPublicTransfer | ExecutionErrorKind::TransferInsufficientBalance | ExecutionErrorKind::InvalidTransactionUpdate | ExecutionErrorKind::ObjectNotFound diff --git a/crates/sui-types/src/object.rs b/crates/sui-types/src/object.rs index 24f775e8ca157..a0f5cf1006bff 100644 --- a/crates/sui-types/src/object.rs +++ b/crates/sui-types/src/object.rs @@ -569,7 +569,7 @@ impl Object { } pub fn ensure_public_transfer_eligible(&self) -> Result<(), ExecutionError> { - if !self.is_owned() { + if !matches!(self.owner, Owner::AddressOwner(_)) { return Err(ExecutionErrorKind::TransferUnowned.into()); } let has_public_transfer = match &self.data { @@ -577,7 +577,7 @@ impl Object { Data::Package(_) => false, }; if !has_public_transfer { - return Err(ExecutionErrorKind::TransferNonCoin.into()); + return Err(ExecutionErrorKind::TransferObjectWithoutPublicTransfer.into()); } Ok(()) }