From be0f58a53785ffe70e4b76d0b5e85f76c64bf498 Mon Sep 17 00:00:00 2001 From: Csongor Kiss Date: Fri, 14 Oct 2022 01:19:23 +0000 Subject: [PATCH] aptos/contracts: implement token bridge --- aptos/token_bridge/Makefile | 14 + aptos/token_bridge/Move.toml | 27 ++ aptos/token_bridge/sources/attest_token.move | 143 +++++++ .../sources/complete_transfer.move | 376 ++++++++++++++++++ .../complete_transfer_with_payload.move | 59 +++ .../sources/contract_upgrade.move | 208 ++++++++++ aptos/token_bridge/sources/deploy_coin.move | 38 ++ .../sources/newtypes/normalized_amount.move | 74 ++++ .../sources/newtypes/string32.move | 171 ++++++++ .../token_bridge/sources/register_chain.move | 161 ++++++++ aptos/token_bridge/sources/state.move | 257 ++++++++++++ .../sources/structs/asset_meta.move | 117 ++++++ .../sources/structs/transfer.move | 149 +++++++ .../sources/structs/transfer_result.move | 44 ++ .../structs/transfer_with_payload.move | 122 ++++++ aptos/token_bridge/sources/token_bridge.move | 33 ++ aptos/token_bridge/sources/token_hash.move | 64 +++ .../token_bridge/sources/transfer_tokens.move | 286 +++++++++++++ aptos/token_bridge/sources/vaa.move | 141 +++++++ aptos/token_bridge/sources/wrapped.move | 255 ++++++++++++ 20 files changed, 2739 insertions(+) create mode 100644 aptos/token_bridge/Makefile create mode 100644 aptos/token_bridge/Move.toml create mode 100644 aptos/token_bridge/sources/attest_token.move create mode 100644 aptos/token_bridge/sources/complete_transfer.move create mode 100644 aptos/token_bridge/sources/complete_transfer_with_payload.move create mode 100644 aptos/token_bridge/sources/contract_upgrade.move create mode 100644 aptos/token_bridge/sources/deploy_coin.move create mode 100644 aptos/token_bridge/sources/newtypes/normalized_amount.move create mode 100644 aptos/token_bridge/sources/newtypes/string32.move create mode 100644 aptos/token_bridge/sources/register_chain.move create mode 100644 aptos/token_bridge/sources/state.move create mode 100644 aptos/token_bridge/sources/structs/asset_meta.move create mode 100644 aptos/token_bridge/sources/structs/transfer.move create mode 100644 aptos/token_bridge/sources/structs/transfer_result.move create mode 100644 aptos/token_bridge/sources/structs/transfer_with_payload.move create mode 100644 aptos/token_bridge/sources/token_bridge.move create mode 100644 aptos/token_bridge/sources/token_hash.move create mode 100644 aptos/token_bridge/sources/transfer_tokens.move create mode 100644 aptos/token_bridge/sources/vaa.move create mode 100644 aptos/token_bridge/sources/wrapped.move diff --git a/aptos/token_bridge/Makefile b/aptos/token_bridge/Makefile new file mode 100644 index 000000000..025c38ddb --- /dev/null +++ b/aptos/token_bridge/Makefile @@ -0,0 +1,14 @@ +-include ../../Makefile.help + +.PHONY: artifacts +artifacts: build + +.PHONY: build +## Build contract +build: + aptos move compile --save-metadata + +.PHONY: test +## Run tests +test: + aptos move test diff --git a/aptos/token_bridge/Move.toml b/aptos/token_bridge/Move.toml new file mode 100644 index 000000000..46033822d --- /dev/null +++ b/aptos/token_bridge/Move.toml @@ -0,0 +1,27 @@ +[package] +name = "TokenBridge" +version = "0.0.1" + +#TODO: pin versions before mainnet release +[dependencies] +AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework/", rev = "main" } +MoveStdlib = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/move-stdlib/", rev = "main" } +AptosStdlib = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-stdlib/", rev = "main" } +AptosToken = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-token/", rev = "main" } +Wormhole = { local = "../wormhole/" } +Deployer = { local = "../deployer/" } + +# only included in testing +[dev-dependencies] +WrappedCoin = { local = "../coin/" } + +[dev-addresses] +# derived address from sha3_256(deployer | "token_bridge" | 0xFF) by running +# worm aptos derive-resource-account 0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b token_bridge +token_bridge = "0x84a5f374d29fc77e370014dce4fd6a55b58ad608de8074b0be5571701724da31" +deployer = "0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b" +wrapped_coin = "0xf4f53cc591e5190eddbc43940746e2b5deea6e0e1562b2bba765d488504842c7" +wormhole = "0xde0036a9600559e295d5f6802ef6f3f802f510366e0c23912b0655d972166017" + +[addresses] +token_bridge = "_" diff --git a/aptos/token_bridge/sources/attest_token.move b/aptos/token_bridge/sources/attest_token.move new file mode 100644 index 000000000..bb06a8373 --- /dev/null +++ b/aptos/token_bridge/sources/attest_token.move @@ -0,0 +1,143 @@ +module token_bridge::attest_token { + use aptos_framework::aptos_coin::{AptosCoin}; + use aptos_framework::coin::{Self, Coin}; + + use token_bridge::asset_meta::{Self, AssetMeta}; + use token_bridge::state; + use token_bridge::token_hash; + use token_bridge::string32; + + const E_COIN_IS_NOT_INITIALIZED: u64 = 0; + /// Wrapped assets can't be attested + const E_WRAPPED_ASSET: u64 = 1; + + public entry fun attest_token_with_signer(user: &signer): u64 { + let message_fee = wormhole::state::get_message_fee(); + let fee_coins = coin::withdraw(user, message_fee); + attest_token(fee_coins) + } + + public fun attest_token(fee_coins: Coin): u64 { + let asset_meta: AssetMeta = attest_token_internal(); + let payload: vector = asset_meta::encode(asset_meta); + let nonce = 0; + state::publish_message( + nonce, + payload, + fee_coins + ) + } + + #[test_only] + public fun attest_token_test(): AssetMeta { + attest_token_internal() + } + + fun attest_token_internal(): AssetMeta { + // wrapped assets and uninitialised type can't be attested. + assert!(!state::is_wrapped_asset(), E_WRAPPED_ASSET); + assert!(coin::is_coin_initialized(), E_COIN_IS_NOT_INITIALIZED); // not tested + + let token_address = token_hash::derive(); + if (!state::is_registered_native_asset()) { + // if native asset is not registered, register it in the reverse look-up map + state::set_native_asset_type_info(); + }; + let token_chain = wormhole::state::get_chain_id(); + let decimals = coin::decimals(); + let symbol = string32::from_string(&coin::symbol()); + let name = string32::from_string(&coin::name()); + asset_meta::create( + token_hash::get_external_address(&token_address), + token_chain, + decimals, + symbol, + name + ) + } +} + +#[test_only] +module token_bridge::attest_token_test { + use aptos_framework::coin; + use aptos_framework::string::utf8; + use aptos_framework::type_info::type_of; + + use token_bridge::token_bridge::{Self as bridge}; + use token_bridge::state; + use token_bridge::attest_token; + use token_bridge::token_hash; + use token_bridge::asset_meta; + use token_bridge::string32; + use token_bridge::wrapped_test; + + struct MyCoin has key {} + + fun setup( + 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. + wormhole::wormhole_test::setup(0); + bridge::init_test(deployer); + + init_my_token(token_bridge); + } + + fun init_my_token(admin: &signer) { + let name = utf8(b"Some test coin"); + let symbol = utf8(b"TEST"); + let decimals = 10; + let monitor_supply = true; + let (burn_cap, freeze_cap, mint_cap) = coin::initialize(admin, name, symbol, decimals, monitor_supply); + coin::destroy_burn_cap(burn_cap); + coin::destroy_freeze_cap(freeze_cap); + coin::destroy_mint_cap(mint_cap); + } + + #[test(token_bridge=@token_bridge, deployer=@deployer)] + fun test_attest_token(token_bridge: &signer, deployer: &signer) { + use std::string; + + setup(token_bridge, deployer); + let asset_meta = attest_token::attest_token_test(); + + let token_address = asset_meta::get_token_address(&asset_meta); + let token_chain = asset_meta::get_token_chain(&asset_meta); + let decimals = asset_meta::get_decimals(&asset_meta); + let symbol = string32::to_string(&asset_meta::get_symbol(&asset_meta)); + let name = string32::to_string(&asset_meta::get_name(&asset_meta)); + + assert!(token_address == token_hash::get_external_address(&token_hash::derive()), 0); + assert!(token_chain == wormhole::u16::from_u64(22), 0); + assert!(decimals == 10, 0); + assert!(name == string::utf8(b"Some test coin"), 0); + assert!(symbol == string::utf8(b"TEST"), 0); + } + + #[test(token_bridge=@token_bridge, deployer=@deployer)] + #[expected_failure(abort_code = 1)] + fun test_attest_wrapped_token(token_bridge: &signer, deployer: &signer) { + setup(token_bridge, deployer); + wrapped_test::init_wrapped_token(); + // this should fail because T is a wrapped asset + let _asset_meta = attest_token::attest_token_test(); + } + + #[test(token_bridge=@token_bridge, deployer=@deployer)] + fun test_attest_token_with_signer(token_bridge: &signer, deployer: &signer) { + setup(token_bridge, deployer); + let asset_meta1 = attest_token::attest_token_test(); + + // check that native asset is registered with State + let token_address = token_hash::derive(); + assert!(state::native_asset_info(token_address) == type_of(), 0); + + // attest same token a second time, should have no change in behavior + let asset_meta2 = attest_token::attest_token_test(); + assert!(asset_meta1 == asset_meta2, 0); + assert!(state::native_asset_info(token_address) == type_of(), 0); + } +} diff --git a/aptos/token_bridge/sources/complete_transfer.move b/aptos/token_bridge/sources/complete_transfer.move new file mode 100644 index 000000000..f567302c8 --- /dev/null +++ b/aptos/token_bridge/sources/complete_transfer.move @@ -0,0 +1,376 @@ +module token_bridge::complete_transfer { + use aptos_std::from_bcs; + use aptos_framework::coin::{Self, Coin}; + + use token_bridge::vaa; + use token_bridge::transfer::{Self, Transfer}; + use token_bridge::state; + use token_bridge::wrapped; + use token_bridge::normalized_amount; + + use wormhole::external_address::get_bytes; + + const E_INVALID_TARGET: u64 = 0; + + public entry fun submit_vaa(vaa: vector, fee_recipient: address): Transfer { + let vaa = vaa::parse_verify_and_replay_protect(vaa); + let transfer = transfer::parse(wormhole::vaa::destroy(vaa)); + complete_transfer(&transfer, fee_recipient); + transfer + } + + #[test_only] + public fun test(transfer: &Transfer, fee_recipient: address) { + complete_transfer(transfer, fee_recipient) + } + + fun complete_transfer(transfer: &Transfer, fee_recipient: address) { + let to_chain = transfer::get_to_chain(transfer); + assert!(to_chain == wormhole::state::get_chain_id(), E_INVALID_TARGET); + + let token_chain = transfer::get_token_chain(transfer); + let token_address = transfer::get_token_address(transfer); + let origin_info = state::create_origin_info(token_chain, token_address); + + state::assert_coin_origin_info(origin_info); + + let decimals = coin::decimals(); + + let amount = normalized_amount::denormalize(transfer::get_amount(transfer), decimals); + let fee_amount = normalized_amount::denormalize(transfer::get_fee(transfer), decimals); + + let recipient = from_bcs::to_address(get_bytes(&transfer::get_to(transfer))); + + let recipient_coins: Coin; + + if (state::is_wrapped_asset()) { + recipient_coins = wrapped::mint(amount); + } else { + let token_bridge = state::token_bridge_signer(); + recipient_coins = coin::withdraw(&token_bridge, amount); + }; + + // take out fee from the recipient's coins. `extract` will revert + // if fee > amount + let fee_coins = coin::extract(&mut recipient_coins, fee_amount); + coin::deposit(recipient, recipient_coins); + coin::deposit(fee_recipient, fee_coins); + } + +} + +#[test_only] +module token_bridge::complete_transfer_test { + use std::bcs; + use std::signer; + use aptos_framework::coin; + use aptos_framework::account; + use aptos_framework::string::{utf8}; + + use token_bridge::transfer::{Self, Transfer}; + use token_bridge::transfer_tokens; + use token_bridge::token_hash; + use token_bridge::complete_transfer; + use token_bridge::token_bridge; + use token_bridge::wrapped; + use token_bridge::transfer_result; + use token_bridge::normalized_amount; + + use token_bridge::wrapped_test; + + use wormhole::state; + use wormhole::wormhole_test; + use wormhole::external_address; + + struct MyCoin {} + + struct OtherCoin {} + + fun init_my_token(admin: &signer, decimals: u8, amount: u64) { + let name = utf8(b"mycoindd"); + let symbol = utf8(b"MCdd"); + let monitor_supply = true; + let (burn_cap, freeze_cap, mint_cap) = coin::initialize(admin, name, symbol, decimals, monitor_supply); + coin::destroy_freeze_cap(freeze_cap); + coin::destroy_burn_cap(burn_cap); + coin::register(admin); + coin::deposit(signer::address_of(admin), coin::mint(amount, &mint_cap)); + coin::destroy_mint_cap(mint_cap); + } + + public fun setup( + deployer: &signer, + token_bridge: &signer, + to: address, + fee_recipient: address, + decimals: u8, + amount: u64, + ) { + // initialise wormhole and token bridge + wormhole_test::setup(0); + token_bridge::init_test(deployer); + + // initialise MyToken + init_my_token(token_bridge, decimals, amount); + + // initialise 'to' and 'fee_recipient' and register them to accept MyCoins + let to = &account::create_account_for_test(to); + let fee_recipient = &account::create_account_for_test(fee_recipient); + coin::register(to); + coin::register(fee_recipient); + + // initialise wrapped token + wrapped_test::init_wrapped_token(); + coin::register(to); + coin::register(fee_recipient); + + } + + + #[test( + deployer = @deployer, + token_bridge = @token_bridge, + )] + public fun test_native_transfer_10_decimals( + deployer: &signer, + token_bridge: &signer + ) { + let to = @0x12; + let fee_recipient = @0x32; + // the dust at the end will be removed during normalisation/denormalisation + let amount = 10010; + let fee_amount = 4000; + let decimals = 10; + + setup(deployer, token_bridge, to, fee_recipient, decimals, amount); + + let token_address = token_hash::get_external_address(&token_hash::derive()); + let token_chain = state::get_chain_id(); + let to_chain = state::get_chain_id(); + 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), + ); + + assert!(coin::balance(to) == 0, 0); + assert!(coin::balance(fee_recipient) == 0, 0); + complete_transfer::test(&transfer, fee_recipient); + assert!(coin::balance(to) == 6000, 0); + assert!(coin::balance(fee_recipient) == 4000, 0); + } + + #[test( + deployer = @deployer, + token_bridge = @token_bridge, + )] + public fun test_native_transfer_4_decimals( + deployer: &signer, + token_bridge: &signer + ) { + let to = @0x12; + let fee_recipient = @0x32; + let amount = 100; + let fee_amount = 40; + let decimals = 4; + + // the token has 4 decimals, so no scaling is expected + setup(deployer, token_bridge, to, fee_recipient, decimals, amount); + + let token_address = token_hash::get_external_address(&token_hash::derive()); + let token_chain = state::get_chain_id(); + let to_chain = state::get_chain_id(); + 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), + ); + + assert!(coin::balance(to) == 0, 0); + assert!(coin::balance(fee_recipient) == 0, 0); + complete_transfer::test(&transfer, fee_recipient); + assert!(coin::balance(to) == 60, 0); + assert!(coin::balance(fee_recipient) == 40, 0); + } + + #[test( + deployer = @deployer, + token_bridge = @token_bridge, + )] + #[expected_failure(abort_code = 65542)] // EINSUFFICIENT_BALANCE + public fun test_native_too_much_fee( + deployer: &signer, + token_bridge: &signer + ) { + let to = @0x12; + let fee_recipient = @0x32; + let amount = 100; + let fee_amount = 101; // FAIL: too much fee + let decimals = 8; + + setup(deployer, token_bridge, to, fee_recipient, decimals, amount); + + let token_address = token_hash::get_external_address(&token_hash::derive()); + let token_chain = state::get_chain_id(); + let to_chain = state::get_chain_id(); + 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(&transfer, fee_recipient); + } + + #[test( + deployer = @deployer, + token_bridge = @token_bridge, + )] + #[expected_failure(abort_code = 1)] // E_ORIGIN_ADDRESS_MISMATCH + public fun test_native_wrong_coin( + deployer: &signer, + token_bridge: &signer + ) { + let to = @0x12; + let fee_recipient = @0x32; + let amount = 100; + let fee_amount = 40; + let decimals = 8; + + setup(deployer, token_bridge, to, fee_recipient, decimals, amount); + + let token_address = token_hash::get_external_address(&token_hash::derive()); + let token_chain = state::get_chain_id(); + let to_chain = state::get_chain_id(); + 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), + ); + // FAIL: wrong type argument + complete_transfer::test(&transfer, fee_recipient); + } + + #[test( + deployer = @deployer, + token_bridge = @token_bridge, + )] + #[expected_failure(abort_code = 0)] // E_ORIGIN_CHAIN_MISMATCH + public fun test_native_wrong_origin_address( + deployer: &signer, + token_bridge: &signer + ) { + let to = @0x12; + let fee_recipient = @0x32; + let amount = 100; + let fee_amount = 40; + let decimals = 8; + + setup(deployer, token_bridge, to, fee_recipient, decimals, amount); + + let token_address = token_hash::get_external_address(&token_hash::derive()); + let token_chain = wormhole::u16::from_u64(10); // FAIL: wrong origin chain (MyCoin is native) + let to_chain = state::get_chain_id(); + 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(&transfer, fee_recipient); + } + + #[test( + deployer = @deployer, + token_bridge = @token_bridge, + )] + public fun test_wrapped_transfer_roundtrip( + deployer: &signer, + token_bridge: &signer + ) { + let to = @0x12; + let fee_recipient = @0x32; + + setup(deployer, token_bridge, to, fee_recipient, 8, 0); + + let beef_coins = wrapped::mint(100000); + + let result = transfer_tokens::transfer_tokens_test( + beef_coins, + 5000 + ); + + let (token_chain, token_address, normalized_amount, normalized_relayer_fee) + = transfer_result::destroy(result); + + + let to_chain = state::get_chain_id(); + let transfer: Transfer = transfer::create( + normalized_amount, + token_address, + token_chain, + external_address::from_bytes(bcs::to_bytes(&to)), + to_chain, + normalized_relayer_fee, + ); + + assert!(coin::balance(to) == 0, 0); + assert!(coin::balance(fee_recipient) == 0, 0); + + complete_transfer::test(&transfer, fee_recipient); + + assert!(coin::balance(to) == 95000, 0); + assert!(coin::balance(fee_recipient) == 5000, 0); + } + + #[test( + deployer = @deployer, + token_bridge = @token_bridge, + )] + public fun test_wrapped_transfer( + deployer: &signer, + token_bridge: &signer + ) { + let to = @0x12; + let fee_recipient = @0x32; + let amount = 100; + let fee_amount = 40; + let decimals = 8; + + setup(deployer, token_bridge, to, fee_recipient, decimals, 0); + + let token_address = external_address::from_bytes(x"deadbeef"); + let token_chain = wormhole::u16::from_u64(2); + let to_chain = state::get_chain_id(); + 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), + ); + + assert!(coin::balance(to) == 0, 0); + assert!(coin::balance(fee_recipient) == 0, 0); + + complete_transfer::test(&transfer, fee_recipient); + + // the wrapped asset has 9 decimals (see wrapped_test::init_wrapped_token) + assert!(coin::balance(to) == 600, 0); + assert!(coin::balance(fee_recipient) == 400, 0); + } +} diff --git a/aptos/token_bridge/sources/complete_transfer_with_payload.move b/aptos/token_bridge/sources/complete_transfer_with_payload.move new file mode 100644 index 000000000..22ea13dad --- /dev/null +++ b/aptos/token_bridge/sources/complete_transfer_with_payload.move @@ -0,0 +1,59 @@ +module token_bridge::complete_transfer_with_payload { + use aptos_framework::coin::{Self, Coin}; + + use token_bridge::vaa; + use token_bridge::transfer_with_payload::{Self as transfer, TransferWithPayload}; + use token_bridge::state; + use token_bridge::wrapped; + use token_bridge::normalized_amount; + + use wormhole::emitter::{Self, EmitterCapability}; + + const E_INVALID_RECIPIENT: u64 = 0; + const E_INVALID_TARGET: u64 = 1; + + // TODO(csongor): document this, and create an example contract receiving + // such a transfer + public entry fun submit_vaa( + vaa: vector, + emitter_cap: &EmitterCapability + ): (Coin, TransferWithPayload) { + let vaa = vaa::parse_verify_and_replay_protect(vaa); + let transfer = transfer::parse(wormhole::vaa::destroy(vaa)); + + let to_chain = transfer::get_to_chain(&transfer); + assert!(to_chain == wormhole::state::get_chain_id(), E_INVALID_TARGET); + + let token_chain = transfer::get_token_chain(&transfer); + let token_address = transfer::get_token_address(&transfer); + let origin_info = state::create_origin_info(token_chain, token_address); + + state::assert_coin_origin_info(origin_info); + + let decimals = coin::decimals(); + + let amount = normalized_amount::denormalize(transfer::get_amount(&transfer), decimals); + + // transfers with payload can only be redeemed by the recipient. + let recipient = transfer::get_to(&transfer); + assert!( + recipient == emitter::get_external_address(emitter_cap), + E_INVALID_RECIPIENT + ); + + let recipient_coins: Coin; + + if (state::is_wrapped_asset()) { + recipient_coins = wrapped::mint(amount); + } else { + let token_bridge = state::token_bridge_signer(); + recipient_coins = coin::withdraw(&token_bridge, amount); + }; + + (recipient_coins, transfer) + } +} + +#[test_only] +module token_bridge::complete_transfer_with_payload_test { +} diff --git a/aptos/token_bridge/sources/contract_upgrade.move b/aptos/token_bridge/sources/contract_upgrade.move new file mode 100644 index 000000000..53d2377cb --- /dev/null +++ b/aptos/token_bridge/sources/contract_upgrade.move @@ -0,0 +1,208 @@ +/// This module implements upgradeability for the token bridge contract. +/// +/// Contract upgrades are authorised by governance, which means that performing +/// an upgrade requires a governance VAA signed by a supermajority of the +/// wormhole guardians. +/// +/// Upgrades are performed in a commit-reveal scheme, where submitting the VAA +/// authorises a particular contract hash. Then in a subsequent transaction, the +/// bytecode is uploaded, and if the hash of the bytecode matches the committed +/// hash, then the upgrade proceeds. +/// +/// This two-phase process has the advantage that even if the bytecode can't be +/// upgraded to for whatever reason, the governance VAA won't be possible to +/// replay in the future, since the commit transaction replay protects it. +/// +/// Additionally, there is an optional migration step that may include one-off +/// logic to be executed after the upgrade. This has to be done in a separate +/// transaction, because the transaction that uploads bytecode cannot execute +/// it. +module token_bridge::contract_upgrade { + use std::vector; + use aptos_framework::code; + use wormhole::deserialize; + use wormhole::cursor; + use wormhole::vaa; + use wormhole::state as core; + use wormhole::keccak256::keccak256; + + use token_bridge::vaa as token_bridge_vaa; + use token_bridge::state; + + /// "TokenBridge" (left padded) + const TOKEN_BRIDGE: vector = x"000000000000000000000000000000000000000000546f6b656e427269646765"; + + const E_UPGRADE_UNAUTHORIZED: u64 = 0; + const E_UNEXPECTED_HASH: u64 = 1; + const E_INVALID_MODULE: u64 = 2; + const E_INVALID_ACTION: u64 = 3; + const E_INVALID_TARGET: u64 = 4; + const E_NOT_MIGRATING: u64 = 5; + + /// The `UpgradeAuthorized` type in the global storage represents the fact + /// there is an ongoing approved upgrade. + /// When the upgrade is finalised in `upgrade`, this object is deleted. + struct UpgradeAuthorized has key { + hash: vector + } + + struct Hash { + hash: vector + } + + fun parse_payload(payload: vector): Hash { + let cur = cursor::init(payload); + let target_module = deserialize::deserialize_vector(&mut cur, 32); + + assert!(target_module == TOKEN_BRIDGE, E_INVALID_MODULE); + + let action = deserialize::deserialize_u8(&mut cur); + assert!(action == 0x02, E_INVALID_ACTION); + + let chain = deserialize::deserialize_u16(&mut cur); + assert!(chain == core::get_chain_id(), E_INVALID_TARGET); + + let hash = deserialize::deserialize_vector(&mut cur, 32); + + cursor::destroy_empty(cur); + + Hash { hash } + } + +// ----------------------------------------------------------------------------- +// Commit + + public entry fun submit_vaa(vaa: vector) acquires UpgradeAuthorized { + let vaa = vaa::parse_and_verify(vaa); + vaa::assert_governance(&vaa); + token_bridge_vaa::replay_protect(&vaa); + + authorize_upgrade(parse_payload(vaa::destroy(vaa))); + } + + fun authorize_upgrade(hash: Hash) acquires UpgradeAuthorized { + let Hash { hash } = hash; + let token_bridge = state::token_bridge_signer(); + if (exists(@token_bridge)) { + // TODO(csongor): here we're dropping the upgrade hash, in case an + // upgrade fails for some reason. Should we emit a log or something? + let UpgradeAuthorized { hash: _ } = move_from(@token_bridge); + }; + move_to(&token_bridge, UpgradeAuthorized { hash }); + } + + #[test_only] + public fun authorized_hash(): vector acquires UpgradeAuthorized { + let u = borrow_global(@token_bridge); + u.hash + } + +// ----------------------------------------------------------------------------- +// Reveal + + public entry fun upgrade( + metadata_serialized: vector, + code: vector> + ) acquires UpgradeAuthorized { + assert!(exists(@token_bridge), E_UPGRADE_UNAUTHORIZED); + let UpgradeAuthorized { hash } = move_from(@token_bridge); + + // we compute the hash of hashes of the metadata and the bytecodes. + // the aptos framework appears to perform no validation of the metadata, + // so we check it here too. + let c = copy code; + vector::reverse(&mut c); + let a = keccak256(metadata_serialized); + while (!vector::is_empty(&c)) vector::append(&mut a, keccak256(vector::pop_back(&mut c))); + assert!(keccak256(a) == hash, E_UNEXPECTED_HASH); + + let token_bridge = state::token_bridge_signer(); + code::publish_package_txn(&token_bridge, metadata_serialized, code); + + // allow migration to be run. + if (!exists(@token_bridge)) { + move_to(&token_bridge, Migrating {}); + } + } + +// ----------------------------------------------------------------------------- +// Migration + + struct Migrating has key {} + + public fun is_migrating(): bool { + exists(@token_bridge) + } + + public entry fun migrate() acquires Migrating { + assert!(exists(@token_bridge), E_NOT_MIGRATING); + let Migrating { } = move_from(@token_bridge); + + // NOTE: put any one-off migration logic here. + // Most upgrades likely won't need to do anything, in which case the + // rest of this function's body may be empty. + // Make sure to delete it after the migration has gone through + // successfully. + // WARNING: the migration does *not* proceed atomically with the + // upgrade (as they are done in separate transactions). + // If the nature of your migration absolutely requires the migration to + // happen before certain other functionality is available, then guard + // that functionality with `assert!(!is_migrating())` (from above). + } +} + +#[test_only] +module token_bridge::contract_upgrade_test { + use wormhole::wormhole; + + use token_bridge::contract_upgrade; + use token_bridge::token_bridge; + + /// A token bridge upgrade VAA that upgrades to 0x10263f154c466b139fda0bf2caa08fd9819d8ded3810446274a99399f886fc76 + const UPGRADE_VAA: vector = x"01000000000100b5ebfcccb84d740684429622f2fbc16638fb01222e4a580a6d2049227f37a31a7162d32770f72398fe10d160a968c94256eae9225a3da9c69ab7a41d7b307ede010000000100000001000100000000000000000000000000000000000000000000000000000000000000040000000001f96c9900000000000000000000000000000000000000000000546f6b656e42726964676502001610263f154c466b139fda0bf2caa08fd9819d8ded3810446274a99399f886fc76"; + + /// A token bridge upgrade VAA that targets ethereum + const ETH_UPGRADE: vector = x"0100000000010090014add41120b33eb4a03c5dce613815071d18b69a185bf322f327cc79cc52d7d133a59515d13ccfb030f9cc26a86b2bcd4dbe34d8ca6c4cc83299efb3e9b430100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000030a9ea600000000000000000000000000000000000000000000546f6b656e42726964676502000210263f154c466b139fda0bf2caa08fd9819d8ded3810446274a99399f886fc76"; + + fun setup(deployer: &signer) { + let aptos_framework = std::account::create_account_for_test(@aptos_framework); + std::timestamp::set_time_has_started_for_testing(&aptos_framework); + wormhole::init_test( + 22, + 1, + x"0000000000000000000000000000000000000000000000000000000000000004", + x"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe", + 0 + ); + token_bridge::init_test(deployer); + } + + #[test(deployer = @deployer)] + public fun test_contract_upgrade_authorize(deployer: &signer) { + setup(deployer); + + contract_upgrade::submit_vaa(UPGRADE_VAA); + let expected_hash = x"10263f154c466b139fda0bf2caa08fd9819d8ded3810446274a99399f886fc76"; + + assert!(contract_upgrade::authorized_hash() == expected_hash, 0); + } + + #[test(deployer = @deployer)] + #[expected_failure(abort_code = 0x6407)] + public fun test_contract_upgrade_double(deployer: &signer) { + setup(deployer); + + // make sure we can't replay a VAA + contract_upgrade::submit_vaa(UPGRADE_VAA); + contract_upgrade::submit_vaa(UPGRADE_VAA); + } + + #[test(deployer = @deployer)] + #[expected_failure(abort_code = 4)] + public fun test_contract_upgrade_wrong_chain(deployer: &signer) { + setup(deployer); + + contract_upgrade::submit_vaa(ETH_UPGRADE); + } + +} diff --git a/aptos/token_bridge/sources/deploy_coin.move b/aptos/token_bridge/sources/deploy_coin.move new file mode 100644 index 000000000..c28040ba9 --- /dev/null +++ b/aptos/token_bridge/sources/deploy_coin.move @@ -0,0 +1,38 @@ +// This module is for dynamically deploying a module/CoinType on-chain, +// as opposed to submitting a tx from off-chain to publish a module containing the CoinType. +// +// Specifically, we wish to dynamically deploy the the following module +// +// module deployer::coin { +// struct T has key {} +// } +// +// where deployer is a fixed address, but which will be spliced out on-chain. +// +// We create a Move package [INSERT_LINK_HERE], compile it using "aptos move compile --save-metadata", obtain the +// package_metadata.bcs and coin.mv files, then upload them on-chain in byte format (see the deployCoin function below). +// +// We replace the deployer address embedded in the source code with the new deployer address and call publish_package_txn +// to publish the code at the new deployer's account. +// +// +// TODO: find out if we need to update the source_digest in the package metadata as well + +module token_bridge::deploy_coin { + use std::signer::{Self}; + use std::vector::{Self}; + use std::code::{publish_package_txn}; + use std::bcs::{Self}; + + public entry fun deploy_coin(deployer: &signer) { + let addr = signer::address_of(deployer); + let addr_bytes = bcs::to_bytes(&addr); + let code = x"a11ceb0b05000000050100020202040706130819200a390500000001080004636f696e01540b64756d6d795f6669656c64"; + vector::append(&mut code, addr_bytes); + vector::append(&mut code, x"000201020100"); + let metadata_serialized: vector = x"0b57726170706564436f696e010000000000000000404237423334333744324439304635433830464246313246423231423543424543383332443335453138364542373539304431434134324338334631333639324586021f8b08000000000002ffb590316bc330108577fd0a21af8d9dae810ea5a55bc9d0408710cac9ba388765e990649752fadf2b252e5d0299bce9ddbd77ef437b86b6870e0fc2c180f241aaf700cc689e3c3925260c91bc2bf375bdaeef9518b90b60f083bda5f6ab2c5a3f3024d2169510d56efbbcdd482627e76c941a8f3ea01c809cc324035a8488a2da1b6474065d4b180fa27ae4e4e34bc81c9f3ef4f9f4b7ec28958a534a1c374d93e569d4756e6ca0985716749c9f6deea8b341ddc9386a43a1042fabc14fd81cff0ecffe7f9d1301a76237386542257f44f59a336fc958d2cb8114b98ae792eb10e71f599ae232bc89b1f33dbaa5295229b906f10b3085ae64a80200000104636f696e731f8b08000000000002ff0dc0dd0984300c00e0f74e91116a9a26c5396e81246db9e37e8453411177d7ef3bd5f5d3206e28d235e66cac929cd02333d68c899db3172677eb5849ade2d007214dd18b49e1a656c8c6d1a7d70f8e00b779f9afbec0039e3ac3bbed709ce10c176825e8506c00000000000000"; + let code_array = vector::empty>(); + vector::push_back(&mut code_array, code); + publish_package_txn(deployer, metadata_serialized, code_array); + } +} diff --git a/aptos/token_bridge/sources/newtypes/normalized_amount.move b/aptos/token_bridge/sources/newtypes/normalized_amount.move new file mode 100644 index 000000000..b85b5c6b6 --- /dev/null +++ b/aptos/token_bridge/sources/newtypes/normalized_amount.move @@ -0,0 +1,74 @@ +/// Amounts in represented in token bridge VAAs are capped at 8 decimals. This +/// means that any amount that's given as having more decimals is truncated to 8 +/// decimals. On the way out, these amount have to be scaled back to the +/// original decimal amount. This module defines `NormalizedAmount`, which +/// represents amounts that have been capped at 8 decimals. +/// +/// The functions `normalize` and `denormalize` take care of convertion to/from +/// this type given the original amount's decimals. +module token_bridge::normalized_amount { + use wormhole::cursor::Cursor; + use wormhole::deserialize; + use wormhole::serialize; + + struct NormalizedAmount has store, copy, drop { + amount: u64 + } + + #[test_only] + public fun get_amount(n: NormalizedAmount): u64 { + n.amount + } + + public fun normalize(amount: u64, decimals: u8): NormalizedAmount { + if (decimals > 8) { + let n = decimals - 8; + while (n > 0){ + amount = amount / 10; + n = n - 1; + } + }; + NormalizedAmount { amount } + } + + public fun denormalize(amount: NormalizedAmount, decimals: u8): u64 { + let NormalizedAmount { amount } = amount; + if (decimals > 8) { + let n = decimals - 8; + while (n > 0){ + amount = amount * 10; + n = n - 1; + } + }; + amount + } + + public fun deserialize(cur: &mut Cursor): NormalizedAmount { + // in the VAA wire format, amounts are 32 bytes. + let amount = deserialize::deserialize_u256(cur); + NormalizedAmount { amount: wormhole::u256::as_u64(amount) } + } + + public fun serialize(buf: &mut vector, e: NormalizedAmount) { + let NormalizedAmount { amount } = e; + serialize::serialize_u256(buf, wormhole::u256::from_u64(amount)) + } +} + +#[test_only] +module token_bridge::normalized_amount_test { + use token_bridge::normalized_amount; + + #[test] + fun test_normalize_denormalize_amount() { + let a = 12345678910111; + let b = normalized_amount::normalize(a, 9); + let c = normalized_amount::denormalize(b, 9); + assert!(c == 12345678910110, 0); + + let x = 12345678910111; + let y = normalized_amount::normalize(x, 5); + let z = normalized_amount::denormalize(y, 5); + assert!(z == x, 0); + } +} diff --git a/aptos/token_bridge/sources/newtypes/string32.move b/aptos/token_bridge/sources/newtypes/string32.move new file mode 100644 index 000000000..f4ba2a459 --- /dev/null +++ b/aptos/token_bridge/sources/newtypes/string32.move @@ -0,0 +1,171 @@ +/// The `string32` module defines the `String32` type which represents UTF8 +/// encoded strings that are guaranteed to be 32 bytes long, with 0 padding on +/// the right. +module token_bridge::string32 { + + use std::string::{Self, String}; + use std::option; + use std::vector; + + use wormhole::cursor::Cursor; + use wormhole::deserialize; + use wormhole::serialize; + + const E_STRING_TOO_LONG: u64 = 0; + + /// A `String32` holds a ut8 string which is guaranteed to be 32 bytes long. + struct String32 has copy, drop, store { + string: String + } + + spec String32 { + invariant string::length(string) == 32; + } + + /// Right-pads a `String` to a `String32` with 0 bytes. + /// Aborts if the string is longer than 32 bytes. + public fun right_pad(s: &String): String32 { + let length = string::length(s); + assert!(length <= 32, E_STRING_TOO_LONG); + let string = *string::bytes(s); + let zeros = 32 - length; + while ({ + spec { + invariant zeros + vector::length(string) == 32; + }; + zeros > 0 + }) { + vector::push_back(&mut string, 0); + zeros = zeros - 1; + }; + String32 { string: string::utf8(string) } + } + + /// Internal function to take the first 32 bytes of a byte sequence and + /// convert to a utf8 `String`. + /// Takes the longest prefix that's valid utf8 and maximum 32 bytes. + /// + /// Even if the input is valid utf8, the result might be shorter than 32 + /// bytes, because the original string might have a multi-byte utf8 + /// character at the 32 byte boundary, which, when split, results in an + /// invalid code point, so we remove it. + fun take(bytes: vector, n: u64): String { + while (vector::length(&bytes) > n) { + vector::pop_back(&mut bytes); + }; + + let utf8 = string::try_utf8(bytes); + while (option::is_none(&utf8)) { + vector::pop_back(&mut bytes); + utf8 = string::try_utf8(bytes); + }; + option::extract(&mut utf8) + } + + /// Takes the first `n` bytes of a `String`. + /// + /// Even if the input string is longer than `n`, the resulting string might + /// be shorter because the original string might have a multi-byte utf8 + /// character at the byte boundary, which, when split, results in an invalid + /// code point, so we remove it. + public fun take_utf8(str: String, n: u64): String { + take(*string::bytes(&str), n) + } + + /// Truncates or right-pads a `String` to a `String32`. + /// Does not abort. + public fun from_string(s: &String): String32 { + right_pad(&take(*string::bytes(s), 32)) + } + + /// Truncates or right-pads a byte vector to a `String32`. + /// Does not abort. + public fun from_bytes(b: vector): String32 { + right_pad(&take(b, 32)) + } + + /// Converts `String32` to `String`, removing trailing 0s. + public fun to_string(s: &String32): String { + let String32 { string } = s; + let bytes = *string::bytes(string); + // keep dropping the last character while it's 0 + while (!vector::is_empty(&bytes) && + *vector::borrow(&bytes, vector::length(&bytes) - 1) == 0 + ) { + vector::pop_back(&mut bytes); + }; + string::utf8(bytes) + } + + /// Converts `String32` to a byte vector of length 32. + public fun to_bytes(s: &String32): vector { + *string::bytes(&s.string) + } + + public fun deserialize(cur: &mut Cursor): String32 { + let bytes = deserialize::deserialize_vector(cur, 32); + from_bytes(bytes) + } + + public fun serialize(buf: &mut vector, e: String32) { + serialize::serialize_vector(buf, to_bytes(&e)) + } + +} + +#[test_only] +module token_bridge::string32_test { + use std::string; + use std::vector; + use token_bridge::string32; + + #[test] + public fun test_right_pad() { + let result = string32::right_pad(&string::utf8(b"hello")); + assert!(string32::to_string(&result) == string::utf8(b"hello"), 0) + } + + #[test] + #[expected_failure(abort_code = 0)] + public fun test_right_pad_fail() { + let too_long = string::utf8(b"this string is very very very very very very very very very very very very very very very long"); + string32::right_pad(&too_long); + } + + #[test] + public fun test_from_string_short() { + let result = string32::from_string(&string::utf8(b"hello")); + assert!(string32::to_string(&result) == string::utf8(b"hello"), 0) + } + + #[test] + public fun test_from_string_long() { + let long = string32::from_string(&string::utf8(b"this string is very very very very very very very very very very very very very very very long")); + assert!(string32::to_string(&long) == string::utf8(b"this string is very very very ve"), 0) + } + + #[test] + public fun test_from_string_weird_utf8() { + let string = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + assert!(vector::length(&string) == 31, 0); + // append the samaritan letter Alaf, a 3-byte utf8 character the move + // parser only allows ascii characters unfortunately (the character + // looks nice) + vector::append(&mut string, x"e0a080"); + // it's valid utf8 + let string = string::utf8(string); + // string length is bytes, not characters + assert!(string::length(&string) == 34, 0); + let padded = string32::from_string(&string); + // notice that the e0 byte got dropped at the end + assert!(string32::to_string(&padded) == string::utf8(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0) + } + + #[test] + public fun test_from_bytes_invalid_utf8() { + // invalid utf8 + let bytes = x"e0a0"; + let result = string::utf8(b""); + assert!(string32::to_string(&string32::from_bytes(bytes)) == result, 0) + } +} diff --git a/aptos/token_bridge/sources/register_chain.move b/aptos/token_bridge/sources/register_chain.move new file mode 100644 index 000000000..ecb549c28 --- /dev/null +++ b/aptos/token_bridge/sources/register_chain.move @@ -0,0 +1,161 @@ +module token_bridge::register_chain { + + use wormhole::u16::{Self, U16}; + use wormhole::cursor; + use wormhole::deserialize; + use wormhole::vaa; + use wormhole::external_address::{Self, ExternalAddress}; + + use token_bridge::vaa as token_bridge_vaa; + use token_bridge::state; + + /// "TokenBridge" (left padded) + const TOKEN_BRIDGE: vector = x"000000000000000000000000000000000000000000546f6b656e427269646765"; + + const E_INVALID_MODULE: u64 = 0; + const E_INVALID_ACTION: u64 = 1; + const E_INVALID_TARGET: u64 = 2; + + struct RegisterChain has copy, drop { + /// Chain ID + emitter_chain_id: U16, + /// Emitter address. Left-zero-padded if shorter than 32 bytes + emitter_address: ExternalAddress, + } + + #[test_only] + public fun parse_payload_test(payload: vector): RegisterChain { + parse_payload(payload) + } + + fun parse_payload(payload: vector): RegisterChain { + let cur = cursor::init(payload); + let target_module = deserialize::deserialize_vector(&mut cur, 32); + + assert!(target_module == TOKEN_BRIDGE, E_INVALID_MODULE); + + let action = deserialize::deserialize_u8(&mut cur); + assert!(action == 0x01, E_INVALID_ACTION); + + // TODO(csongor): should we also accept a VAA targeting aptos directly? + // why would a registration VAA target a specific chain? + let target_chain = deserialize::deserialize_u16(&mut cur); + assert!(target_chain == u16::from_u64(0x0), E_INVALID_TARGET); + + let emitter_chain_id = deserialize::deserialize_u16(&mut cur); + + let emitter_address = external_address::deserialize(&mut cur); + + cursor::destroy_empty(cur); + + RegisterChain { emitter_chain_id, emitter_address } + } + + public entry fun submit_vaa(vaa: vector) { + let vaa = vaa::parse_and_verify(vaa); + vaa::assert_governance(&vaa); // not tested + token_bridge_vaa::replay_protect(&vaa); + + let RegisterChain { emitter_chain_id, emitter_address } = parse_payload(vaa::destroy(vaa)); + + state::set_registered_emitter(emitter_chain_id, emitter_address) + } + + public fun get_emitter_chain_id(a: &RegisterChain): U16 { + a.emitter_chain_id + } + + public fun get_emitter_address(a: &RegisterChain): ExternalAddress { + a.emitter_address + } + +} + +#[test_only] +module token_bridge::register_chain_test { + use std::option; + use wormhole::u16; + use token_bridge::register_chain; + use wormhole::vaa; + use wormhole::wormhole; + use wormhole::external_address; + use token_bridge::token_bridge; + use token_bridge::state; + + /// Registration VAA for the etheruem token bridge 0xdeadbeef + const ETHEREUM_TOKEN_REG: vector = x"0100000000010015d405c74be6d93c3c33ed6b48d8db70dfb31e0981f8098b2a6c7583083e0c3343d4a1abeb3fc1559674fa067b0c0e2e9de2fafeaecdfeae132de2c33c9d27cc0100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000016911ae00000000000000000000000000000000000000000000546f6b656e427269646765010000000200000000000000000000000000000000000000000000000000000000deadbeef"; + + /// Another registration VAA for the ethereum token bridge, 0xbeefface + const ETHEREUM_TOKEN_REG_2:vector = x"01000000000100c2157fa1c14957dff26d891e4ad0d993ad527f1d94f603e3d2bb1e37541e2fbe45855ffda1efc7eb2eb24009a1585fa25a267815db97e4a9d4a5eb31987b5fb40100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000017ca43300000000000000000000000000000000000000000000546f6b656e427269646765010000000200000000000000000000000000000000000000000000000000000000beefface"; + + /// Registration VAA for the etheruem NFT bridge 0xdeadbeef + const ETHEREUM_NFT_REG: vector = x"0100000000010066cce2cb12d88c97d4975cba858bb3c35d6430003e97fced46a158216f3ca01710fd16cc394441a08fef978108ed80c653437f43bb2ca039226974d9512298b10000000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000018483540000000000000000000000000000000000000000000000004e4654427269646765010000000200000000000000000000000000000000000000000000000000000000deadbeef"; + + const ETH_ID: u64 = 2; + + fun setup(deployer: &signer) { + let aptos_framework = std::account::create_account_for_test(@aptos_framework); + std::timestamp::set_time_has_started_for_testing(&aptos_framework); + wormhole::init_test( + 22, + 1, + x"0000000000000000000000000000000000000000000000000000000000000004", + x"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe", + 0 + ); + token_bridge::init_test(deployer); + } + + #[test] + public fun test_parse() { + let vaa = vaa::parse_test(ETHEREUM_TOKEN_REG); + let register_chain = register_chain::parse_payload_test(vaa::destroy(vaa)); + let chain = register_chain::get_emitter_chain_id(®ister_chain); + let address = register_chain::get_emitter_address(®ister_chain); + + assert!(chain == u16::from_u64(ETH_ID), 0); + assert!(address == external_address::from_bytes(x"deadbeef"), 0); + + } + + #[test] + #[expected_failure(abort_code = 0)] + public fun test_parse_fail() { + let vaa = vaa::parse_test(ETHEREUM_NFT_REG); + // this should fail because it's an NFT registration + let _register_chain = register_chain::parse_payload_test(vaa::destroy(vaa)); + + } + + #[test(deployer = @deployer)] + public fun test_registration(deployer: &signer) { + setup(deployer); + + register_chain::submit_vaa(ETHEREUM_TOKEN_REG); + let address = state::get_registered_emitter(u16::from_u64(ETH_ID)); + assert!(address == option::some(external_address::from_bytes(x"deadbeef")), 0); + } + + #[test(deployer = @deployer)] + #[expected_failure(abort_code = 25607)] + public fun test_replay_protect(deployer: &signer) { + setup(deployer); + + register_chain::submit_vaa(ETHEREUM_TOKEN_REG); + register_chain::submit_vaa(ETHEREUM_TOKEN_REG); + } + + #[test(deployer = @deployer)] + public fun test_re_registration(deployer: &signer) { + test_registration(deployer); + + // TODO(csongor): we register ethereum again, which overrides the + // previous one. This deviates from other chains (where this is + // rejected), but I think this is the right behaviour. + // Easy to change, should be discussed. + register_chain::submit_vaa(ETHEREUM_TOKEN_REG_2); + let address = state::get_registered_emitter(u16::from_u64(ETH_ID)); + assert!(address == option::some(external_address::from_bytes(x"beefface")), 0); + } + +} diff --git a/aptos/token_bridge/sources/state.move b/aptos/token_bridge/sources/state.move new file mode 100644 index 000000000..9a365ae3f --- /dev/null +++ b/aptos/token_bridge/sources/state.move @@ -0,0 +1,257 @@ +module token_bridge::state { + use std::table::{Self, Table}; + use std::option::{Self, Option}; + use aptos_framework::type_info::{Self, TypeInfo, type_of}; + use aptos_framework::account::{Self, SignerCapability}; + use aptos_framework::aptos_coin::AptosCoin; + use aptos_framework::coin::Coin; + + use wormhole::u16::U16; + use wormhole::emitter::EmitterCapability; + use wormhole::state; + use wormhole::wormhole; + use wormhole::set::{Self, Set}; + use wormhole::external_address::ExternalAddress; + + use token_bridge::token_hash::{Self, TokenHash}; + + friend token_bridge::contract_upgrade; + friend token_bridge::register_chain; + friend token_bridge::token_bridge; + friend token_bridge::vaa; + friend token_bridge::attest_token; + friend token_bridge::wrapped; + friend token_bridge::complete_transfer; + friend token_bridge::complete_transfer_with_payload; + friend token_bridge::transfer_tokens; + + #[test_only] + friend token_bridge::wrapped_test; + #[test_only] + friend token_bridge::vaa_test; + + const E_ORIGIN_CHAIN_MISMATCH: u64 = 0; + const E_ORIGIN_ADDRESS_MISMATCH: u64 = 1; + const E_WRAPPING_NATIVE_COIN: u64 = 2; + const E_WRAPPED_ASSET_NOT_INITIALIZED: u64 = 3; + + /// The origin chain and address of a token. In case of native tokens + /// (where the chain is aptos), the token_address is the hash of the token + /// info (see token_hash.move for more details) + struct OriginInfo has key, store, copy, drop { + token_chain: U16, + token_address: ExternalAddress, + } + + public fun get_origin_info_token_address(info: &OriginInfo): ExternalAddress { + info.token_address + } + + public fun get_origin_info_token_chain(info: &OriginInfo): U16 { + info.token_chain + } + + public(friend) fun create_origin_info( + token_chain: U16, + token_address: ExternalAddress, + ): OriginInfo { + OriginInfo { token_address: token_address, token_chain: token_chain } + } + + struct WrappedInfo has store { + type_info: Option, + signer_cap: SignerCapability + } + + struct State has key, store { + governance_chain_id: U16, + governance_contract: ExternalAddress, + + /// Set of consumed VAA hashes + consumed_vaas: Set>, + + /// Mapping of wrapped assets ((chain_id, origin_address) => wrapped_asset info) + wrapped_infos: Table, + + /// Reverse mapping of hash(TypeInfo) for native tokens, so their + /// information can be looked up externally by knowing their hash (which + /// is the 32 byte "address" that goes into the VAA). + native_infos: Table, + + signer_cap: SignerCapability, + + emitter_cap: EmitterCapability, + + // Mapping of bridge contracts on other chains + registered_emitters: Table, + } + + // getters + + public fun vaa_is_consumed(hash: vector): bool acquires State { + let state = borrow_global(@token_bridge); + set::contains(&state.consumed_vaas, hash) + } + + public fun governance_chain_id(): U16 acquires State { + let state = borrow_global(@token_bridge); + return state.governance_chain_id + } + + public fun governance_contract(): ExternalAddress acquires State { + let state = borrow_global(@token_bridge); + return state.governance_contract + } + + public fun wrapped_asset_info(native_info: OriginInfo): TypeInfo acquires State { + let wrapped_infos = &borrow_global(@token_bridge).wrapped_infos; + let type_info = table::borrow(wrapped_infos, native_info).type_info; + assert!(option::is_some(&type_info), E_WRAPPED_ASSET_NOT_INITIALIZED); + option::extract(&mut type_info) + } + + public fun native_asset_info(token_address: TokenHash): TypeInfo acquires State { + let native_infos = &borrow_global(@token_bridge).native_infos; + *table::borrow(native_infos, token_address) + } + + /// Returns the origin information for a CoinType + public fun origin_info(): OriginInfo acquires OriginInfo { + if (is_wrapped_asset()) { + *borrow_global(type_info::account_address(&type_of())) + } else { + let token_chain = state::get_chain_id(); + let token_address = token_hash::get_external_address(&token_hash::derive()); + OriginInfo { token_chain, token_address } + } + } + + public fun get_registered_emitter(chain_id: U16): Option acquires State { + let state = borrow_global(@token_bridge); + if (table::contains(&state.registered_emitters, chain_id)) { + option::some(*table::borrow(&state.registered_emitters, chain_id)) + } else { + option::none() + } + + } + + // given the hash of the TypeInfo of a Coin, this tells us if it is registered with Token Bridge + public fun is_registered_native_asset(): bool acquires State { + let token = token_hash::derive(); + let native_infos = &borrow_global(@token_bridge).native_infos; + !is_wrapped_asset() && table::contains(native_infos, token) + } + + public fun is_wrapped_asset(): bool { + exists(type_info::account_address(&type_of())) + } + + public(friend) fun setup_wrapped( + origin_info: OriginInfo + ) acquires State { + assert!(origin_info.token_chain != state::get_chain_id(), E_WRAPPING_NATIVE_COIN); + let wrapped_infos = &mut borrow_global_mut(@token_bridge).wrapped_infos; + let wrapped_info = table::borrow_mut(wrapped_infos, origin_info); + + let coin_signer = account::create_signer_with_capability(&wrapped_info.signer_cap); + move_to(&coin_signer, origin_info); + + wrapped_info.type_info = option::some(type_of()); + + } + + public fun assert_coin_origin_info(origin: OriginInfo) acquires OriginInfo { + let coin_origin = origin_info(); + assert!(coin_origin.token_chain == origin.token_chain, E_ORIGIN_CHAIN_MISMATCH); + assert!(coin_origin.token_address == origin.token_address, E_ORIGIN_ADDRESS_MISMATCH); + } + + public(friend) fun publish_message( + nonce: u64, + payload: vector, + message_fee: Coin, + ): u64 acquires State { + let emitter_cap = &mut borrow_global_mut(@token_bridge).emitter_cap; + wormhole::publish_message( + emitter_cap, + nonce, + payload, + message_fee + ) + } + + public(friend) fun token_bridge_signer(): signer acquires State { + account::create_signer_with_capability(&borrow_global(@token_bridge).signer_cap) + } + + // setters + + public(friend) fun set_vaa_consumed(hash: vector) acquires State { + let state = borrow_global_mut(@token_bridge); + set::add(&mut state.consumed_vaas, hash); + } + + public(friend) fun set_governance_chain_id(governance_chain_id: U16) acquires State { + let state = borrow_global_mut(@token_bridge); + state.governance_chain_id = governance_chain_id; + } + + public(friend) fun set_governance_contract(governance_contract: ExternalAddress) acquires State { + let state = borrow_global_mut(@token_bridge); + state.governance_contract = governance_contract; + } + + public(friend) fun set_registered_emitter(chain_id: U16, bridge_contract: ExternalAddress) acquires State { + let state = borrow_global_mut(@token_bridge); + table::upsert(&mut state.registered_emitters, chain_id, bridge_contract); + } + + // 32-byte native asset address => type info + public(friend) fun set_native_asset_type_info() acquires State { + let token_address = token_hash::derive(); + let type_info = type_of(); + + let state = borrow_global_mut(@token_bridge); + let native_infos = &mut state.native_infos; + if (table::contains(native_infos, token_address)){ + //TODO: throw error, because we should only be able to set native asset type info once? + table::remove(native_infos, token_address); + }; + table::add(native_infos, token_address, type_info); + } + + public(friend) fun set_wrapped_asset_signer_capability(token: OriginInfo, signer_cap: SignerCapability) acquires State { + let state = borrow_global_mut(@token_bridge); + let wrapped_info = WrappedInfo { + type_info: option::none(), + signer_cap + }; + table::add(&mut state.wrapped_infos, token, wrapped_info); + } + + public(friend) fun get_wrapped_asset_signer(origin_info: OriginInfo): signer acquires State { + let wrapped_coin_signer_caps + = &borrow_global(@token_bridge).wrapped_infos; + let wrapped_info = table::borrow(wrapped_coin_signer_caps, origin_info); + account::create_signer_with_capability(&wrapped_info.signer_cap) + } + + public(friend) fun init_token_bridge_state( + signer_cap: SignerCapability, + emitter_cap: EmitterCapability + ) { + let token_bridge = account::create_signer_with_capability(&signer_cap); + move_to(&token_bridge, State { + governance_chain_id: state::get_chain_id(), + governance_contract: state::get_governance_contract(), + consumed_vaas: set::new>(), + wrapped_infos: table::new(), + native_infos: table::new(), + signer_cap: signer_cap, + emitter_cap: emitter_cap, + registered_emitters: table::new(), + } + ); + } +} diff --git a/aptos/token_bridge/sources/structs/asset_meta.move b/aptos/token_bridge/sources/structs/asset_meta.move new file mode 100644 index 000000000..0f83659dc --- /dev/null +++ b/aptos/token_bridge/sources/structs/asset_meta.move @@ -0,0 +1,117 @@ +module token_bridge::asset_meta { + use std::vector::{Self}; + use wormhole::serialize::{serialize_u8, serialize_u16, serialize_vector}; + use wormhole::deserialize::{deserialize_u8, deserialize_u16, deserialize_vector}; + use wormhole::cursor::{Self}; + + use wormhole::u16::{U16}; + use wormhole::external_address::{Self, ExternalAddress}; + + use token_bridge::string32::{Self, String32}; + + friend token_bridge::attest_token; + friend token_bridge::wrapped; + + #[test_only] + friend token_bridge::wrapped_test; + + const E_INVALID_ACTION: u64 = 0; + + struct AssetMeta has key, store, drop { + /// Address of the token. Left-zero-padded if shorter than 32 bytes + token_address: ExternalAddress, + /// Chain ID of the token + token_chain: U16, + /// Number of decimals of the token (big-endian uint256) + decimals: u8, + /// Symbol of the token (UTF-8) + symbol: String32, + /// Name of the token (UTF-8) + name: String32, + } + + public fun get_token_address(a: &AssetMeta): ExternalAddress { + a.token_address + } + + public fun get_token_chain(a: &AssetMeta): U16 { + a.token_chain + } + + public fun get_decimals(a: &AssetMeta): u8 { + a.decimals + } + + public fun get_symbol(a: &AssetMeta): String32 { + a.symbol + } + + public fun get_name(a: &AssetMeta): String32 { + a.name + } + + public(friend) fun create( + token_address: ExternalAddress, + token_chain: U16, + decimals: u8, + symbol: String32, + name: String32, + ): AssetMeta { + AssetMeta { + token_address, + token_chain, + decimals, + symbol, + name + } + } + + public fun encode(meta: AssetMeta): vector { + let encoded = vector::empty(); + serialize_u8(&mut encoded, 2); + serialize_vector(&mut encoded, external_address::get_bytes(&meta.token_address)); + serialize_u16(&mut encoded, meta.token_chain); + serialize_u8(&mut encoded, meta.decimals); + string32::serialize(&mut encoded, meta.symbol); + string32::serialize(&mut encoded, meta.name); + encoded + } + + public fun parse(meta: vector): AssetMeta { + let cur = cursor::init(meta); + let action = deserialize_u8(&mut cur); + assert!(action == 2, E_INVALID_ACTION); + let token_address = deserialize_vector(&mut cur, 32); + let token_chain = deserialize_u16(&mut cur); + let decimals = deserialize_u8(&mut cur); + let symbol = string32::deserialize(&mut cur); + let name = string32::deserialize(&mut cur); + cursor::destroy_empty(cur); + AssetMeta { + token_address: external_address::from_bytes(token_address), + token_chain, + decimals, + symbol, + name + } + } + + // Construct a seed using AssetMeta fields for creating a new resource account + // N.B. seed is product of coin native chain and native address + // TODO(csongor): technically this only requires the OriginInfo, so we could + // perhaps make this a function of that instead of the whole AssetMeta. + public(friend) fun create_seed(asset_meta: &AssetMeta): vector{ + let token_chain = get_token_chain(asset_meta); + let token_address = get_token_address(asset_meta); + let seed = vector::empty(); + serialize_u16(&mut seed, token_chain); + // TODO(csongor): why do we need '::' here? The seed is binary anyway, + // but appending '::' suggests that it might be ASCII, which is + // confusing. We should either make it ASCII, or just drop these + // characters. + serialize_vector(&mut seed, b"::"); + external_address::serialize(&mut seed, token_address); + seed + } + +} diff --git a/aptos/token_bridge/sources/structs/transfer.move b/aptos/token_bridge/sources/structs/transfer.move new file mode 100644 index 000000000..3720386bd --- /dev/null +++ b/aptos/token_bridge/sources/structs/transfer.move @@ -0,0 +1,149 @@ +module token_bridge::transfer { + use std::vector; + use wormhole::serialize::{ + serialize_u8, + serialize_u16, + }; + use wormhole::deserialize::{ + deserialize_u8, + deserialize_u16, + }; + use wormhole::cursor; + use wormhole::external_address::{Self, ExternalAddress}; + use wormhole::u16::U16; + + use token_bridge::normalized_amount::{Self, NormalizedAmount}; + + friend token_bridge::transfer_tokens; + + #[test_only] + friend token_bridge::complete_transfer_test; + #[test_only] + friend token_bridge::transfer_test; + + const E_INVALID_ACTION: u64 = 0; + + struct Transfer has drop { + /// Amount being transferred + amount: NormalizedAmount, + /// Address of the token. Left-zero-padded if shorter than 32 bytes + token_address: ExternalAddress, + /// Chain ID of the token + token_chain: U16, + /// Address of the recipient. Left-zero-padded if shorter than 32 bytes + to: ExternalAddress, + /// Chain ID of the recipient + to_chain: U16, + /// Amount of tokens that the user is willing to pay as relayer fee. Must be <= Amount. + fee: NormalizedAmount, + } + + public fun get_amount(a: &Transfer): NormalizedAmount { + a.amount + } + + public fun get_token_address(a: &Transfer): ExternalAddress { + a.token_address + } + + public fun get_token_chain(a: &Transfer): U16 { + a.token_chain + } + + public fun get_to(a: &Transfer): ExternalAddress { + a.to + } + + public fun get_to_chain(a: &Transfer): U16 { + a.to_chain + } + + public fun get_fee(a: &Transfer): NormalizedAmount { + a.fee + } + + public(friend) fun create( + amount: NormalizedAmount, + token_address: ExternalAddress, + token_chain: U16, + to: ExternalAddress, + to_chain: U16, + fee: NormalizedAmount, + ): Transfer { + Transfer { + amount, + token_address, + token_chain, + to, + to_chain, + fee, + } + } + + public fun parse(transfer: vector): Transfer { + let cur = cursor::init(transfer); + let action = deserialize_u8(&mut cur); + assert!(action == 1, E_INVALID_ACTION); + let amount = normalized_amount::deserialize(&mut cur); + let token_address = external_address::deserialize(&mut cur); + let token_chain = deserialize_u16(&mut cur); + let to = external_address::deserialize(&mut cur); + let to_chain = deserialize_u16(&mut cur); + let fee = normalized_amount::deserialize(&mut cur); + cursor::destroy_empty(cur); + Transfer { + amount, + token_address, + token_chain, + to, + to_chain, + fee, + } + } + + public fun encode(transfer: Transfer): vector { + let encoded = vector::empty(); + serialize_u8(&mut encoded, 1); + normalized_amount::serialize(&mut encoded, transfer.amount); + external_address::serialize(&mut encoded, transfer.token_address); + serialize_u16(&mut encoded, transfer.token_chain); + external_address::serialize(&mut encoded, transfer.to); + serialize_u16(&mut encoded, transfer.to_chain); + normalized_amount::serialize(&mut encoded, transfer.fee); + encoded + } + +} + +#[test_only] +module token_bridge::transfer_test { + use token_bridge::transfer; + use token_bridge::normalized_amount; + use wormhole::external_address; + use wormhole::u16; + + #[test] + public fun parse_roundtrip() { + let amount = normalized_amount::normalize(100, 8); + let token_address = external_address::from_bytes(x"beef"); + let token_chain = u16::from_u64(1); + let to = external_address::from_bytes(x"cafe"); + let to_chain = u16::from_u64(7); + let fee = normalized_amount::normalize(50, 8); + let transfer = transfer::create( + amount, + token_address, + token_chain, + to, + to_chain, + fee, + ); + let transfer = transfer::parse(transfer::encode(transfer)); + assert!(transfer::get_amount(&transfer) == amount, 0); + assert!(transfer::get_token_address(&transfer) == token_address, 0); + assert!(transfer::get_token_chain(&transfer) == token_chain, 0); + assert!(transfer::get_to(&transfer) == to, 0); + assert!(transfer::get_to_chain(&transfer) == to_chain, 0); + assert!(transfer::get_fee(&transfer) == fee, 0); + } +} diff --git a/aptos/token_bridge/sources/structs/transfer_result.move b/aptos/token_bridge/sources/structs/transfer_result.move new file mode 100644 index 000000000..6ba5ac53f --- /dev/null +++ b/aptos/token_bridge/sources/structs/transfer_result.move @@ -0,0 +1,44 @@ +module token_bridge::transfer_result { + use wormhole::u16::U16; + use wormhole::external_address::ExternalAddress; + + use token_bridge::normalized_amount::NormalizedAmount; + + friend token_bridge::transfer_tokens; + + struct TransferResult { + /// Chain ID of the token + token_chain: U16, + /// Address of the token. Left-zero-padded if shorter than 32 bytes + token_address: ExternalAddress, + /// Amount being transferred + normalized_amount: NormalizedAmount, + /// Amount of tokens that the user is willing to pay as relayer fee. Must be <= Amount. + normalized_relayer_fee: NormalizedAmount, + } + + public fun destroy(a: TransferResult): (U16, ExternalAddress, NormalizedAmount, NormalizedAmount) { + let TransferResult { + token_chain, + token_address, + normalized_amount, + normalized_relayer_fee + } = a; + (token_chain, token_address, normalized_amount, normalized_relayer_fee) + } + + public(friend) fun create( + token_chain: U16, + token_address: ExternalAddress, + normalized_amount: NormalizedAmount, + normalized_relayer_fee: NormalizedAmount, + ): TransferResult { + TransferResult { + token_chain, + token_address, + normalized_amount, + normalized_relayer_fee, + } + } + +} diff --git a/aptos/token_bridge/sources/structs/transfer_with_payload.move b/aptos/token_bridge/sources/structs/transfer_with_payload.move new file mode 100644 index 000000000..a7cf8df7a --- /dev/null +++ b/aptos/token_bridge/sources/structs/transfer_with_payload.move @@ -0,0 +1,122 @@ +module token_bridge::transfer_with_payload { + use std::vector; + use wormhole::serialize::{ + serialize_u8, + serialize_u16, + serialize_vector, + }; + use wormhole::deserialize::{ + deserialize_u8, + deserialize_u16, + }; + use wormhole::cursor; + + use wormhole::u16::U16; + use wormhole::external_address::{Self, ExternalAddress}; + + use token_bridge::normalized_amount::{Self, NormalizedAmount}; + + friend token_bridge::transfer_tokens; + + const E_INVALID_ACTION: u64 = 0; + + struct TransferWithPayload has key, store, drop { + /// Amount being transferred (big-endian uint256) + amount: NormalizedAmount, + /// Address of the token. Left-zero-padded if shorter than 32 bytes + token_address: ExternalAddress, + /// Chain ID of the token + token_chain: U16, + /// Address of the recipient. Left-zero-padded if shorter than 32 bytes + to: ExternalAddress, + /// Chain ID of the recipient + to_chain: U16, + /// Address of the message sender. Left-zero-padded if shorter than 32 bytes + from_address: ExternalAddress, + /// An arbitrary payload + payload: vector, + } + + public fun get_amount(a: &TransferWithPayload): NormalizedAmount { + a.amount + } + + public fun get_token_address(a: &TransferWithPayload): ExternalAddress { + a.token_address + } + + public fun get_token_chain(a: &TransferWithPayload): U16 { + a.token_chain + } + + public fun get_to(a: &TransferWithPayload): ExternalAddress { + a.to + } + + public fun get_to_chain(a: &TransferWithPayload): U16 { + a.to_chain + } + + public fun get_from_address(a: &TransferWithPayload): ExternalAddress { + a.from_address + } + + public fun get_payload(a: &TransferWithPayload): vector { + a.payload + } + + public(friend) fun create( + amount: NormalizedAmount, + token_address: ExternalAddress, + token_chain: U16, + to: ExternalAddress, + to_chain: U16, + from_address: ExternalAddress, + payload: vector + ): TransferWithPayload { + TransferWithPayload { + amount, + token_address, + token_chain, + to, + to_chain, + from_address, + payload, + } + } + + public fun encode(transfer: TransferWithPayload): vector { + let encoded = vector::empty(); + serialize_u8(&mut encoded, 3); + normalized_amount::serialize(&mut encoded, transfer.amount); + external_address::serialize(&mut encoded, transfer.token_address); + serialize_u16(&mut encoded, transfer.token_chain); + external_address::serialize(&mut encoded, transfer.to); + serialize_u16(&mut encoded, transfer.to_chain); + external_address::serialize(&mut encoded, transfer.from_address); + serialize_vector(&mut encoded, transfer.payload); + encoded + } + + public fun parse(transfer: vector): TransferWithPayload { + let cur = cursor::init(transfer); + let action = deserialize_u8(&mut cur); + assert!(action == 3, E_INVALID_ACTION); + let amount = normalized_amount::deserialize(&mut cur); + let token_address = external_address::deserialize(&mut cur); + let token_chain = deserialize_u16(&mut cur); + let to = external_address::deserialize(&mut cur); + let to_chain = deserialize_u16(&mut cur); + let from_address = external_address::deserialize(&mut cur); + let payload = cursor::rest(cur); + TransferWithPayload { + amount, + token_address, + token_chain, + to, + to_chain, + from_address, + payload + } + } +} diff --git a/aptos/token_bridge/sources/token_bridge.move b/aptos/token_bridge/sources/token_bridge.move new file mode 100644 index 000000000..8f12c75e1 --- /dev/null +++ b/aptos/token_bridge/sources/token_bridge.move @@ -0,0 +1,33 @@ +module token_bridge::token_bridge { + #[test_only] + use aptos_framework::account::{Self}; + use aptos_framework::account::{SignerCapability}; + use deployer::deployer::{claim_signer_capability}; + use token_bridge::state::{init_token_bridge_state}; + use wormhole::wormhole; + + /// Initializes the contract. + /// The native `init_module` cannot be used, because it runs on each upgrade + /// (oddly). + /// TODO: the above behaviour has been remedied in the Aptos VM, so we could + /// use `init_module` now. Let's reconsider before the mainnet launch. + /// Can only be called by the deployer (checked by the + /// `deployer::claim_signer_capability` function). + public entry fun init(deployer: &signer) { + let signer_cap = claim_signer_capability(deployer, @token_bridge); + init_internal(signer_cap); + } + + fun init_internal(signer_cap: SignerCapability){ + let emitter_cap = wormhole::register_emitter(); + init_token_bridge_state(signer_cap, emitter_cap); + } + + #[test_only] + /// Initialise contracts for testing + /// Returns the token_bridge signer and wormhole signer + public fun init_test(deployer: &signer) { + let (_token_bridge, signer_cap) = account::create_resource_account(deployer, b"token_bridge"); + init_internal(signer_cap); + } +} diff --git a/aptos/token_bridge/sources/token_hash.move b/aptos/token_bridge/sources/token_hash.move new file mode 100644 index 000000000..43993984e --- /dev/null +++ b/aptos/token_bridge/sources/token_hash.move @@ -0,0 +1,64 @@ +/// 32 byte hash representing an arbitrary Aptos token, to be used in VAAs to +/// refer to coins. +module token_bridge::token_hash { + use aptos_framework::type_info; + use std::hash; + use std::string; + + use wormhole::external_address::{Self, ExternalAddress}; + + struct TokenHash has drop, copy, store { + // 32 bytes + hash: vector, + } + + public fun get_external_address(a: &TokenHash): ExternalAddress { + external_address::from_bytes(a.hash) + } + + /// Get the 32 token address of an arbitary CoinType + public fun derive(): TokenHash { + let type_name = type_info::type_name(); + let hash = hash::sha3_256(*string::bytes(&type_name)); + TokenHash { hash } + } + +} + +#[test_only] +module token_bridge::token_hash_test { + use token_bridge::token_hash; + use wormhole::external_address; + use wrapped_coin::coin; + + use std::type_info; + use std::string; + + struct MyCoin {} + + #[test] + public fun test_type_name() { + let t = type_info::type_name(); + assert!(*string::bytes(&t) == b"0x84a5f374d29fc77e370014dce4fd6a55b58ad608de8074b0be5571701724da31::token_hash_test::MyCoin", 0) + } + + #[test] + public fun test_derive() { + let t = token_hash::derive(); + let expected = x"4f69c5d0be57aee780277b1179e4833d61f0563869145e971d24a4e49fcd9302"; + assert!(token_hash::get_external_address(&t) == external_address::from_bytes(expected), 0); + } + + #[test] + public fun test_type_name_T() { + let t = type_info::type_name(); + assert!(*string::bytes(&t) == b"0xf4f53cc591e5190eddbc43940746e2b5deea6e0e1562b2bba765d488504842c7::coin::T", 0) + } + + #[test] + public fun test_derive_T() { + let t = token_hash::derive(); + let expected = x"f0dcbf26a2d59b2196630ed6d5fb5c5bc4fd33996c9f31f19d29389d0c8e7ec2"; + assert!(token_hash::get_external_address(&t) == external_address::from_bytes(expected), 0); + } +} diff --git a/aptos/token_bridge/sources/transfer_tokens.move b/aptos/token_bridge/sources/transfer_tokens.move new file mode 100644 index 000000000..6b7c33bf3 --- /dev/null +++ b/aptos/token_bridge/sources/transfer_tokens.move @@ -0,0 +1,286 @@ +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_with_signer( + sender: &signer, + amount: u64, + recipient_chain: u64, + recipient: vector, + relayer_fee: u64, + wormhole_fee: u64, + nonce: u64 + ): u64 { + let coins = coin::withdraw(sender, amount); + 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 + const ETHEREUM_TOKEN_REG: vector = x"0100000000010015d405c74be6d93c3c33ed6b48d8db70dfb31e0981f8098b2a6c7583083e0c3343d4a1abeb3fc1559674fa067b0c0e2e9de2fafeaecdfeae132de2c33c9d27cc0100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000016911ae00000000000000000000000000000000000000000000546f6b656e427269646765010000000200000000000000000000000000000000000000000000000000000000deadbeef"; + + /// Attestation VAA sent from the ethereum token bridge 0xdeadbeef + 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 coin has 12 decimals, so the amount gets scaled by a factor 10^-4 + // since the normalised amounts are 8 decimals + assert!(normalized_amount::get_amount(normalized_amount) == 10, 0); + assert!(normalized_amount::get_amount(normalized_relayer_fee) == 0, 0); + } + + #[test(aptos_framework = @aptos_framework, token_bridge=@token_bridge, deployer=@deployer)] + #[expected_failure(abort_code = 0)] + 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); + } +} diff --git a/aptos/token_bridge/sources/vaa.move b/aptos/token_bridge/sources/vaa.move new file mode 100644 index 000000000..6df17b96b --- /dev/null +++ b/aptos/token_bridge/sources/vaa.move @@ -0,0 +1,141 @@ +/// Token Bridge VAA utilities +module token_bridge::vaa { + use std::option; + use wormhole::vaa::{Self, VAA}; + use token_bridge::state; + + friend token_bridge::complete_transfer; + friend token_bridge::complete_transfer_with_payload; + friend token_bridge::contract_upgrade; + friend token_bridge::register_chain; + friend token_bridge::wrapped; + + #[test_only] + friend token_bridge::vaa_test; + + /// We have no registration for this chain + const E_UNKNOWN_CHAIN: u64 = 0; + /// We have a registration, but it's different from what's given + const E_UNKNOWN_EMITTER: u64 = 1; + + /// Aborts if the VAA has already been consumed. Marks the VAA as consumed + /// the first time around. + public(friend) fun replay_protect(vaa: &VAA) { + // this calls set::add which aborts if the element already exists + state::set_vaa_consumed(vaa::get_hash(vaa)); + } + + /// Asserts that the VAA is from a known token bridge. + public fun assert_known_emitter(vm: &VAA) { + let maybe_emitter = state::get_registered_emitter(vaa::get_emitter_chain(vm)); + assert!(option::is_some(&maybe_emitter), E_UNKNOWN_CHAIN); + + let emitter = option::extract(&mut maybe_emitter); + assert!(emitter == vaa::get_emitter_address(vm), E_UNKNOWN_EMITTER); + } + + /// Parses, verifies, and replay protects a token bridge VAA. + /// Aborts if the VAA is not from a known token bridge emitter. + /// + /// Has a 'friend' visibility so that it's only callable by the token bridge + /// (otherwise the replay protection could be abused to DoS the bridge) + public(friend) fun parse_verify_and_replay_protect(vaa: vector): VAA { + let vaa = parse_and_verify(vaa); + replay_protect(&vaa); + vaa + } + + /// Parses, and verifies a token bridge VAA. + /// Aborts if the VAA is not from a known token bridge emitter. + public fun parse_and_verify(vaa: vector): VAA { + let vaa = vaa::parse_and_verify(vaa); + assert_known_emitter(&vaa); + vaa + } +} + +#[test_only] +module token_bridge::vaa_test { + use token_bridge::vaa; + use token_bridge::state; + use token_bridge::token_bridge; + + use wormhole::vaa as core_vaa; + use wormhole::wormhole; + use wormhole::u16; + use wormhole::external_address; + + /// VAA sent from the ethereum token bridge 0xdeadbeef + const VAA: vector = x"01000000000100102d399190fa61daccb11c2ea4f7a3db3a9365e5936bcda4cded87c1b9eeb095173514f226256d5579af71d4089eb89496befb998075ba94cd1d4460c5c57b84000000000100000001000200000000000000000000000000000000000000000000000000000000deadbeef0000000002634973000200000000000000000000000000000000000000000000000000000000beefface00020c0000000000000000000000000000000000000000000000000000000042454546000000000000000000000000000000000042656566206661636520546f6b656e"; + + fun setup(deployer: &signer) { + let aptos_framework = std::account::create_account_for_test(@aptos_framework); + std::timestamp::set_time_has_started_for_testing(&aptos_framework); + wormhole::init_test( + 22, + 1, + x"0000000000000000000000000000000000000000000000000000000000000004", + x"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe", + 0 + ); + token_bridge::init_test(deployer); + } + + #[test(deployer = @deployer)] + #[expected_failure(abort_code = 0)] // E_UNKNOWN_CHAIN + public fun test_unknown_chain(deployer: &signer) { + setup(deployer); + let vaa = vaa::parse_verify_and_replay_protect(VAA); + core_vaa::destroy(vaa); + } + + #[test(deployer = @deployer)] + #[expected_failure(abort_code = 1)] // E_UNKNOWN_EMITTER + public fun test_unknown_emitter(deployer: &signer) { + setup(deployer); + state::set_registered_emitter( + u16::from_u64(2), + external_address::from_bytes(x"deadbeed"), // not deadbeef + ); + let vaa = vaa::parse_verify_and_replay_protect(VAA); + core_vaa::destroy(vaa); + } + + #[test(deployer = @deployer)] + public fun test_known_emitter(deployer: &signer) { + setup(deployer); + state::set_registered_emitter( + u16::from_u64(2), + external_address::from_bytes(x"deadbeef"), + ); + let vaa = vaa::parse_verify_and_replay_protect(VAA); + core_vaa::destroy(vaa); + } + + #[test(deployer = @deployer)] + #[expected_failure(abort_code = 25607)] // add_box error + public fun test_replay_protect(deployer: &signer) { + setup(deployer); + state::set_registered_emitter( + u16::from_u64(2), + external_address::from_bytes(x"deadbeef"), + ); + let vaa = vaa::parse_verify_and_replay_protect(VAA); + core_vaa::destroy(vaa); + let vaa = vaa::parse_verify_and_replay_protect(VAA); + core_vaa::destroy(vaa); + } + + #[test(deployer = @deployer)] + public fun test_can_verify_after_replay_protect(deployer: &signer) { + setup(deployer); + state::set_registered_emitter( + u16::from_u64(2), + external_address::from_bytes(x"deadbeef"), + ); + let vaa = vaa::parse_verify_and_replay_protect(VAA); + core_vaa::destroy(vaa); + let vaa = vaa::parse_and_verify(VAA); + core_vaa::destroy(vaa); + } +} diff --git a/aptos/token_bridge/sources/wrapped.move b/aptos/token_bridge/sources/wrapped.move new file mode 100644 index 000000000..fc9fc3d75 --- /dev/null +++ b/aptos/token_bridge/sources/wrapped.move @@ -0,0 +1,255 @@ +module token_bridge::wrapped { + use aptos_framework::account; + use aptos_framework::coin::{Self, Coin, MintCapability, BurnCapability, FreezeCapability}; + + use wormhole::vaa; + + use token_bridge::state; + use token_bridge::asset_meta::{Self, AssetMeta}; + use token_bridge::deploy_coin::{deploy_coin}; + use token_bridge::vaa as token_bridge_vaa; + use token_bridge::string32; + + friend token_bridge::complete_transfer; + friend token_bridge::complete_transfer_with_payload; + friend token_bridge::transfer_tokens; + + #[test_only] + friend token_bridge::transfer_tokens_test; + #[test_only] + friend token_bridge::wrapped_test; + #[test_only] + friend token_bridge::complete_transfer_test; + + const E_IS_NOT_WRAPPED_ASSET: u64 = 0; + const E_COIN_CAP_DOES_NOT_EXIST: u64 = 1; + + struct CoinCapabilities has key, store { + mint_cap: MintCapability, + freeze_cap: FreezeCapability, + burn_cap: BurnCapability, + } + + // this function is called before create_wrapped_coin + // TODO(csongor): document why these two are in separate transactions + public entry fun create_wrapped_coin_type(vaa: vector) { + // NOTE: we do not do replay protection here, only verify that the VAA + // comes from a known emitter. This is because `create_wrapped_coin` + // itself will need to verify the VAA again in a separate transaction, + // and it itself will perform the replay protection. + // This function cannot be called twice with the same VAA because it + // creates a resource account, which will fail the second time if the + // account already exists. + // TODO(csongor): should we implement a more explicit replay protection + // for this function? + let vaa = token_bridge_vaa::parse_and_verify(vaa); + let asset_meta = asset_meta::parse(vaa::destroy(vaa)); + let seed = asset_meta::create_seed(&asset_meta); + + //create resource account + let token_bridge_signer = state::token_bridge_signer(); + let (new_signer, new_cap) = account::create_resource_account(&token_bridge_signer, seed); + + let token_address = asset_meta::get_token_address(&asset_meta); + let token_chain = asset_meta::get_token_chain(&asset_meta); + let origin_info = state::create_origin_info(token_chain, token_address); + + deploy_coin(&new_signer); + state::set_wrapped_asset_signer_capability(origin_info, new_cap); + } + + // this function is called in tandem with bridge_implementation::create_wrapped_coin_type + // initializes a coin for CoinType, updates mappings in State + public entry fun create_wrapped_coin(vaa: vector) { + let vaa = token_bridge_vaa::parse_verify_and_replay_protect(vaa); + let asset_meta: AssetMeta = asset_meta::parse(vaa::destroy(vaa)); + + let native_token_address = asset_meta::get_token_address(&asset_meta); + let native_token_chain = asset_meta::get_token_chain(&asset_meta); + let origin_info = state::create_origin_info(native_token_chain, native_token_address); + + // The CoinType type variable is instantiated by the caller of the + // function, so a malicious actor could try and pass in something other + // than what we're expecting based on the VAA. So how do we protect + // against this? The signer capability is keyed by the origin info of + // the token, and a coin can only be initialised by the signer that owns + // the module that defines the CoinType. + // See the `test_create_wrapped_coin_bad_type` negative test below. + let coin_signer = state::get_wrapped_asset_signer(origin_info); + init_wrapped_coin(&coin_signer, &asset_meta) + } + + public(friend) fun init_wrapped_coin( + coin_signer: &signer, + asset_meta: &AssetMeta, + ) { + // initialize new coin using CoinType + let name = asset_meta::get_name(asset_meta); + let symbol = asset_meta::get_symbol(asset_meta); + let decimals = asset_meta::get_decimals(asset_meta); + let monitor_supply = true; + let (burn_cap, freeze_cap, mint_cap) + = coin::initialize( + coin_signer, + string32::to_string(&name), + // take the first 10 characters of the symbol (maximum in aptos) + string32::take_utf8(string32::to_string(&symbol), 10), + decimals, + monitor_supply + ); + + let token_address = asset_meta::get_token_address(asset_meta); + let token_chain = asset_meta::get_token_chain(asset_meta); + let origin_info = state::create_origin_info(token_chain, token_address); + + // update the following two mappings in State + // 1. (native chain, native address) => wrapped address + // 2. wrapped address => (native chain, native address) + state::setup_wrapped(origin_info); + + // store coin capabilities + let token_bridge = state::token_bridge_signer(); + move_to(&token_bridge, CoinCapabilities { mint_cap, freeze_cap, burn_cap }); + } + + public(friend) fun mint(amount: u64): Coin acquires CoinCapabilities { + assert!(state::is_wrapped_asset(), E_IS_NOT_WRAPPED_ASSET); + assert!(exists>(@token_bridge), E_COIN_CAP_DOES_NOT_EXIST); + let caps = borrow_global>(@token_bridge); + let mint_cap = &caps.mint_cap; + let coins = coin::mint(amount, mint_cap); + coins + } + + public(friend) fun burn(coins: Coin) acquires CoinCapabilities { + assert!(state::is_wrapped_asset(), E_IS_NOT_WRAPPED_ASSET); + assert!(exists>(@token_bridge), E_COIN_CAP_DOES_NOT_EXIST); + let caps = borrow_global>(@token_bridge); + let burn_cap = &caps.burn_cap; + coin::burn(coins, burn_cap); + } + +} + +#[test_only] +module token_bridge::wrapped_test { + use aptos_framework::account; + use aptos_framework::coin; + use aptos_framework::string::{utf8}; + use aptos_framework::type_info::{type_of}; + use aptos_framework::option; + + use token_bridge::token_bridge::{Self as bridge}; + use token_bridge::state; + use token_bridge::wrapped; + use token_bridge::asset_meta; + use token_bridge::string32; + + use token_bridge::register_chain; + + use wormhole::u16::{Self}; + use wrapped_coin::coin::T; + use wormhole::external_address::{Self}; + + /// Registration VAA for the etheruem token bridge 0xdeadbeef + const ETHEREUM_TOKEN_REG: vector = x"0100000000010015d405c74be6d93c3c33ed6b48d8db70dfb31e0981f8098b2a6c7583083e0c3343d4a1abeb3fc1559674fa067b0c0e2e9de2fafeaecdfeae132de2c33c9d27cc0100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000016911ae00000000000000000000000000000000000000000000546f6b656e427269646765010000000200000000000000000000000000000000000000000000000000000000deadbeef"; + + /// Attestation VAA sent from the ethereum token bridge 0xdeadbeef + const ATTESTATION_VAA: vector = x"0100000000010080366065746148420220f25a6275097370e8db40984529a6676b7a5fc9feb11755ec49ca626b858ddfde88d15601f85ab7683c5f161413b0412143241c700aff010000000100000001000200000000000000000000000000000000000000000000000000000000deadbeef000000000150eb23000200000000000000000000000000000000000000000000000000000000beefface00020c424545460000000000000000000000000000000000000000000000000000000042656566206661636520546f6b656e0000000000000000000000000000000000"; + + fun setup( + deployer: &signer, + ) { + wormhole::wormhole_test::setup(0); + bridge::init_test(deployer); + } + + public fun init_wrapped_token() { + let chain = wormhole::u16::from_u64(2); + let token_address = external_address::from_bytes(x"deadbeef"); + let asset_meta = asset_meta::create( + token_address, + chain, + 9, + string32::from_bytes(b"foo"), + string32::from_bytes(b"Foo bar token") + ); + let wrapped_coin = account::create_account_for_test(@wrapped_coin); + + // set up the signer capability first + let signer_cap = account::create_test_signer_cap(@wrapped_coin); + let origin_info = state::create_origin_info(chain, token_address); + state::set_wrapped_asset_signer_capability(origin_info, signer_cap); + + wrapped::init_wrapped_coin(&wrapped_coin, &asset_meta); + } + + + #[test(deployer=@deployer)] + #[expected_failure(abort_code = 0)] + fun test_create_wrapped_coin_unregistered(deployer: &signer) { + setup(deployer); + + wrapped::create_wrapped_coin_type(ATTESTATION_VAA); + } + + struct YourCoin {} + + // This test ensures that I can't take a valid attestation VAA and trick the + // token bridge to register my own type. I think what that could lead to is + // a denial of service in case the 3rd party type is belongs to a module + // with an 'arbitrary' upgrade policy which can be deleted in the future. + // This upgrade policy is not enabled in the VM as of writing, but that + // might well change in the future, so we future proof ourselves here. + #[test(deployer=@deployer)] + #[expected_failure(abort_code = 65537)] // ECOIN_INFO_ADDRESS_MISMATCH + fun test_create_wrapped_coin_bad_type(deployer: &signer) { + setup(deployer); + register_chain::submit_vaa(ETHEREUM_TOKEN_REG); + wrapped::create_wrapped_coin_type(ATTESTATION_VAA); + + // initialize coin using type T, move caps to token_bridge, sets bridge state variables + wrapped::create_wrapped_coin(ATTESTATION_VAA); + } + + // test create_wrapped_coin_type and create_wrapped_coin + #[test(deployer=@deployer)] + fun test_create_wrapped_coin(deployer: &signer) { + setup(deployer); + register_chain::submit_vaa(ETHEREUM_TOKEN_REG); + + wrapped::create_wrapped_coin_type(ATTESTATION_VAA); + + // assert coin is NOT initialized + assert!(!coin::is_coin_initialized(), 0); + + // initialize coin using type T, move caps to token_bridge, sets bridge state variables + wrapped::create_wrapped_coin(ATTESTATION_VAA); + + // assert that coin IS initialized + assert!(coin::is_coin_initialized(), 0); + + // assert coin info is correct + assert!(coin::name() == utf8(b"Beef face Token"), 0); + assert!(coin::symbol() == utf8(b"BEEF"), 0); + assert!(coin::decimals() == 12, 0); + + // assert origin address, chain, type_info, is_wrapped are correct + let origin_info = state::origin_info(); + let origin_token_address = state::get_origin_info_token_address(&origin_info); + let origin_token_chain = state::get_origin_info_token_chain(&origin_info); + let wrapped_asset_type_info = state::wrapped_asset_info(origin_info); + let is_wrapped_asset = state::is_wrapped_asset(); + assert!(type_of() == wrapped_asset_type_info, 0); //utf8(b"0xf4f53cc591e5190eddbc43940746e2b5deea6e0e1562b2bba765d488504842c7::coin::T"), 0); + assert!(origin_token_chain == u16::from_u64(2), 0); + assert!(external_address::get_bytes(&origin_token_address) == x"00000000000000000000000000000000000000000000000000000000beefface", 0); + assert!(is_wrapped_asset, 0); + + // load beef face token cap and mint some beef face coins, then burn + let beef_coins = wrapped::mint(10000); + assert!(coin::value(&beef_coins)==10000, 0); + assert!(coin::supply() == option::some(10000), 0); + wrapped::burn(beef_coins); + assert!(coin::supply() == option::some(0), 0); + } +}