module token_bridge::complete_transfer { use sui::tx_context::{TxContext}; use sui::transfer::{Self as transfer_object}; use sui::coin::{Self, CoinMetadata}; use wormhole::state::{State as WormholeState}; use wormhole::external_address::{Self}; use token_bridge::bridge_state::{Self, BridgeState, VerifiedCoinType}; use token_bridge::vaa::{Self}; use token_bridge::transfer::{Self, Transfer}; use token_bridge::normalized_amount::{denormalize}; const E_INVALID_TARGET: u64 = 0; public entry fun submit_vaa( wormhole_state: &mut WormholeState, bridge_state: &mut BridgeState, coin_meta: &CoinMetadata, vaa: vector, fee_recipient: address, ctx: &mut TxContext ) { let vaa = vaa::parse_verify_and_replay_protect( wormhole_state, bridge_state, vaa, ctx ); let transfer = transfer::parse(wormhole::myvaa::destroy(vaa)); let token_chain = transfer::get_token_chain(&transfer); let token_address = transfer::get_token_address(&transfer); let verified_coin_witness = bridge_state::verify_coin_type( bridge_state, token_chain, token_address ); complete_transfer( verified_coin_witness, &transfer, wormhole_state, bridge_state, coin_meta, fee_recipient, ctx ); } // complete transfer with arbitrary Transfer request and without the VAA // for native tokens #[test_only] public fun test_complete_transfer( transfer: &Transfer, wormhole_state: &mut WormholeState, bridge_state: &mut BridgeState, coin_meta: &CoinMetadata, fee_recipient: address, ctx: &mut TxContext ) { let token_chain = transfer::get_token_chain(transfer); let token_address = transfer::get_token_address(transfer); let verified_coin_witness = bridge_state::verify_coin_type( bridge_state, token_chain, token_address ); complete_transfer( verified_coin_witness, transfer, wormhole_state, bridge_state, coin_meta, fee_recipient, ctx ); } fun complete_transfer( verified_coin_witness: VerifiedCoinType, transfer: &Transfer, wormhole_state: &mut WormholeState, bridge_state: &mut BridgeState, coin_meta: &CoinMetadata, fee_recipient: address, ctx: &mut TxContext ) { let to_chain = transfer::get_to_chain(transfer); let this_chain = wormhole::state::get_chain_id(wormhole_state); assert!(to_chain == this_chain, E_INVALID_TARGET); let recipient = external_address::to_address(&transfer::get_to(transfer)); let decimals = coin::get_decimals(coin_meta); let amount = denormalize(transfer::get_amount(transfer), decimals); let fee_amount = denormalize(transfer::get_fee(transfer), decimals); let recipient_coins; if (bridge_state::is_wrapped_asset(bridge_state)) { recipient_coins = bridge_state::mint( verified_coin_witness, bridge_state, amount, ctx ); } else { recipient_coins = bridge_state::withdraw( verified_coin_witness, bridge_state, amount, ctx ); }; // take out fee from the recipient's coins. `extract` will revert // if fee > amount let fee_coins = coin::split(&mut recipient_coins, fee_amount, ctx); transfer_object::transfer(recipient_coins, recipient); transfer_object::transfer(fee_coins, fee_recipient); } } #[test_only] module token_bridge::complete_transfer_test { use std::bcs::{Self}; use sui::test_scenario::{Self, Scenario, next_tx, return_shared, take_shared, ctx, take_from_address, return_to_address}; use sui::coin::{Self, Coin, CoinMetadata}; use wormhole::myu16::{Self as u16}; use wormhole::external_address::{Self}; use token_bridge::normalized_amount::{Self}; use token_bridge::transfer::{Self, Transfer}; use token_bridge::bridge_state::{Self, BridgeState}; use token_bridge::coin_witness::{Self, COIN_WITNESS}; use token_bridge::coin_witness_test::{test_register_wrapped_}; use token_bridge::complete_transfer::{Self}; use token_bridge::native_coin_witness::{Self, NATIVE_COIN_WITNESS}; use token_bridge::native_coin_witness_v2::{Self, NATIVE_COIN_WITNESS_V2}; use token_bridge::bridge_state_test::{set_up_wormhole_core_and_token_bridges}; use wormhole::state::{Self as wormhole_state, State}; fun scenario(): Scenario { test_scenario::begin(@0x123233) } fun people(): (address, address, address) { (@0x124323, @0xE05, @0xFACE) } struct OTHER_COIN_WITNESS has drop {} #[test] fun test_complete_native_transfer(){ let (admin, fee_recipient_person, _) = people(); let test = scenario(); test = set_up_wormhole_core_and_token_bridges(admin, test); next_tx(&mut test, admin);{ native_coin_witness::test_init(ctx(&mut test)); }; // register native asset type with the token bridge next_tx(&mut test, admin);{ let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let coin_meta = take_shared>(&test); bridge_state::register_native_asset( &mut worm_state, &mut bridge_state, &coin_meta, ctx(&mut test) ); return_shared(bridge_state); return_shared(worm_state); return_shared>(coin_meta); }; // create a treasury cap for the native asset type, mint some tokens, // and deposit the native tokens into the token bridge next_tx(&mut test, admin); { let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let t_cap = take_shared>(&test); let coins = coin::mint(&mut t_cap, 10000000000, ctx(&mut test)); bridge_state::deposit(&mut bridge_state, coins); return_shared>(t_cap); return_shared(bridge_state); return_shared(worm_state); }; // complete transfer, sending native tokens to a recipient address next_tx(&mut test, admin); { let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let coin_meta = take_shared>(&test); let to = admin; let amount = 1000000000; let fee_amount = 100000000; let decimals = 10; let token_address = external_address::from_bytes(x"01"); let token_chain = wormhole_state::get_chain_id(&worm_state); let to_chain = wormhole_state::get_chain_id(&worm_state); let transfer: Transfer = transfer::create( normalized_amount::normalize(amount, decimals), token_address, token_chain, external_address::from_bytes(bcs::to_bytes(&to)), to_chain, normalized_amount::normalize(fee_amount, decimals), ); complete_transfer::test_complete_transfer( &transfer, &mut worm_state, &mut bridge_state, &coin_meta, fee_recipient_person, ctx(&mut test) ); return_shared(bridge_state); return_shared(worm_state); return_shared>(coin_meta); }; // check balances after next_tx(&mut test, admin);{ let coins = take_from_address>(&test, admin); assert!(coin::value(&coins) == 900000000, 0); return_to_address>(admin, coins); let fee_coins = take_from_address>(&test, fee_recipient_person); assert!(coin::value(&fee_coins) == 100000000, 0); return_to_address>(fee_recipient_person, fee_coins); }; test_scenario::end(test); } #[test] fun test_complete_native_transfer_10_decimals(){ let (admin, fee_recipient_person, _) = people(); let test = scenario(); test = set_up_wormhole_core_and_token_bridges(admin, test); next_tx(&mut test, admin);{ native_coin_witness::test_init(ctx(&mut test)); }; // register native asset type with the token bridge next_tx(&mut test, admin);{ let coin_meta = take_shared>(&test); let bridge_state = take_shared(&test); let worm_state = take_shared(&test); bridge_state::register_native_asset( &mut worm_state, &mut bridge_state, &coin_meta, ctx(&mut test) ); native_coin_witness::test_init(ctx(&mut test)); return_shared>(coin_meta); return_shared(bridge_state); return_shared(worm_state); }; // create a treasury cap for the native asset type, mint some tokens, // and deposit the native tokens into the token bridge next_tx(&mut test, admin); { let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let t_cap = take_shared>(&test); let coins = coin::mint(&mut t_cap, 10000000000, ctx(&mut test)); bridge_state::deposit(&mut bridge_state, coins); return_shared>(t_cap); return_shared(bridge_state); return_shared(worm_state); }; // complete transfer, sending native tokens to a recipient address next_tx(&mut test, admin); { let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let coin_meta = take_shared>(&test); let to = admin; // dust at the end gets rounded to nothing, since 10-8=2 digits are lopped off let amount = 1000000079; let fee_amount = 100000000; let decimals = 10; let token_address = external_address::from_bytes(x"01"); let token_chain = wormhole_state::get_chain_id(&worm_state); let to_chain = wormhole_state::get_chain_id(&worm_state); let transfer: Transfer = transfer::create( normalized_amount::normalize(amount, decimals), token_address, token_chain, external_address::from_bytes(bcs::to_bytes(&to)), to_chain, normalized_amount::normalize(fee_amount, decimals), ); complete_transfer::test_complete_transfer( &transfer, &mut worm_state, &mut bridge_state, &coin_meta, fee_recipient_person, ctx(&mut test) ); return_shared(bridge_state); return_shared(worm_state); return_shared>(coin_meta); }; // check balances after next_tx(&mut test, admin);{ let coins = take_from_address>(&test, admin); assert!(coin::value(&coins) == 900000000, 0); return_to_address>(admin, coins); let fee_coins = take_from_address>(&test, fee_recipient_person); assert!(coin::value(&fee_coins) == 100000000, 0); return_to_address>(fee_recipient_person, fee_coins); }; test_scenario::end(test); } #[test] fun test_complete_native_transfer_4_decimals(){ let (admin, fee_recipient_person, _) = people(); let test = scenario(); test = set_up_wormhole_core_and_token_bridges(admin, test); next_tx(&mut test, admin);{ native_coin_witness_v2::test_init(ctx(&mut test)); }; // register native asset type with the token bridge next_tx(&mut test, admin);{ let coin_meta = take_shared>(&test); let bridge_state = take_shared(&test); let worm_state = take_shared(&test); bridge_state::register_native_asset( &mut worm_state, &mut bridge_state, &coin_meta, ctx(&mut test) ); return_shared>(coin_meta); return_shared(bridge_state); return_shared(worm_state); }; // create a treasury cap for the native asset type, mint some tokens, // and deposit the native tokens into the token bridge next_tx(&mut test, admin); { let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let t_cap = take_shared>(&test); let coins = coin::mint(&mut t_cap, 10000000000, ctx(&mut test)); bridge_state::deposit(&mut bridge_state, coins); return_shared>(t_cap); return_shared(bridge_state); return_shared(worm_state); }; // complete transfer, sending native tokens to a recipient address next_tx(&mut test, admin); { let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let coin_meta = take_shared>(&test); let to = admin; let amount = 100; let fee_amount = 40; let decimals = 4; let token_address = external_address::from_bytes(x"01"); let token_chain = wormhole_state::get_chain_id(&worm_state); let to_chain = wormhole_state::get_chain_id(&worm_state); let transfer: Transfer = transfer::create( normalized_amount::normalize(amount, decimals), token_address, token_chain, external_address::from_bytes(bcs::to_bytes(&to)), to_chain, normalized_amount::normalize(fee_amount, decimals), ); complete_transfer::test_complete_transfer( &transfer, &mut worm_state, &mut bridge_state, &coin_meta, fee_recipient_person, ctx(&mut test) ); return_shared(bridge_state); return_shared(worm_state); return_shared>(coin_meta); }; // check balances after next_tx(&mut test, admin);{ let coins = take_from_address>(&test, admin); assert!(coin::value(&coins) == 60, 0); return_to_address>(admin, coins); let fee_coins = take_from_address>(&test, fee_recipient_person); assert!(coin::value(&fee_coins) == 40, 0); return_to_address>(fee_recipient_person, fee_coins); }; test_scenario::end(test); } #[test] #[expected_failure(abort_code = 4, location=0000000000000000000000000000000000000000::bridge_state)] // E_ORIGIN_CHAIN_MISMATCH fun test_complete_native_transfer_wrong_origin_chain(){ let (admin, fee_recipient_person, _) = people(); let test = scenario(); test = set_up_wormhole_core_and_token_bridges(admin, test); next_tx(&mut test, admin);{ native_coin_witness::test_init(ctx(&mut test)); }; // register native asset type with the token bridge next_tx(&mut test, admin);{ let coin_meta = take_shared>(&test); let bridge_state = take_shared(&test); let worm_state = take_shared(&test); bridge_state::register_native_asset( &mut worm_state, &mut bridge_state, &coin_meta, ctx(&mut test) ); native_coin_witness::test_init(ctx(&mut test)); return_shared>(coin_meta); return_shared(bridge_state); return_shared(worm_state); }; // create a treasury cap for the native asset type, mint some tokens, // and deposit the native tokens into the token bridge next_tx(&mut test, admin); { let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let t_cap = take_shared>(&test); let coins = coin::mint(&mut t_cap, 10000000000, ctx(&mut test)); bridge_state::deposit(&mut bridge_state, coins); return_shared>(t_cap); return_shared(bridge_state); return_shared(worm_state); }; // attempt complete transfer next_tx(&mut test, admin); { let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let coin_meta = take_shared>(&test); let to = admin; let amount = 1000000000; let fee_amount = 100000000; let decimals = 8; let token_address = external_address::from_bytes(x"01"); let token_chain = u16::from_u64(34); // wrong chain! let to_chain = wormhole_state::get_chain_id(&worm_state); let transfer: Transfer = transfer::create( normalized_amount::normalize(amount, decimals), token_address, token_chain, external_address::from_bytes(bcs::to_bytes(&to)), to_chain, normalized_amount::normalize(fee_amount, decimals), ); complete_transfer::test_complete_transfer( &transfer, &mut worm_state, &mut bridge_state, &coin_meta, fee_recipient_person, ctx(&mut test) ); return_shared(bridge_state); return_shared(worm_state); return_shared>(coin_meta); }; test_scenario::end(test); } #[test] #[expected_failure(abort_code = 5, location=0000000000000000000000000000000000000000::bridge_state)] // E_ORIGIN_ADDRESS_MISMATCH fun test_complete_native_transfer_wrong_coin_address(){ let (admin, fee_recipient_person, _) = people(); let test = scenario(); test = set_up_wormhole_core_and_token_bridges(admin, test); next_tx(&mut test, admin);{ native_coin_witness::test_init(ctx(&mut test)); }; // register native asset type with the token bridge next_tx(&mut test, admin);{ let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let coin_meta = take_shared>(&test); bridge_state::register_native_asset( &mut worm_state, &mut bridge_state, &coin_meta, ctx(&mut test) ); native_coin_witness::test_init(ctx(&mut test)); return_shared>(coin_meta); return_shared(bridge_state); return_shared(worm_state); }; // create a treasury cap for the native asset type, mint some tokens, // and deposit the native tokens into the token bridge next_tx(&mut test, admin); { let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let t_cap = take_shared>(&test); let coins = coin::mint(&mut t_cap, 10000000000, ctx(&mut test)); bridge_state::deposit(&mut bridge_state, coins); return_shared>(t_cap); return_shared(bridge_state); return_shared(worm_state); }; // attempt complete transfer next_tx(&mut test, admin); { let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let coin_meta = take_shared>(&test); let to = admin; let amount = 1000000000; let fee_amount = 100000000; let decimals = 8; let token_address = external_address::from_bytes(x"1111"); // wrong address! let token_chain = wormhole_state::get_chain_id(&worm_state); let to_chain = wormhole_state::get_chain_id(&worm_state); let transfer: Transfer = transfer::create( normalized_amount::normalize(amount, decimals), token_address, token_chain, external_address::from_bytes(bcs::to_bytes(&to)), to_chain, normalized_amount::normalize(fee_amount, decimals), ); complete_transfer::test_complete_transfer( &transfer, &mut worm_state, &mut bridge_state, &coin_meta, fee_recipient_person, ctx(&mut test) ); return_shared(bridge_state); return_shared(worm_state); return_shared>(coin_meta); }; test_scenario::end(test); } #[test] #[expected_failure(abort_code = 2, location=0000000000000000000000000000000000000002::balance)] // E_TOO_MUCH_FEE fun test_complete_native_transfer_too_much_fee(){ let (admin, fee_recipient_person, _) = people(); let test = scenario(); test = set_up_wormhole_core_and_token_bridges(admin, test); next_tx(&mut test, admin);{ native_coin_witness::test_init(ctx(&mut test)); }; // register native asset type with the token bridge next_tx(&mut test, admin);{ let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let coin_meta = take_shared>(&test); bridge_state::register_native_asset( &mut worm_state, &mut bridge_state, &coin_meta, ctx(&mut test) ); native_coin_witness::test_init(ctx(&mut test)); return_shared>(coin_meta); return_shared(bridge_state); return_shared(worm_state); }; // create a treasury cap for the native asset type, mint some tokens, // and deposit the native tokens into the token bridge next_tx(&mut test, admin); { let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let t_cap = take_shared>(&test); let coins = coin::mint(&mut t_cap, 10000000000, ctx(&mut test)); bridge_state::deposit(&mut bridge_state, coins); return_shared>(t_cap); return_shared(bridge_state); return_shared(worm_state); }; // attempt complete transfer next_tx(&mut test, admin); { let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let coin_meta = take_shared>(&test); let to = admin; let amount = 1000000000; let fee_amount = 1000000001; // Too much fee! Can't be greater than amount let decimals = 8; let token_address = external_address::from_bytes(x"01"); let token_chain = wormhole_state::get_chain_id(&worm_state); let to_chain = wormhole_state::get_chain_id(&worm_state); let transfer: Transfer = transfer::create( normalized_amount::normalize(amount, decimals), token_address, token_chain, external_address::from_bytes(bcs::to_bytes(&to)), to_chain, normalized_amount::normalize(fee_amount, decimals), ); complete_transfer::test_complete_transfer( &transfer, &mut worm_state, &mut bridge_state, &coin_meta, fee_recipient_person, ctx(&mut test) ); return_shared(bridge_state); return_shared(worm_state); return_shared>(coin_meta); }; test_scenario::end(test); } #[test] #[expected_failure(abort_code = 1, location=0000000000000000000000000000000000000002::dynamic_field)] // E_WRONG_COIN_TYPE fun test_complete_native_transfer_wrong_coin(){ let (admin, fee_recipient_person, _) = people(); let test = scenario(); test = set_up_wormhole_core_and_token_bridges(admin, test); next_tx(&mut test, admin);{ native_coin_witness::test_init(ctx(&mut test)); }; next_tx(&mut test, admin);{ native_coin_witness_v2::test_init(ctx(&mut test)); }; // register native asset type with the token bridge next_tx(&mut test, admin);{ let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let coin_meta = take_shared>(&test); bridge_state::register_native_asset( &mut worm_state, &mut bridge_state, &coin_meta, ctx(&mut test) ); native_coin_witness::test_init(ctx(&mut test)); return_shared>(coin_meta); return_shared(bridge_state); return_shared(worm_state); }; // create a treasury cap for the native asset type, mint some tokens, // and deposit the native tokens into the token bridge next_tx(&mut test, admin); { let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let t_cap = take_shared>(&test); let coins = coin::mint(&mut t_cap, 10000000000, ctx(&mut test)); bridge_state::deposit(&mut bridge_state, coins); return_shared>(t_cap); return_shared(bridge_state); return_shared(worm_state); }; // attempt complete transfer with wrong coin type (NATIVE_COIN_WITNESS_V2) next_tx(&mut test, admin); { let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let coin_meta = take_shared>(&test); let to = admin; let amount = 1000000000; let fee_amount = 10000000; let decimals = 8; let token_address = external_address::from_bytes(x"01"); let token_chain = wormhole_state::get_chain_id(&worm_state); let to_chain = wormhole_state::get_chain_id(&worm_state); let transfer: Transfer = transfer::create( normalized_amount::normalize(amount, decimals), token_address, token_chain, external_address::from_bytes(bcs::to_bytes(&to)), to_chain, normalized_amount::normalize(fee_amount, decimals), ); complete_transfer::test_complete_transfer( &transfer, &mut worm_state, &mut bridge_state, &coin_meta, fee_recipient_person, ctx(&mut test) ); return_shared(bridge_state); return_shared(worm_state); return_shared>(coin_meta); }; test_scenario::end(test); } // the following test is for the "beefface" token from ethereum (chain id = 2), // which has 8 decimals #[test] fun complete_wrapped_transfer_test(){ let (admin, fee_recipient_person, _) = people(); let scenario = scenario(); // First register foreign chain, create wrapped asset, register wrapped asset. let test = test_register_wrapped_(admin, scenario); next_tx(&mut test, admin);{ coin_witness::test_init(ctx(&mut test)); }; // Complete transfer of wrapped asset from foreign chain to this chain. next_tx(&mut test, admin); { let bridge_state = take_shared(&test); let worm_state = take_shared(&test); let coin_meta = take_shared>(&test); let to = admin; let amount = 1000000000; let fee_amount = 100000000; let decimals = 8; let token_address = external_address::from_bytes(x"beefface"); let token_chain = u16::from_u64(2); let to_chain = wormhole_state::get_chain_id(&worm_state); let transfer: Transfer = transfer::create( normalized_amount::normalize(amount, decimals), token_address, token_chain, external_address::from_bytes(bcs::to_bytes(&to)), to_chain, normalized_amount::normalize(fee_amount, decimals), ); complete_transfer::test_complete_transfer( &transfer, &mut worm_state, &mut bridge_state, &coin_meta, fee_recipient_person, ctx(&mut test) ); return_shared(bridge_state); return_shared(worm_state); return_shared>(coin_meta); }; // check balances after next_tx(&mut test, admin);{ let coins = take_from_address>(&test, admin); assert!(coin::value(&coins) == 900000000, 0); return_to_address>(admin, coins); let fee_coins = take_from_address>(&test, fee_recipient_person); assert!(coin::value(&fee_coins) == 100000000, 0); return_to_address>(fee_recipient_person, fee_coins); }; test_scenario::end(test); } }