module token_bridge::transfer_tokens { use aptos_framework::aptos_coin::{AptosCoin}; use aptos_framework::coin::{Self, Coin}; use wormhole::u16::{Self, U16}; use wormhole::external_address::{Self, ExternalAddress}; use wormhole::emitter::{Self, EmitterCapability}; use token_bridge::state; use token_bridge::transfer; use token_bridge::transfer_result::{Self, TransferResult}; use token_bridge::transfer_with_payload; use token_bridge::normalized_amount; use token_bridge::wrapped; const E_TOO_MUCH_RELAYER_FEE: u64 = 0; public entry fun transfer_tokens_entry( sender: &signer, amount: u64, recipient_chain: u64, recipient: vector, relayer_fee: u64, nonce: u64 ) { let coins = coin::withdraw(sender, amount); let wormhole_fee = wormhole::state::get_message_fee(); let wormhole_fee_coins = coin::withdraw(sender, wormhole_fee); transfer_tokens( coins, wormhole_fee_coins, u16::from_u64(recipient_chain), external_address::from_bytes(recipient), relayer_fee, nonce ); } public fun transfer_tokens( coins: Coin, wormhole_fee_coins: Coin, recipient_chain: U16, recipient: ExternalAddress, relayer_fee: u64, nonce: u64 ): u64 { let result = transfer_tokens_internal(coins, relayer_fee); let (token_chain, token_address, normalized_amount, normalized_relayer_fee) = transfer_result::destroy(result); let transfer = transfer::create( normalized_amount, token_address, token_chain, recipient, recipient_chain, normalized_relayer_fee, ); state::publish_message( nonce, transfer::encode(transfer), wormhole_fee_coins, ) } public fun transfer_tokens_with_payload( emitter_cap: &EmitterCapability, coins: Coin, wormhole_fee_coins: Coin, recipient_chain: U16, recipient: ExternalAddress, nonce: u64, payload: vector ): u64 { let result = transfer_tokens_internal(coins, 0); let (token_chain, token_address, normalized_amount, _) = transfer_result::destroy(result); let transfer = transfer_with_payload::create( normalized_amount, token_address, token_chain, recipient, recipient_chain, emitter::get_external_address(emitter_cap), payload ); let payload = transfer_with_payload::encode(transfer); state::publish_message( nonce, payload, wormhole_fee_coins, ) } #[test_only] public fun transfer_tokens_test( coins: Coin, relayer_fee: u64, ): TransferResult { transfer_tokens_internal(coins, relayer_fee) } // transfer a native or wraped token from sender to token_bridge fun transfer_tokens_internal( coins: Coin, relayer_fee: u64, ): TransferResult { // transfer coin to token_bridge if (!coin::is_account_registered(@token_bridge)) { coin::register(&state::token_bridge_signer()); }; if (!coin::is_account_registered(@token_bridge)) { coin::register(&state::token_bridge_signer()); }; let amount = coin::value(&coins); assert!(relayer_fee <= amount, E_TOO_MUCH_RELAYER_FEE); if (state::is_wrapped_asset()) { // now we burn the wrapped coins to remove them from circulation wrapped::burn(coins); } else { coin::deposit(@token_bridge, coins); // if we're seeing this native token for the first time, store its // type info if (!state::is_registered_native_asset()) { state::set_native_asset_type_info(); }; }; let origin_info = state::origin_info(); let token_chain = state::get_origin_info_token_chain(&origin_info); let token_address = state::get_origin_info_token_address(&origin_info); let decimals_token = coin::decimals(); let normalized_amount = normalized_amount::normalize(amount, decimals_token); let normalized_relayer_fee = normalized_amount::normalize(relayer_fee, decimals_token); let transfer_result: TransferResult = transfer_result::create( token_chain, token_address, normalized_amount, normalized_relayer_fee, ); transfer_result } } #[test_only] module token_bridge::transfer_tokens_test { use aptos_framework::coin::{Self, Coin}; use aptos_framework::string::{utf8}; use aptos_framework::aptos_coin::{Self, AptosCoin}; use token_bridge::token_bridge::{Self as bridge}; use token_bridge::transfer_tokens; use token_bridge::wrapped; use token_bridge::transfer_result; use token_bridge::token_hash; use token_bridge::register_chain; use token_bridge::normalized_amount; use wormhole::external_address::{Self}; use wrapped_coin::coin::T; /// Registration VAA for the etheruem token bridge 0xdeadbeef /// +------------------------------------------------------------------------------+ /// | Wormhole VAA v1 | nonce: 1 | time: 1 | /// | guardian set #0 | #23663022 | consistency: 0 | /// |------------------------------------------------------------------------------| /// | Signature: | /// | #0: 15d405c74be6d93c3c33ed6b48d8db70dfb31e0981f8098b2a6c7583083e... | /// |------------------------------------------------------------------------------| /// | Emitter: 11111111111111111111111111111115 (Solana) | /// |==============================================================================| /// | Chain registration (TokenBridge) | /// | Emitter chain: Ethereum | /// | Emitter address: 0x00000000000000000000000000000000deadbeef (Ethereum) | /// +------------------------------------------------------------------------------+ const ETHEREUM_TOKEN_REG: vector = x"0100000000010015d405c74be6d93c3c33ed6b48d8db70dfb31e0981f8098b2a6c7583083e0c3343d4a1abeb3fc1559674fa067b0c0e2e9de2fafeaecdfeae132de2c33c9d27cc0100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000016911ae00000000000000000000000000000000000000000000546f6b656e427269646765010000000200000000000000000000000000000000000000000000000000000000deadbeef"; /// Attestation VAA sent from the ethereum token bridge 0xdeadbeef /// +------------------------------------------------------------------------------+ /// | Wormhole VAA v1 | nonce: 1 | time: 1 | /// | guardian set #0 | #22080291 | consistency: 0 | /// |------------------------------------------------------------------------------| /// | Signature: | /// | #0: 80366065746148420220f25a6275097370e8db40984529a6676b7a5fc9fe... | /// |------------------------------------------------------------------------------| /// | Emitter: 0x00000000000000000000000000000000deadbeef (Ethereum) | /// |==============================================================================| /// | Token attestation | /// | decimals: 12 | /// | Token: 0x00000000000000000000000000000000beefface (Ethereum) | /// | Symbol: BEEF | /// | Name: Beef face Token | /// +------------------------------------------------------------------------------+ const ATTESTATION_VAA: vector = x"0100000000010080366065746148420220f25a6275097370e8db40984529a6676b7a5fc9feb11755ec49ca626b858ddfde88d15601f85ab7683c5f161413b0412143241c700aff010000000100000001000200000000000000000000000000000000000000000000000000000000deadbeef000000000150eb23000200000000000000000000000000000000000000000000000000000000beefface00020c424545460000000000000000000000000000000000000000000000000000000042656566206661636520546f6b656e0000000000000000000000000000000000"; struct MyCoin has key {} fun init_my_token(admin: &signer, amount: u64): Coin { let name = utf8(b"mycoindd"); let symbol = utf8(b"MCdd"); let decimals = 6; let monitor_supply = true; let (burn_cap, freeze_cap, mint_cap) = coin::initialize(admin, name, symbol, decimals, monitor_supply); let coins = coin::mint(amount, &mint_cap); coin::destroy_burn_cap(burn_cap); coin::destroy_mint_cap(mint_cap); coin::destroy_freeze_cap(freeze_cap); coins } fun setup( aptos_framework: &signer, token_bridge: &signer, deployer: &signer, ) { // we initialise the bridge with zero fees to avoid having to mint fee // tokens in these tests. The wormolhe fee handling is already tested // in wormhole.move, so it's unnecessary here. let (burn_cap, mint_cap) = aptos_coin::initialize_for_test(aptos_framework); wormhole::wormhole_test::setup(0); bridge::init_test(deployer); coin::register(deployer); coin::register(token_bridge); //how important is this registration step and where to check it? coin::destroy_burn_cap(burn_cap); coin::destroy_mint_cap(mint_cap); } // test transfer wrapped coin #[test(aptos_framework = @aptos_framework, token_bridge=@token_bridge, deployer=@deployer)] fun test_transfer_wrapped_token(aptos_framework: &signer, token_bridge: &signer, deployer: &signer) { setup(aptos_framework, token_bridge, deployer); register_chain::submit_vaa(ETHEREUM_TOKEN_REG); // TODO(csongor): create a better error message when attestation is missing wrapped::create_wrapped_coin_type(ATTESTATION_VAA); // TODO(csongor): write a blurb about why this test works (something // something static linking) // initialize coin using type T, move caps to token_bridge, sets bridge state variables wrapped::create_wrapped_coin(ATTESTATION_VAA); // test transfer wrapped tokens let beef_coins = wrapped::mint(100000); assert!(coin::supply() == std::option::some(100000), 0); let result = transfer_tokens::transfer_tokens_test( beef_coins, 2, ); let (token_chain, token_address, normalized_amount, normalized_relayer_fee) = transfer_result::destroy(result); // make sure the wrapped assets have been burned assert!(coin::supply() == std::option::some(0), 0); assert!(token_chain == wormhole::u16::from_u64(2), 0); assert!(external_address::get_bytes(&token_address) == x"00000000000000000000000000000000000000000000000000000000beefface", 0); // the original coin has 12 decimals, but wrapped assets are capped at 8 // decimals, so the normalized amount matches the transferred amount. assert!(normalized_amount::get_amount(normalized_amount) == 100000, 0); assert!(normalized_amount::get_amount(normalized_relayer_fee) == 2, 0); } #[test(aptos_framework = @aptos_framework, token_bridge=@token_bridge, deployer=@deployer)] #[expected_failure(abort_code = 0, location = token_bridge::transfer_tokens)] fun test_transfer_wrapped_token_too_much_relayer_fee( aptos_framework: &signer, token_bridge: &signer, deployer: &signer ) { setup(aptos_framework, token_bridge, deployer); register_chain::submit_vaa(ETHEREUM_TOKEN_REG); wrapped::create_wrapped_coin_type(ATTESTATION_VAA); wrapped::create_wrapped_coin(ATTESTATION_VAA); // this will fail because the relayer fee exceeds the amount let beef_coins = wrapped::mint(100000); assert!(coin::supply() == std::option::some(100000), 0); let result = transfer_tokens::transfer_tokens_test(beef_coins, 200000); let (_, _, _, _) = transfer_result::destroy(result); } // test transfer native coin #[test(aptos_framework = @aptos_framework, token_bridge=@token_bridge, deployer=@deployer)] fun test_transfer_native_token(aptos_framework: &signer, token_bridge: &signer, deployer: &signer) { setup(aptos_framework, token_bridge, deployer); let my_coins = init_my_token(token_bridge, 10000); // make sure the token bridge is not registered yet for this coin assert!(!coin::is_account_registered(@token_bridge), 0); let result = transfer_tokens::transfer_tokens_test(my_coins, 500); // the token bridge should now be registered and hold the balance assert!(coin::balance(@token_bridge) == 10000, 0); let (token_chain, token_address, normalized_amount, normalized_relayer_fee) = transfer_result::destroy(result); assert!(token_chain == wormhole::state::get_chain_id(), 0); assert!(token_address == token_hash::get_external_address(&token_hash::derive()), 0); // the coin has 6 decimals, so the amount doesn't get scaled assert!(normalized_amount::get_amount(normalized_amount) == 10000, 0); assert!(normalized_amount::get_amount(normalized_relayer_fee) == 500, 0); } }