| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053 |
- // SPDX-License-Identifier: Apache 2
- /// This module implements three methods: `prepare_transfer` and
- /// `transfer_tokens`, which are meant to work together.
- ///
- /// `prepare_transfer` allows a contract to pack token transfer parameters in
- /// preparation to bridge these assets to another network. Anyone can call this
- /// method to create `TransferTicket`.
- ///
- /// `transfer_tokens` 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` 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`.
- ///
- /// Instead, an integrator is encouraged to execute a transaction block, which
- /// executes `transfer_tokens` using the latest Token Bridge package ID and to
- /// implement `prepare_transfer` in his contract to produce `PrepareTransfer`.
- ///
- /// 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` module for serialization and deserialization of Wormhole
- /// message payload.
- module token_bridge::transfer_tokens {
- use sui::balance::{Self, Balance};
- use sui::coin::{Self, Coin};
- use wormhole::bytes32::{Self};
- use wormhole::external_address::{Self, ExternalAddress};
- use wormhole::publish_message::{MessageTicket};
- use token_bridge::native_asset::{Self};
- use token_bridge::normalized_amount::{Self, NormalizedAmount};
- use token_bridge::state::{Self, State, LatestOnly};
- use token_bridge::token_registry::{Self, VerifiedAsset};
- use token_bridge::transfer::{Self};
- use token_bridge::wrapped_asset::{Self};
- friend token_bridge::transfer_tokens_with_payload;
- /// Relayer fee exceeds `Coin` object's value.
- const E_RELAYER_FEE_EXCEEDS_AMOUNT: u64 = 0;
- /// This type represents transfer data for a recipient on a foreign chain.
- /// The only way to destroy this type is calling `transfer_tokens`.
- ///
- /// NOTE: An integrator that expects to bridge assets between his contracts
- /// should probably use the `transfer_tokens_with_payload` module, which
- /// expects a specific redeemer to complete the transfer (transfers sent
- /// using `transfer_tokens` can be redeemed by anyone on behalf of the
- /// encoded recipient).
- struct TransferTicket<phantom CoinType> {
- asset_info: VerifiedAsset<CoinType>,
- bridged_in: Balance<CoinType>,
- norm_amount: NormalizedAmount,
- recipient_chain: u16,
- recipient: vector<u8>,
- relayer_fee: u64,
- 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<CoinType>(
- asset_info: VerifiedAsset<CoinType>,
- funded: Coin<CoinType>,
- recipient_chain: u16,
- recipient: vector<u8>,
- relayer_fee: u64,
- nonce: u32
- ): (
- TransferTicket<CoinType>,
- Coin<CoinType>
- ) {
- let (
- bridged_in,
- norm_amount
- ) = take_truncated_amount(&asset_info, &mut funded);
- let ticket =
- TransferTicket {
- asset_info,
- bridged_in,
- norm_amount,
- relayer_fee,
- recipient_chain,
- recipient,
- nonce
- };
- // The remaining amount of funded may have dust depending on the
- // decimals of this asset.
- (ticket, funded)
- }
- /// `transfer_tokens` 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.
- ///
- /// A `relayer_fee` of some value less than or equal to the bridged balance
- /// can be specified to incentivize someone to redeem this transfer on
- /// behalf of the `recipient`.
- ///
- /// 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
- /// transaction 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<CoinType>(
- token_bridge_state: &mut State,
- ticket: TransferTicket<CoinType>
- ): MessageTicket {
- // This capability ensures that the current build version is used.
- let latest_only = state::assert_latest_only(token_bridge_state);
- let (
- nonce,
- encoded_transfer
- ) =
- bridge_in_and_serialize_transfer(
- &latest_only,
- token_bridge_state,
- ticket
- );
- // Prepare Wormhole message with encoded `Transfer`.
- state::prepare_wormhole_message(
- &latest_only,
- token_bridge_state,
- nonce,
- encoded_transfer
- )
- }
- /// Modify coin based on the decimals of a given coin type, which may
- /// leave some amount if the decimals lead to truncating the coin's balance.
- /// This method returns the extracted balance (which will be bridged out of
- /// Sui) and the normalized amount, which will be encoded in the token
- /// transfer payload.
- ///
- /// NOTE: This is a privileged method, which only this and the
- /// `transfer_tokens_with_payload` modules can use.
- public(friend) fun take_truncated_amount<CoinType>(
- asset_info: &VerifiedAsset<CoinType>,
- funded: &mut Coin<CoinType>
- ): (
- Balance<CoinType>,
- NormalizedAmount
- ) {
- // Calculate dust. If there is any, `bridged_in` will have remaining
- // value after split. `norm_amount` is copied since it is denormalized
- // at this step.
- let decimals = token_registry::coin_decimals(asset_info);
- let norm_amount =
- normalized_amount::from_raw(coin::value(funded), decimals);
- // Split the `bridged_in` coin object to return any dust remaining on
- // that object. Only bridge in the adjusted amount after de-normalizing
- // the normalized amount.
- let truncated =
- balance::split(
- coin::balance_mut(funded),
- normalized_amount::to_raw(norm_amount, decimals)
- );
- (truncated, norm_amount)
- }
- /// For a given coin type, either burn Token Bridge wrapped assets or
- /// deposit coin into Token Bridge's custody. This method returns the
- /// canonical token info (chain ID and address), which will be encoded in
- /// the token transfer.
- ///
- /// NOTE: This is a privileged method, which only this and the
- /// `transfer_tokens_with_payload` modules can use.
- public(friend) fun burn_or_deposit_funds<CoinType>(
- latest_only: &LatestOnly,
- token_bridge_state: &mut State,
- asset_info: &VerifiedAsset<CoinType>,
- bridged_in: Balance<CoinType>
- ): (
- u16,
- ExternalAddress
- ) {
- // Either burn or deposit depending on `CoinType`.
- let registry =
- state::borrow_mut_token_registry(latest_only, token_bridge_state);
- if (token_registry::is_wrapped(asset_info)) {
- wrapped_asset::burn(
- token_registry::borrow_mut_wrapped(registry),
- bridged_in
- );
- } else {
- native_asset::deposit(
- token_registry::borrow_mut_native(registry),
- bridged_in
- );
- };
- // Return canonical token info.
- (
- token_registry::token_chain(asset_info),
- token_registry::token_address(asset_info)
- )
- }
- fun bridge_in_and_serialize_transfer<CoinType>(
- latest_only: &LatestOnly,
- token_bridge_state: &mut State,
- ticket: TransferTicket<CoinType>
- ): (
- u32,
- vector<u8>
- ) {
- let TransferTicket {
- asset_info,
- bridged_in,
- norm_amount,
- recipient_chain,
- recipient,
- relayer_fee,
- nonce
- } = ticket;
- // Disallow `relayer_fee` to be greater than the `Coin` object's value.
- // Keep in mind that the relayer fee is evaluated against the truncated
- // amount.
- let amount = sui::balance::value(&bridged_in);
- assert!(relayer_fee <= amount, E_RELAYER_FEE_EXCEEDS_AMOUNT);
- // Handle funds and get canonical token info for encoded transfer.
- let (
- token_chain,
- token_address
- ) = burn_or_deposit_funds(
- latest_only,
- token_bridge_state,
- &asset_info, bridged_in
- );
- // Ensure that the recipient is a 32-byte address.
- let recipient = external_address::new(bytes32::from_bytes(recipient));
- // Finally encode `Transfer`.
- let encoded =
- transfer::serialize(
- transfer::new(
- norm_amount,
- token_address,
- token_chain,
- recipient,
- recipient_chain,
- normalized_amount::from_raw(
- relayer_fee,
- token_registry::coin_decimals(&asset_info)
- )
- )
- );
- (nonce, encoded)
- }
- #[test_only]
- public fun bridge_in_and_serialize_transfer_test_only<CoinType>(
- token_bridge_state: &mut State,
- ticket: TransferTicket<CoinType>
- ): (
- u32,
- vector<u8>
- ) {
- // 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,
- ticket
- )
- }
- }
- #[test_only]
- module token_bridge::transfer_token_tests {
- use sui::coin::{Self};
- use sui::test_scenario::{Self};
- use wormhole::bytes32::{Self};
- use wormhole::external_address::{Self};
- use wormhole::publish_message::{Self};
- use wormhole::state::{chain_id};
- use token_bridge::coin_native_10::{Self, COIN_NATIVE_10};
- use token_bridge::coin_wrapped_7::{Self, COIN_WRAPPED_7};
- 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::{Self};
- use token_bridge::transfer_tokens::{Self};
- use token_bridge::wrapped_asset::{Self};
- /// Test consts.
- const TEST_TARGET_RECIPIENT: vector<u8> = 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;
- #[test]
- fun test_transfer_tokens_native_10() {
- use token_bridge::transfer_tokens::{prepare_transfer, transfer_tokens};
- 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);
- // Define the relayer fee.
- let relayer_fee = 100000;
- // 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<COIN_NATIVE_10>(registry);
- assert!(native_asset::custody(asset) == 0, 0);
- };
- let asset_info = state::verified_asset(&token_bridge_state);
- let (
- ticket,
- dust
- ) =
- prepare_transfer(
- asset_info,
- coin::from_balance(
- coin_10_balance,
- test_scenario::ctx(scenario)
- ),
- TEST_TARGET_CHAIN,
- TEST_TARGET_RECIPIENT,
- relayer_fee,
- TEST_NONCE,
- );
- coin::destroy_zero(dust);
- // Call `transfer_tokens`.
- let prepared_msg =
- transfer_tokens(&mut token_bridge_state, ticket);
- // 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<COIN_NATIVE_10>(registry);
- assert!(native_asset::custody(asset) == transfer_amount, 0);
- };
- // Clean up.
- publish_message::destroy(prepared_msg);
- return_state(token_bridge_state);
- // Done.
- test_scenario::end(my_scenario);
- }
- #[test]
- fun test_transfer_tokens_native_10_with_dust_refund() {
- use token_bridge::transfer_tokens::{prepare_transfer, transfer_tokens};
- 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);
- // Define the relayer fee.
- let relayer_fee = 100000;
- // 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<COIN_NATIVE_10>(registry);
- assert!(native_asset::custody(asset) == 0, 0);
- };
- let asset_info = state::verified_asset(&token_bridge_state);
- let (
- ticket,
- dust
- ) =
- prepare_transfer(
- asset_info,
- coin::from_balance(
- coin_10_balance,
- test_scenario::ctx(scenario)
- ),
- TEST_TARGET_CHAIN,
- TEST_TARGET_RECIPIENT,
- relayer_fee,
- TEST_NONCE,
- );
- assert!(coin::value(&dust) == expected_dust, 0);
- // Call `transfer_tokens`.
- let prepared_msg =
- transfer_tokens(&mut token_bridge_state, ticket);
- // 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<COIN_NATIVE_10>(registry);
- assert!(
- native_asset::custody(asset) == transfer_amount - expected_dust,
- 0
- );
- };
- // Clean up.
- publish_message::destroy(prepared_msg);
- coin::burn_for_testing(dust);
- return_state(token_bridge_state);
- // Done.
- test_scenario::end(my_scenario);
- }
- #[test]
- fun test_serialize_transfer_tokens_native_10() {
- use token_bridge::transfer_tokens::{
- 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_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);
- // Define the relayer fee.
- let relayer_fee = 100000;
- let asset_info = state::verified_asset(&token_bridge_state);
- let expected_token_address = token_registry::token_address(&asset_info);
- let (
- ticket,
- dust
- ) =
- prepare_transfer(
- asset_info,
- bridged_coin_10,
- TEST_TARGET_CHAIN,
- TEST_TARGET_RECIPIENT,
- relayer_fee,
- TEST_NONCE,
- );
- coin::destroy_zero(dust);
- // Call `transfer_tokens`.
- let (
- nonce,
- payload
- ) =
- bridge_in_and_serialize_transfer_test_only(
- &mut token_bridge_state,
- ticket
- );
- 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_relayer_fee =
- normalized_amount::from_raw(
- relayer_fee,
- TEST_COIN_NATIVE_10_DECIMALS
- );
- let expected_payload =
- transfer::new_test_only(
- expected_amount,
- expected_token_address,
- chain_id(),
- external_address::new(
- bytes32::from_bytes(TEST_TARGET_RECIPIENT)
- ),
- TEST_TARGET_CHAIN,
- expected_relayer_fee
- );
- assert!(transfer::serialize_test_only(expected_payload) == payload, 0);
- // Clean up.
- return_state(token_bridge_state);
- // Done.
- test_scenario::end(my_scenario);
- }
- #[test]
- fun test_transfer_tokens_wrapped_7() {
- use token_bridge::transfer_tokens::{prepare_transfer, transfer_tokens};
- 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 = 42069000;
- 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);
- // Define the relayer fee.
- let relayer_fee = 100000;
- // 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<COIN_WRAPPED_7>(registry);
- assert!(wrapped_asset::total_supply(asset) == transfer_amount, 0);
- };
- let asset_info = state::verified_asset(&token_bridge_state);
- let (
- ticket,
- dust
- ) =
- prepare_transfer(
- asset_info,
- coin::from_balance(
- coin_7_balance,
- test_scenario::ctx(scenario)
- ),
- TEST_TARGET_CHAIN,
- TEST_TARGET_RECIPIENT,
- relayer_fee,
- TEST_NONCE,
- );
- coin::destroy_zero(dust);
- // Call `transfer_tokens`.
- let prepared_msg =
- transfer_tokens(&mut token_bridge_state, ticket);
- // 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<COIN_WRAPPED_7>(registry);
- assert!(wrapped_asset::total_supply(asset) == 0, 0);
- };
- // Clean up.
- publish_message::destroy(prepared_msg);
- return_state(token_bridge_state);
- // Done.
- test_scenario::end(my_scenario);
- }
- #[test]
- fun test_serialize_transfer_tokens_wrapped_7() {
- use token_bridge::transfer_tokens::{
- 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);
- // Define the relayer fee.
- let relayer_fee = 100000;
- 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 (
- ticket,
- dust
- ) =
- prepare_transfer(
- asset_info,
- bridged_coin_7,
- TEST_TARGET_CHAIN,
- TEST_TARGET_RECIPIENT,
- relayer_fee,
- TEST_NONCE,
- );
- coin::destroy_zero(dust);
- // Call `transfer_tokens`.
- let (
- nonce,
- payload
- ) =
- bridge_in_and_serialize_transfer_test_only(
- &mut token_bridge_state,
- ticket
- );
- 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_relayer_fee =
- normalized_amount::from_raw(
- relayer_fee,
- TEST_COIN_WRAPPED_7_DECIMALS
- );
- let expected_payload =
- transfer::new_test_only(
- expected_amount,
- expected_token_address,
- expected_token_chain,
- external_address::new(
- bytes32::from_bytes(TEST_TARGET_RECIPIENT)
- ),
- TEST_TARGET_CHAIN,
- expected_relayer_fee
- );
- assert!(transfer::serialize_test_only(expected_payload) == payload, 0);
- // Clean up.
- return_state(token_bridge_state);
- // Done.
- test_scenario::end(my_scenario);
- }
- #[test]
- #[expected_failure(abort_code = token_registry::E_UNREGISTERED)]
- fun test_cannot_transfer_tokens_native_not_registered() {
- use token_bridge::transfer_tokens::{prepare_transfer, transfer_tokens};
- 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);
- // Initialize COIN_NATIVE_10 (but don't register it).
- coin_native_10::init_test_only(test_scenario::ctx(scenario));
- // NOTE: This test purposely doesn't `attest` COIN_NATIVE_10.
- let transfer_amount = 6942000;
- let test_coins =
- coin::mint_for_testing<COIN_NATIVE_10>(
- 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);
- // Define the relayer fee.
- let relayer_fee = 100000;
- let asset_info = state::verified_asset(&token_bridge_state);
- let (
- ticket,
- dust
- ) =
- prepare_transfer(
- asset_info,
- test_coins,
- TEST_TARGET_CHAIN,
- TEST_TARGET_RECIPIENT,
- relayer_fee,
- TEST_NONCE,
- );
- coin::destroy_zero(dust);
- // You shall not pass!
- let prepared_msg =
- transfer_tokens(&mut token_bridge_state, ticket);
- // Clean up.
- publish_message::destroy(prepared_msg);
- abort 42
- }
- #[test]
- #[expected_failure(abort_code = token_registry::E_UNREGISTERED)]
- fun test_cannot_transfer_tokens_wrapped_not_registered() {
- use token_bridge::transfer_tokens::{prepare_transfer, transfer_tokens};
- 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);
- // Initialize COIN_WRAPPED_7 (but don't register it).
- coin_native_10::init_test_only(test_scenario::ctx(scenario));
- let treasury_cap =
- coin_wrapped_7::init_and_take_treasury_cap(
- scenario,
- sender
- );
- sui::test_utils::destroy(treasury_cap);
- // NOTE: This test purposely doesn't `attest` COIN_WRAPPED_7.
- let transfer_amount = 42069;
- let test_coins =
- coin::mint_for_testing<COIN_WRAPPED_7>(
- 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);
- // Define the relayer fee.
- let relayer_fee = 1000;
- let asset_info = state::verified_asset(&token_bridge_state);
- let (
- ticket,
- dust
- ) =
- prepare_transfer(
- asset_info,
- test_coins,
- TEST_TARGET_CHAIN,
- TEST_TARGET_RECIPIENT,
- relayer_fee,
- TEST_NONCE,
- );
- coin::destroy_zero(dust);
- // You shall not pass!
- let prepared_msg =
- transfer_tokens(&mut token_bridge_state, ticket);
- // Clean up.
- publish_message::destroy(prepared_msg);
- abort 42
- }
- #[test]
- #[expected_failure(
- abort_code = transfer_tokens::E_RELAYER_FEE_EXCEEDS_AMOUNT
- )]
- fun test_cannot_transfer_tokens_fee_exceeds_amount() {
- use token_bridge::transfer_tokens::{prepare_transfer, transfer_tokens};
- 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);
- // NOTE: The `relayer_fee` is intentionally set to a higher number
- // than the `transfer_amount`.
- let relayer_fee = 100001;
- let transfer_amount = 100000;
- 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);
- let asset_info = state::verified_asset(&token_bridge_state);
- let (
- ticket,
- dust
- ) =
- prepare_transfer(
- asset_info,
- coin::from_balance(
- coin_10_balance,
- test_scenario::ctx(scenario)
- ),
- TEST_TARGET_CHAIN,
- TEST_TARGET_RECIPIENT,
- relayer_fee,
- TEST_NONCE,
- );
- coin::destroy_zero(dust);
- // You shall not pass!
- let prepared_msg =
- transfer_tokens(&mut token_bridge_state, ticket);
- // Done.
- publish_message::destroy(prepared_msg);
- abort 42
- }
- #[test]
- #[expected_failure(abort_code = wormhole::package_utils::E_NOT_CURRENT_VERSION)]
- fun test_cannot_transfer_tokens_outdated_version() {
- use token_bridge::transfer_tokens::{prepare_transfer, transfer_tokens};
- 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);
- let asset_info = state::verified_asset(&token_bridge_state);
- let relayer_fee = 0;
- let (
- ticket,
- dust
- ) =
- prepare_transfer(
- asset_info,
- coin::from_balance(
- coin_10_balance,
- test_scenario::ctx(scenario)
- ),
- TEST_TARGET_CHAIN,
- TEST_TARGET_RECIPIENT,
- relayer_fee,
- 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(&mut token_bridge_state, ticket);
- // Clean up.
- publish_message::destroy(prepared_msg);
- abort 42
- }
- }
|