wormhole/sui/token_bridge/sources/create_wrapped.move

643 lines
22 KiB
Plaintext

// 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<u8> = 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<phantom CoinType, phantom Version> has key, store {
id: UID,
treasury_cap: TreasuryCap<CoinType>
}
/// 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<CoinType: drop, Version>(
witness: CoinType,
decimals: u8,
ctx: &mut TxContext
): WrappedAssetSetup<CoinType, Version> {
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<CoinType>());
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<CoinType: drop, Version>(
witness: CoinType,
decimals: u8,
ctx: &mut TxContext
): WrappedAssetSetup<CoinType, Version> {
// 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<CoinType: drop, Version>(
token_bridge_state: &mut State,
coin_meta: &mut CoinMetadata<CoinType>,
setup: WrappedAssetSetup<CoinType, Version>,
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<Version>(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<CoinType>(
token_bridge_state: &mut State,
coin_meta: &mut CoinMetadata<CoinType>,
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<CoinType>(registry);
// Now update wrapped.
wrapped_asset::update_metadata(
token_registry::borrow_mut_wrapped<CoinType>(registry),
coin_meta,
token_meta
);
}
public fun incomplete_metadata<CoinType>(
coin_meta: &CoinMetadata<CoinType>
): 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<CoinType: drop, Version: drop>(
_version: Version,
witness: CoinType,
decimals: u8,
ctx: &mut TxContext
): (WrappedAssetSetup<CoinType, Version>, 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<CoinType: drop>(
witness: CoinType,
decimals: u8,
ctx: &mut TxContext
): (WrappedAssetSetup<CoinType, V__CURRENT>, UpgradeCap) {
new_setup_test_only(
version_control::current_version_test_only(),
witness,
decimals,
ctx
)
}
#[test_only]
public fun take_treasury_cap<CoinType>(
setup: WrappedAssetSetup<CoinType, V__CURRENT>
): TreasuryCap<CoinType> {
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, V__CURRENT>(
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<CREATE_WRAPPED_TESTS>(registry);
assert!(token_registry::is_wrapped(&verified), 0);
let asset =
token_registry::borrow_wrapped<CREATE_WRAPPED_TESTS>(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<CREATE_WRAPPED_TESTS>(
&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<CREATE_WRAPPED_TESTS>(
&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
}
}