// SPDX-License-Identifier: Apache 2 /// This module implements three methods: `prepare_transfer` and /// `transfer_tokens_with_payload`, which are meant to work together. /// /// `prepare_transfer` allows a contract to pack token transfer parameters with /// an arbitrary payload in preparation to bridge these assets to another /// network. Only an `EmitterCap` has the capability to create /// `TransferTicket`. The `EmitterCap` object ID is encoded as the /// sender. /// /// `transfer_tokens_with_payload` unpacks the `TransferTicket` and /// constructs a `MessageTicket`, which will be used by Wormhole's /// `publish_message` module. /// /// The purpose of splitting this token transferring into two steps is in case /// Token Bridge needs to be upgraded and there is a breaking change for this /// module, an integrator would not be left broken. It is discouraged to put /// `transfer_tokens_with_payload` in an integrator's package logic. Otherwise, /// this integrator needs to be prepared to upgrade his contract to handle the /// latest version of `transfer_tokens_with_payload`. /// /// Instead, an integrator is encouraged to execute a transaction block, which /// executes `transfer_tokens_with_payload` using the latest Token Bridge /// package ID and to implement `prepare_transfer` in his contract to produce /// `PrepareTransferWithPayload`. /// /// NOTE: Only assets that exist in the `TokenRegistry` can be bridged out, /// which are native Sui assets that have been attested for via `attest_token` /// and wrapped foreign assets that have been created using foreign asset /// metadata via the `create_wrapped` module. /// /// See `transfer_with_payload` module for serialization and deserialization of /// Wormhole message payload. module token_bridge::transfer_tokens_with_payload { use sui::balance::{Balance}; use sui::coin::{Coin}; use sui::object::{Self, ID}; use wormhole::bytes32::{Self}; use wormhole::emitter::{EmitterCap}; use wormhole::external_address::{Self}; use wormhole::publish_message::{MessageTicket}; use token_bridge::normalized_amount::{NormalizedAmount}; use token_bridge::state::{Self, State, LatestOnly}; use token_bridge::token_registry::{VerifiedAsset}; use token_bridge::transfer_with_payload::{Self}; /// This type represents transfer data for a specific redeemer contract on a /// foreign chain. The only way to destroy this type is calling /// `transfer_tokens_with_payload`. Only the owner of an `EmitterCap` has /// the capability of generating `TransferTicket`. This emitter /// cap will usually live in an integrator's contract storage object. struct TransferTicket { asset_info: VerifiedAsset, bridged_in: Balance, norm_amount: NormalizedAmount, sender: ID, redeemer_chain: u16, redeemer: vector, payload: vector, nonce: u32 } /// `prepare_transfer` constructs token transfer parameters. Any remaining /// amount (A.K.A. dust) from the funds provided will be returned along with /// the `TransferTicket` type. The returned coin object is the /// same object moved into this method. /// /// NOTE: Integrators of Token Bridge should be calling only this method /// from their contracts. This method is not guarded by version control /// (thus not requiring a reference to the Token Bridge `State` object), so /// it is intended to work for any package version. public fun prepare_transfer( emitter_cap: &EmitterCap, asset_info: VerifiedAsset, funded: Coin, redeemer_chain: u16, redeemer: vector, payload: vector, nonce: u32 ): ( TransferTicket, Coin ) { use token_bridge::transfer_tokens::{take_truncated_amount}; let ( bridged_in, norm_amount ) = take_truncated_amount(&asset_info, &mut funded); let prepared_transfer = TransferTicket { asset_info, bridged_in, norm_amount, sender: object::id(emitter_cap), redeemer_chain, redeemer, payload, nonce }; // The remaining amount of funded may have dust depending on the // decimals of this asset. (prepared_transfer, funded) } /// `transfer_tokens_with_payload` is the only method that can unpack the /// members of `TransferTicket`. This method takes the balance /// from this type and bridges this asset out of Sui by either joining its /// balance in the Token Bridge's custody for native assets or burning its /// balance for wrapped assets. /// /// The unpacked sender ID comes from an `EmitterCap`. It is encoded as the /// sender of these assets. And associated with this transfer is an /// arbitrary payload, which can be consumed by the specified redeemer and /// used as instructions for a contract composing with Token Bridge. /// /// This method returns the prepared Wormhole message (which should be /// consumed by calling `publish_message` in a transaction block). /// /// NOTE: This method is guarded by a minimum build version check. This /// method could break backward compatibility on an upgrade. /// /// It is important for integrators to refrain from calling this method /// within their contracts. This method is meant to be called in a /// tranasction block after receiving a `TransferTicket` from calling /// `prepare_transfer` within a contract. If in a circumstance where this /// module has a breaking change in an upgrade, `prepare_transfer` will not /// be affected by this change. public fun transfer_tokens_with_payload( token_bridge_state: &mut State, prepared_transfer: TransferTicket ): MessageTicket { // This capability ensures that the current build version is used. let latest_only = state::assert_latest_only(token_bridge_state); // Encode Wormhole message payload. let ( nonce, encoded_transfer_with_payload ) = bridge_in_and_serialize_transfer( &latest_only, token_bridge_state, prepared_transfer ); // Prepare Wormhole message with encoded `TransferWithPayload`. state::prepare_wormhole_message( &latest_only, token_bridge_state, nonce, encoded_transfer_with_payload ) } fun bridge_in_and_serialize_transfer( latest_only: &LatestOnly, token_bridge_state: &mut State, prepared_transfer: TransferTicket ): ( u32, vector ) { use token_bridge::transfer_tokens::{burn_or_deposit_funds}; let TransferTicket { asset_info, bridged_in, norm_amount, sender, redeemer_chain, redeemer, payload, nonce } = prepared_transfer; let ( token_chain, token_address ) = burn_or_deposit_funds( latest_only, token_bridge_state, &asset_info, bridged_in ); let redeemer = external_address::new(bytes32::from_bytes(redeemer)); let encoded = transfer_with_payload::serialize( transfer_with_payload::new( sender, norm_amount, token_address, token_chain, redeemer, redeemer_chain, payload ) ); (nonce, encoded) } #[test_only] public fun bridge_in_and_serialize_transfer_test_only( token_bridge_state: &mut State, prepared_transfer: TransferTicket ): ( u32, vector ) { // This capability ensures that the current build version is used. let latest_only = state::assert_latest_only(token_bridge_state); bridge_in_and_serialize_transfer( &latest_only, token_bridge_state, prepared_transfer ) } } #[test_only] module token_bridge::transfer_tokens_with_payload_tests { use sui::coin::{Self}; use sui::object::{Self}; use sui::test_scenario::{Self}; use wormhole::bytes32::{Self}; use wormhole::emitter::{Self}; use wormhole::external_address::{Self}; use wormhole::publish_message::{Self}; use wormhole::state::{chain_id}; use token_bridge::coin_wrapped_7::{Self, COIN_WRAPPED_7}; use token_bridge::coin_native_10::{Self, COIN_NATIVE_10}; use token_bridge::native_asset::{Self}; use token_bridge::normalized_amount::{Self}; use token_bridge::state::{Self}; use token_bridge::token_bridge_scenario::{ set_up_wormhole_and_token_bridge, register_dummy_emitter, return_state, take_state, person }; use token_bridge::token_registry::{Self}; use token_bridge::transfer_with_payload::{Self}; use token_bridge::wrapped_asset::{Self}; /// Test consts. const TEST_TARGET_RECIPIENT: vector = x"beef4269"; const TEST_TARGET_CHAIN: u16 = 2; const TEST_NONCE: u32 = 0; const TEST_COIN_NATIVE_10_DECIMALS: u8 = 10; const TEST_COIN_WRAPPED_7_DECIMALS: u8 = 7; const TEST_MESSAGE_PAYLOAD: vector = x"deadbeefdeadbeef"; #[test] fun test_transfer_tokens_with_payload_native_10() { use token_bridge::transfer_tokens_with_payload::{ prepare_transfer, transfer_tokens_with_payload }; let sender = person(); let my_scenario = test_scenario::begin(sender); let scenario = &mut my_scenario; // Set up contracts. let wormhole_fee = 350; set_up_wormhole_and_token_bridge(scenario, wormhole_fee); // Register foreign emitter on chain ID == 2. register_dummy_emitter(scenario, TEST_TARGET_CHAIN); // Register and mint coins. let transfer_amount = 6942000; let coin_10_balance = coin_native_10::init_register_and_mint( scenario, sender, transfer_amount ); // Ignore effects. test_scenario::next_tx(scenario, sender); // Fetch objects necessary for sending the transfer. let token_bridge_state = take_state(scenario); // Balance check the Token Bridge before executing the transfer. The // initial balance should be zero for COIN_NATIVE_10. { let registry = state::borrow_token_registry(&token_bridge_state); let asset = token_registry::borrow_native(registry); assert!(native_asset::custody(asset) == 0, 0); }; // Register and obtain a new wormhole emitter cap. let emitter_cap = emitter::dummy(); let asset_info = state::verified_asset(&token_bridge_state); let ( prepared_transfer, dust ) = prepare_transfer( &emitter_cap, asset_info, coin::from_balance( coin_10_balance, test_scenario::ctx(scenario) ), TEST_TARGET_CHAIN, TEST_TARGET_RECIPIENT, TEST_MESSAGE_PAYLOAD, TEST_NONCE, ); coin::destroy_zero(dust); // Call `transfer_tokens_with_payload`. let prepared_msg = transfer_tokens_with_payload( &mut token_bridge_state, prepared_transfer ); // Balance check the Token Bridge after executing the transfer. The // balance should now reflect the `transfer_amount` defined in this // test. { let registry = state::borrow_token_registry(&token_bridge_state); let asset = token_registry::borrow_native(registry); assert!(native_asset::custody(asset) == transfer_amount, 0); }; // Clean up. publish_message::destroy(prepared_msg); return_state(token_bridge_state); emitter::destroy_test_only(emitter_cap); // Done. test_scenario::end(my_scenario); } #[test] fun test_transfer_tokens_native_10_with_dust_refund() { use token_bridge::transfer_tokens_with_payload::{ prepare_transfer, transfer_tokens_with_payload }; let sender = person(); let my_scenario = test_scenario::begin(sender); let scenario = &mut my_scenario; // Set up contracts. let wormhole_fee = 350; set_up_wormhole_and_token_bridge(scenario, wormhole_fee); // Register foreign emitter on chain ID == 2. register_dummy_emitter(scenario, TEST_TARGET_CHAIN); // Register and mint coins. let transfer_amount = 1000069; let coin_10_balance = coin_native_10::init_register_and_mint( scenario, sender, transfer_amount ); // This value will be used later. The contract should return dust // to the caller since COIN_NATIVE_10 has 10 decimals. let expected_dust = 69; // Ignore effects. test_scenario::next_tx(scenario, sender); // Fetch objects necessary for sending the transfer. let token_bridge_state = take_state(scenario); // Balance check the Token Bridge before executing the transfer. The // initial balance should be zero for COIN_NATIVE_10. { let registry = state::borrow_token_registry(&token_bridge_state); let asset = token_registry::borrow_native(registry); assert!(native_asset::custody(asset) == 0, 0); }; // Register and obtain a new wormhole emitter cap. let emitter_cap = emitter::dummy(); let asset_info = state::verified_asset(&token_bridge_state); let ( prepared_transfer, dust ) = prepare_transfer( &emitter_cap, asset_info, coin::from_balance( coin_10_balance, test_scenario::ctx(scenario) ), TEST_TARGET_CHAIN, TEST_TARGET_RECIPIENT, TEST_MESSAGE_PAYLOAD, TEST_NONCE, ); assert!(coin::value(&dust) == expected_dust, 0); // Call `transfer_tokens`. let prepared_msg = transfer_tokens_with_payload( &mut token_bridge_state, prepared_transfer ); // Balance check the Token Bridge after executing the transfer. The // balance should now reflect the `transfer_amount` less `expected_dust` // defined in this test. { let registry = state::borrow_token_registry(&token_bridge_state); let asset = token_registry::borrow_native(registry); assert!( native_asset::custody(asset) == transfer_amount - expected_dust, 0 ); }; // Clean up. publish_message::destroy(prepared_msg); coin::burn_for_testing(dust); emitter::destroy_test_only(emitter_cap); return_state(token_bridge_state); // Done. test_scenario::end(my_scenario); } #[test] fun test_serialize_transfer_tokens_native_10() { use token_bridge::transfer_tokens_with_payload::{ bridge_in_and_serialize_transfer_test_only, prepare_transfer }; let sender = person(); let my_scenario = test_scenario::begin(sender); let scenario = &mut my_scenario; // Set up contracts. let wormhole_fee = 350; set_up_wormhole_and_token_bridge(scenario, wormhole_fee); // Register foreign emitter on chain ID == 2. register_dummy_emitter(scenario, TEST_TARGET_CHAIN); // Register and mint coins. let transfer_amount = 6942000; let bridge_coin_10 = coin::from_balance( coin_native_10::init_register_and_mint( scenario, sender, transfer_amount ), test_scenario::ctx(scenario) ); // Ignore effects. test_scenario::next_tx(scenario, sender); // Fetch objects necessary for sending the transfer. let token_bridge_state = take_state(scenario); // Register and obtain a new wormhole emitter cap. let emitter_cap = emitter::dummy(); let asset_info = state::verified_asset(&token_bridge_state); let expected_token_address = token_registry::token_address(&asset_info); let ( prepared_transfer, dust ) = prepare_transfer( &emitter_cap, asset_info, bridge_coin_10, TEST_TARGET_CHAIN, TEST_TARGET_RECIPIENT, TEST_MESSAGE_PAYLOAD, TEST_NONCE, ); coin::destroy_zero(dust); // Serialize the payload. let ( nonce, payload ) = bridge_in_and_serialize_transfer_test_only( &mut token_bridge_state, prepared_transfer ); assert!(nonce == TEST_NONCE, 0); // Construct expected payload from scratch and confirm that the // `transfer_tokens` call produces the same payload. let expected_amount = normalized_amount::from_raw( transfer_amount, TEST_COIN_NATIVE_10_DECIMALS ); let expected_payload = transfer_with_payload::new_test_only( object::id(&emitter_cap), expected_amount, expected_token_address, chain_id(), external_address::new(bytes32::from_bytes(TEST_TARGET_RECIPIENT)), TEST_TARGET_CHAIN, TEST_MESSAGE_PAYLOAD ); assert!( transfer_with_payload::serialize(expected_payload) == payload, 0 ); // Clean up. return_state(token_bridge_state); emitter::destroy_test_only(emitter_cap); // Done. test_scenario::end(my_scenario); } #[test] fun test_transfer_tokens_with_payload_wrapped_7() { use token_bridge::transfer_tokens_with_payload::{ prepare_transfer, transfer_tokens_with_payload }; let sender = person(); let my_scenario = test_scenario::begin(sender); let scenario = &mut my_scenario; // Set up contracts. let wormhole_fee = 350; set_up_wormhole_and_token_bridge(scenario, wormhole_fee); // Register foreign emitter on chain ID == 2. register_dummy_emitter(scenario, TEST_TARGET_CHAIN); // Register and mint coins. let transfer_amount = 6942000; let coin_7_balance = coin_wrapped_7::init_register_and_mint( scenario, sender, transfer_amount ); // Ignore effects. test_scenario::next_tx(scenario, sender); // Fetch objects necessary for sending the transfer. let token_bridge_state = take_state(scenario); // Balance check the Token Bridge before executing the transfer. The // initial balance should be the `transfer_amount` for COIN_WRAPPED_7. { let registry = state::borrow_token_registry(&token_bridge_state); let asset = token_registry::borrow_wrapped(registry); assert!(wrapped_asset::total_supply(asset) == transfer_amount, 0); }; // Register and obtain a new wormhole emitter cap. let emitter_cap = emitter::dummy(); let asset_info = state::verified_asset(&token_bridge_state); let ( prepared_transfer, dust ) = prepare_transfer( &emitter_cap, asset_info, coin::from_balance( coin_7_balance, test_scenario::ctx(scenario) ), TEST_TARGET_CHAIN, TEST_TARGET_RECIPIENT, TEST_MESSAGE_PAYLOAD, TEST_NONCE, ); coin::destroy_zero(dust); // Call `transfer_tokens_with_payload`. let prepared_msg = transfer_tokens_with_payload( &mut token_bridge_state, prepared_transfer ); // Balance check the Token Bridge after executing the transfer. The // balance should be zero, since tokens are burned when an outbound // wrapped token transfer occurs. { let registry = state::borrow_token_registry(&token_bridge_state); let asset = token_registry::borrow_wrapped(registry); assert!(wrapped_asset::total_supply(asset) == 0, 0); }; // Clean up. publish_message::destroy(prepared_msg); emitter::destroy_test_only(emitter_cap); return_state(token_bridge_state); // Done. test_scenario::end(my_scenario); } #[test] fun test_serialize_transfer_tokens_wrapped_7() { use token_bridge::transfer_tokens_with_payload::{ bridge_in_and_serialize_transfer_test_only, prepare_transfer }; let sender = person(); let my_scenario = test_scenario::begin(sender); let scenario = &mut my_scenario; // Set up contracts. let wormhole_fee = 350; set_up_wormhole_and_token_bridge(scenario, wormhole_fee); // Register foreign emitter on chain ID == 2. register_dummy_emitter(scenario, TEST_TARGET_CHAIN); // Register and mint coins. let transfer_amount = 6942000; let bridged_coin_7 = coin::from_balance( coin_wrapped_7::init_register_and_mint( scenario, sender, transfer_amount ), test_scenario::ctx(scenario) ); // Ignore effects. test_scenario::next_tx(scenario, sender); // Fetch objects necessary for sending the transfer. let token_bridge_state = take_state(scenario); // Register and obtain a new wormhole emitter cap. let emitter_cap = emitter::dummy(); let asset_info = state::verified_asset(&token_bridge_state); let expected_token_address = token_registry::token_address(&asset_info); let expected_token_chain = token_registry::token_chain(&asset_info); let ( prepared_transfer, dust ) = prepare_transfer( &emitter_cap, asset_info, bridged_coin_7, TEST_TARGET_CHAIN, TEST_TARGET_RECIPIENT, TEST_MESSAGE_PAYLOAD, TEST_NONCE, ); coin::destroy_zero(dust); // Serialize the payload. let ( nonce, payload ) = bridge_in_and_serialize_transfer_test_only( &mut token_bridge_state, prepared_transfer ); assert!(nonce == TEST_NONCE, 0); // Construct expected payload from scratch and confirm that the // `transfer_tokens` call produces the same payload. let expected_amount = normalized_amount::from_raw( transfer_amount, TEST_COIN_WRAPPED_7_DECIMALS ); let expected_payload = transfer_with_payload::new_test_only( object::id(&emitter_cap), expected_amount, expected_token_address, expected_token_chain, external_address::new(bytes32::from_bytes(TEST_TARGET_RECIPIENT)), TEST_TARGET_CHAIN, TEST_MESSAGE_PAYLOAD ); assert!( transfer_with_payload::serialize(expected_payload) == payload, 0 ); // Clean up. emitter::destroy_test_only(emitter_cap); return_state(token_bridge_state); // Done. test_scenario::end(my_scenario); } #[test] #[expected_failure(abort_code = wormhole::package_utils::E_NOT_CURRENT_VERSION)] fun test_cannot_transfer_tokens_with_payload_outdated_version() { use token_bridge::transfer_tokens_with_payload::{ prepare_transfer, transfer_tokens_with_payload }; let sender = person(); let my_scenario = test_scenario::begin(sender); let scenario = &mut my_scenario; // Set up contracts. let wormhole_fee = 350; set_up_wormhole_and_token_bridge(scenario, wormhole_fee); // Register foreign emitter on chain ID == 2. register_dummy_emitter(scenario, TEST_TARGET_CHAIN); // Register and mint coins. let transfer_amount = 6942000; let coin_10_balance = coin_native_10::init_register_and_mint( scenario, sender, transfer_amount ); // Ignore effects. test_scenario::next_tx(scenario, sender); // Fetch objects necessary for sending the transfer. let token_bridge_state = take_state(scenario); // Register and obtain a new wormhole emitter cap. let emitter_cap = emitter::dummy(); let asset_info = state::verified_asset(&token_bridge_state); let ( prepared_transfer, dust ) = prepare_transfer( &emitter_cap, asset_info, coin::from_balance( coin_10_balance, test_scenario::ctx(scenario) ), TEST_TARGET_CHAIN, TEST_TARGET_RECIPIENT, TEST_MESSAGE_PAYLOAD, TEST_NONCE, ); coin::destroy_zero(dust); // Conveniently roll version back. state::reverse_migrate_version(&mut token_bridge_state); // Simulate executing with an outdated build by upticking the minimum // required version for `publish_message` to something greater than // this build. state::migrate_version_test_only( &mut token_bridge_state, token_bridge::version_control::previous_version_test_only(), token_bridge::version_control::next_version() ); // You shall not pass! let prepared_msg = transfer_tokens_with_payload( &mut token_bridge_state, prepared_transfer ); // Clean up. publish_message::destroy(prepared_msg); abort 42 } }