// SPDX-License-Identifier: Apache 2 /// This module implements methods that create a specific coin type reflecting a /// wrapped (foreign) asset, whose metadata is encoded in a VAA sent from /// another network. /// /// Wrapped assets are created in two steps. /// 1. `prepare_registration`: This method creates a new `TreasuryCap` for a /// given coin type and wraps an encoded asset metadata VAA. We require a /// one-time witness (OTW) to throw an explicit error (even though it is /// redundant with what `create_currency` requires). This coin will /// be published using this method, meaning the `init` method in that /// untrusted package will have the asset's decimals hard-coded for its /// coin metadata. A `WrappedAssetSetup` object is transferred to the /// transaction sender. /// 2. `complete_registration`: This method destroys the `WrappedAssetSetup` /// object by unpacking its `TreasuryCap`, which will be warehoused in the /// `TokenRegistry`. The shared coin metadata object will be updated to /// reflect the contents of the encoded asset metadata payload. /// /// Wrapped asset metadata can also be updated with a new asset metadata VAA. /// By calling `update_attestation`, Token Bridge verifies that the specific /// coin type is registered and agrees with the encoded asset metadata's /// canonical token info. `ForeignInfo` and the coin's metadata will be updated /// based on the encoded asset metadata payload. /// /// See `state` and `wrapped_asset` modules for more details. /// /// References: /// https://examples.sui.io/basics/one-time-witness.html module token_bridge::create_wrapped { use std::ascii::{Self}; use std::option::{Self}; use std::type_name::{Self}; use sui::coin::{Self, TreasuryCap, CoinMetadata}; use sui::object::{Self, UID}; use sui::package::{UpgradeCap}; use sui::transfer::{Self}; use sui::tx_context::{TxContext}; use token_bridge::asset_meta::{Self}; use token_bridge::normalized_amount::{max_decimals}; use token_bridge::state::{Self, State}; use token_bridge::token_registry::{Self}; use token_bridge::vaa::{Self, TokenBridgeMessage}; use token_bridge::wrapped_asset::{Self}; #[test_only] use token_bridge::version_control::{Self, V__0_2_0 as V__CURRENT}; /// Failed one-time witness verification. const E_BAD_WITNESS: u64 = 0; /// Coin witness does not equal "COIN". const E_INVALID_COIN_MODULE_NAME: u64 = 1; /// Decimals value exceeds `MAX_DECIMALS` from `normalized_amount`. const E_DECIMALS_EXCEED_WRAPPED_MAX: u64 = 2; /// A.K.A. "coin". const COIN_MODULE_NAME: vector = b"coin"; /// Container holding new coin type's `TreasuryCap` and encoded asset metadata /// VAA, which are required to complete this asset's registration. struct WrappedAssetSetup has key, store { id: UID, treasury_cap: TreasuryCap } /// This method is executed within the `init` method of an untrusted module, /// which defines a one-time witness (OTW) type (`CoinType`). OTW is /// required to ensure that only one `TreasuryCap` exists for `CoinType`. This /// is similar to how a `TreasuryCap` is created in `coin::create_currency`. /// /// Because this method is stateless (i.e. no dependency on Token Bridge's /// `State` object), the contract defers VAA verification to /// `complete_registration` after this method has been executed. public fun prepare_registration( witness: CoinType, decimals: u8, ctx: &mut TxContext ): WrappedAssetSetup { let setup = prepare_registration_internal(witness, decimals, ctx); // Also make sure that this witness module name is literally "coin". let module_name = type_name::get_module(&type_name::get()); assert!( ascii::into_bytes(module_name) == COIN_MODULE_NAME, E_INVALID_COIN_MODULE_NAME ); setup } /// This function performs the bulk of `prepare_registration`, except /// checking the module name. This separation is useful for testing. fun prepare_registration_internal( witness: CoinType, decimals: u8, ctx: &mut TxContext ): WrappedAssetSetup { // Make sure there's only one instance of the type `CoinType`. This // resembles the same check for `coin::create_currency`. // Technically this check is redundant as it's performed by // `coin::create_currency` below, but it doesn't hurt. assert!(sui::types::is_one_time_witness(&witness), E_BAD_WITNESS); // Ensure that the decimals passed into this method do not exceed max // decimals (see `normalized_amount` module). assert!(decimals <= max_decimals(), E_DECIMALS_EXCEED_WRAPPED_MAX); // We initialise the currency with empty metadata. Later on, in the // `complete_registration` call, when `CoinType` gets associated with a // VAA, we update these fields. let no_symbol = b""; let no_name = b""; let no_description = b""; let no_icon_url = option::none(); let (treasury_cap, coin_meta) = coin::create_currency( witness, decimals, no_symbol, no_name, no_description, no_icon_url, ctx ); // The CoinMetadata is turned into a shared object so that other // functions (and wallets) can easily grab references to it. This is // safe to do, as the metadata setters require a `TreasuryCap` for the // coin too, which is held by the token bridge. transfer::public_share_object(coin_meta); // Create `WrappedAssetSetup` object and transfer to transaction sender. // The owner of this object will call `complete_registration` to destroy // it. WrappedAssetSetup { id: object::new(ctx), treasury_cap } } /// After executing `prepare_registration`, owner of `WrappedAssetSetup` /// executes this method to complete this wrapped asset's registration. /// /// This method destroys `WrappedAssetSetup`, unpacking the `TreasuryCap` and /// encoded asset metadata VAA. The deserialized asset metadata VAA is used /// to update the associated `CoinMetadata`. public fun complete_registration( token_bridge_state: &mut State, coin_meta: &mut CoinMetadata, setup: WrappedAssetSetup, coin_upgrade_cap: UpgradeCap, msg: TokenBridgeMessage ) { // This capability ensures that the current build version is used. This // call performs an additional check of whether `WrappedAssetSetup` was // created using the current package. let latest_only = state::assert_latest_only_specified(token_bridge_state); let WrappedAssetSetup { id, treasury_cap } = setup; // Finally destroy the object. object::delete(id); // Deserialize to `AssetMeta`. let token_meta = asset_meta::deserialize(vaa::take_payload(msg)); // `register_wrapped_asset` uses `token_registry::add_new_wrapped`, // which will check whether the asset has already been registered and if // the token chain ID is not Sui's. // // If both of these conditions are met, `register_wrapped_asset` will // succeed and the new wrapped coin will be registered. token_registry::add_new_wrapped( state::borrow_mut_token_registry(&latest_only, token_bridge_state), token_meta, coin_meta, treasury_cap, coin_upgrade_cap ); } /// For registered wrapped assets, we can update `ForeignInfo` for a /// given `CoinType` with a new asset meta VAA emitted from another network. public fun update_attestation( token_bridge_state: &mut State, coin_meta: &mut CoinMetadata, msg: TokenBridgeMessage ) { // This capability ensures that the current build version is used. let latest_only = state::assert_latest_only(token_bridge_state); // Deserialize to `AssetMeta`. let token_meta = asset_meta::deserialize(vaa::take_payload(msg)); // This asset must exist in the registry. let registry = state::borrow_mut_token_registry(&latest_only, token_bridge_state); token_registry::assert_has(registry); // Now update wrapped. wrapped_asset::update_metadata( token_registry::borrow_mut_wrapped(registry), coin_meta, token_meta ); } public fun incomplete_metadata( coin_meta: &CoinMetadata ): bool { use std::string::{bytes}; use std::vector::{is_empty}; ( is_empty(ascii::as_bytes(&coin::get_symbol(coin_meta))) && is_empty(bytes(&coin::get_name(coin_meta))) && is_empty(bytes(&coin::get_description(coin_meta))) && std::option::is_none(&coin::get_icon_url(coin_meta)) ) } #[test_only] public fun new_setup_test_only( _version: Version, witness: CoinType, decimals: u8, ctx: &mut TxContext ): (WrappedAssetSetup, UpgradeCap) { let setup = prepare_registration_internal( witness, decimals, ctx ); let upgrade_cap = sui::package::test_publish( object::id_from_address(@token_bridge), ctx ); (setup, upgrade_cap) } #[test_only] public fun new_setup_current( witness: CoinType, decimals: u8, ctx: &mut TxContext ): (WrappedAssetSetup, UpgradeCap) { new_setup_test_only( version_control::current_version_test_only(), witness, decimals, ctx ) } #[test_only] public fun take_treasury_cap( setup: WrappedAssetSetup ): TreasuryCap { let WrappedAssetSetup { id, treasury_cap } = setup; object::delete(id); treasury_cap } } #[test_only] module token_bridge::create_wrapped_tests { use sui::coin::{Self}; use sui::test_scenario::{Self}; use sui::test_utils::{Self}; use sui::tx_context::{Self}; use wormhole::wormhole_scenario::{parse_and_verify_vaa}; use token_bridge::asset_meta::{Self}; use token_bridge::coin_wrapped_12::{Self}; use token_bridge::coin_wrapped_7::{Self}; use token_bridge::create_wrapped::{Self}; use token_bridge::state::{Self}; use token_bridge::string_utils::{Self}; use token_bridge::token_bridge_scenario::{ register_dummy_emitter, return_state, set_up_wormhole_and_token_bridge, take_state, two_people }; use token_bridge::token_registry::{Self}; use token_bridge::vaa::{Self}; use token_bridge::version_control::{V__0_2_0 as V__CURRENT}; use token_bridge::wrapped_asset::{Self}; struct NOT_A_WITNESS has drop {} struct CREATE_WRAPPED_TESTS has drop {} #[test] #[expected_failure(abort_code = create_wrapped::E_BAD_WITNESS)] fun test_cannot_prepare_registration_bad_witness() { let ctx = &mut tx_context::dummy(); // You shall not pass! let wrapped_asset_setup = create_wrapped::prepare_registration( NOT_A_WITNESS {}, 3, ctx ); // Clean up. test_utils::destroy(wrapped_asset_setup); abort 42 } #[test] #[expected_failure(abort_code = create_wrapped::E_INVALID_COIN_MODULE_NAME)] fun test_cannot_prepare_registration_invalid_coin_module_name() { let ctx = &mut tx_context::dummy(); // You shall not pass! let wrapped_asset_setup = create_wrapped::prepare_registration< CREATE_WRAPPED_TESTS, V__CURRENT >( CREATE_WRAPPED_TESTS {}, 3, ctx ); // Clean up. test_utils::destroy(wrapped_asset_setup); abort 42 } #[test] fun test_complete_and_update_attestation() { let (caller, coin_deployer) = two_people(); let my_scenario = test_scenario::begin(caller); let scenario = &mut my_scenario; // Set up contracts. let wormhole_fee = 350; set_up_wormhole_and_token_bridge(scenario, wormhole_fee); // Register foreign emitter on chain ID == 2. let expected_source_chain = 2; register_dummy_emitter(scenario, expected_source_chain); // Ignore effects. Make sure `coin_deployer` receives // `WrappedAssetSetup`. test_scenario::next_tx(scenario, coin_deployer); // Publish coin. let ( wrapped_asset_setup, upgrade_cap ) = create_wrapped::new_setup_current( CREATE_WRAPPED_TESTS {}, 8, test_scenario::ctx(scenario) ); let token_bridge_state = take_state(scenario); let verified_vaa = parse_and_verify_vaa(scenario, coin_wrapped_12::encoded_vaa()); let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa); let coin_meta = test_scenario::take_shared(scenario); create_wrapped::complete_registration( &mut token_bridge_state, &mut coin_meta, wrapped_asset_setup, upgrade_cap, msg ); let ( token_address, token_chain, native_decimals, symbol, name ) = asset_meta::unpack_test_only(coin_wrapped_12::token_meta()); // Check registry. { let registry = state::borrow_token_registry(&token_bridge_state); let verified = token_registry::verified_asset(registry); assert!(token_registry::is_wrapped(&verified), 0); let asset = token_registry::borrow_wrapped(registry); assert!(wrapped_asset::total_supply(asset) == 0, 0); // Decimals are capped for this wrapped asset. assert!(coin::get_decimals(&coin_meta) == 8, 0); // Check metadata against asset metadata. let info = wrapped_asset::info(asset); assert!(wrapped_asset::token_chain(info) == token_chain, 0); assert!(wrapped_asset::token_address(info) == token_address, 0); assert!( wrapped_asset::native_decimals(info) == native_decimals, 0 ); assert!(coin::get_symbol(&coin_meta) == string_utils::to_ascii(&symbol), 0); assert!(coin::get_name(&coin_meta) == name, 0); }; // Now update metadata. let verified_vaa = parse_and_verify_vaa( scenario, coin_wrapped_12::encoded_updated_vaa() ); let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa); create_wrapped::update_attestation( &mut token_bridge_state, &mut coin_meta, msg ); // Check updated name and symbol. let ( _, _, _, new_symbol, new_name ) = asset_meta::unpack_test_only(coin_wrapped_12::updated_token_meta()); assert!(symbol != new_symbol, 0); assert!(coin::get_symbol(&coin_meta) == string_utils::to_ascii(&new_symbol), 0); assert!(name != new_name, 0); assert!(coin::get_name(&coin_meta) == new_name, 0); test_scenario::return_shared(coin_meta); // Clean up. return_state(token_bridge_state); // Done. test_scenario::end(my_scenario); } #[test] #[expected_failure(abort_code = wrapped_asset::E_ASSET_META_MISMATCH)] fun test_cannot_update_attestation_wrong_canonical_info() { let (caller, coin_deployer) = two_people(); let my_scenario = test_scenario::begin(caller); let scenario = &mut my_scenario; // Set up contracts. let wormhole_fee = 350; set_up_wormhole_and_token_bridge(scenario, wormhole_fee); // Register foreign emitter on chain ID == 2. let expected_source_chain = 2; register_dummy_emitter(scenario, expected_source_chain); // Ignore effects. Make sure `coin_deployer` receives // `WrappedAssetSetup`. test_scenario::next_tx(scenario, coin_deployer); // Publish coin. let ( wrapped_asset_setup, upgrade_cap ) = create_wrapped::new_setup_current( CREATE_WRAPPED_TESTS {}, 8, test_scenario::ctx(scenario) ); let token_bridge_state = take_state(scenario); let verified_vaa = parse_and_verify_vaa(scenario, coin_wrapped_12::encoded_vaa()); let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa); let coin_meta = test_scenario::take_shared(scenario); create_wrapped::complete_registration( &mut token_bridge_state, &mut coin_meta, wrapped_asset_setup, upgrade_cap, msg ); // This VAA is for COIN_WRAPPED_7 metadata, which disagrees with // COIN_WRAPPED_12. let invalid_asset_meta_vaa = coin_wrapped_7::encoded_vaa(); let verified_vaa = parse_and_verify_vaa(scenario, invalid_asset_meta_vaa); let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa); // You shall not pass! create_wrapped::update_attestation( &mut token_bridge_state, &mut coin_meta, msg ); abort 42 } #[test] #[expected_failure(abort_code = state::E_VERSION_MISMATCH)] fun test_cannot_complete_registration_version_mismatch() { let (caller, coin_deployer) = two_people(); let my_scenario = test_scenario::begin(caller); let scenario = &mut my_scenario; // Set up contracts. let wormhole_fee = 350; set_up_wormhole_and_token_bridge(scenario, wormhole_fee); // Register foreign emitter on chain ID == 2. let expected_source_chain = 2; register_dummy_emitter(scenario, expected_source_chain); // Ignore effects. Make sure `coin_deployer` receives // `WrappedAssetSetup`. test_scenario::next_tx(scenario, coin_deployer); // Publish coin. let ( wrapped_asset_setup, upgrade_cap ) = create_wrapped::new_setup_test_only( token_bridge::version_control::dummy(), CREATE_WRAPPED_TESTS {}, 8, test_scenario::ctx(scenario) ); let token_bridge_state = take_state(scenario); let verified_vaa = parse_and_verify_vaa(scenario, coin_wrapped_12::encoded_vaa()); let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa); let coin_meta = test_scenario::take_shared(scenario); create_wrapped::complete_registration( &mut token_bridge_state, &mut coin_meta, wrapped_asset_setup, upgrade_cap, msg ); abort 42 } #[test] #[expected_failure(abort_code = wormhole::package_utils::E_NOT_CURRENT_VERSION)] fun test_cannot_complete_registration_outdated_version() { let (caller, coin_deployer) = two_people(); let my_scenario = test_scenario::begin(caller); let scenario = &mut my_scenario; // Set up contracts. let wormhole_fee = 350; set_up_wormhole_and_token_bridge(scenario, wormhole_fee); // Register foreign emitter on chain ID == 2. let expected_source_chain = 2; register_dummy_emitter(scenario, expected_source_chain); // Ignore effects. Make sure `coin_deployer` receives // `WrappedAssetSetup`. test_scenario::next_tx(scenario, coin_deployer); // Publish coin. let ( wrapped_asset_setup, upgrade_cap ) = create_wrapped::new_setup_current( CREATE_WRAPPED_TESTS {}, 8, test_scenario::ctx(scenario) ); let token_bridge_state = take_state(scenario); let verified_vaa = parse_and_verify_vaa(scenario, coin_wrapped_12::encoded_vaa()); let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa); let coin_meta = test_scenario::take_shared(scenario); // Conveniently roll version back. state::reverse_migrate_version(&mut token_bridge_state); // Simulate executing with an outdated build by upticking the minimum // required version for `publish_message` to something greater than // this build. state::migrate_version_test_only( &mut token_bridge_state, token_bridge::version_control::previous_version_test_only(), token_bridge::version_control::next_version() ); // You shall not pass! create_wrapped::complete_registration( &mut token_bridge_state, &mut coin_meta, wrapped_asset_setup, upgrade_cap, msg ); abort 42 } }