wormhole/sui/token_bridge/sources/attest_token.move

386 lines
13 KiB
Plaintext

// SPDX-License-Identifier: Apache 2
/// This module implements the method `attest_token` which allows someone
/// to send asset metadata of a coin type native to Sui. Part of this process
/// is registering this asset in the `TokenRegistry`.
///
/// NOTE: If an asset has not been attested for, it cannot be bridged using
/// `transfer_tokens` or `transfer_tokens_with_payload`.
///
/// See `asset_meta` module for serialization and deserialization of Wormhole
/// message payload.
module token_bridge::attest_token {
use sui::coin::{CoinMetadata};
use wormhole::publish_message::{MessageTicket};
use token_bridge::asset_meta::{Self};
use token_bridge::create_wrapped::{Self};
use token_bridge::state::{Self, State, LatestOnly};
use token_bridge::token_registry::{Self};
/// Coin type belongs to a wrapped asset.
const E_WRAPPED_ASSET: u64 = 0;
/// Coin type belongs to an untrusted contract from `create_wrapped` which
/// has not completed registration.
const E_FROM_CREATE_WRAPPED: u64 = 1;
/// `attest_token` takes `CoinMetadata` of a coin type and generates a
/// `MessageTicket` with encoded asset metadata for a foreign Token Bridge
/// contract to consume and create a wrapped asset reflecting this Sui
/// asset. Asset metadata is encoded using `AssetMeta`.
///
/// See `token_registry` and `asset_meta` module for more info.
public fun attest_token<CoinType>(
token_bridge_state: &mut State,
coin_meta: &CoinMetadata<CoinType>,
nonce: u32
): MessageTicket {
// This capability ensures that the current build version is used.
let latest_only = state::assert_latest_only(token_bridge_state);
// Encode Wormhole message payload.
let encoded_asset_meta =
serialize_asset_meta(&latest_only, token_bridge_state, coin_meta);
// Prepare Wormhole message.
state::prepare_wormhole_message(
&latest_only,
token_bridge_state,
nonce,
encoded_asset_meta
)
}
fun serialize_asset_meta<CoinType>(
latest_only: &LatestOnly,
token_bridge_state: &mut State,
coin_meta: &CoinMetadata<CoinType>,
): vector<u8> {
let registry = state::borrow_token_registry(token_bridge_state);
// Register if it is a new asset.
//
// NOTE: We don't want to abort if the asset is already registered
// because we may want to send asset metadata again after registration
// (the owner of a particular `CoinType` can change `CoinMetadata` any
// time after we register the asset).
if (token_registry::has<CoinType>(registry)) {
let asset_info = token_registry::verified_asset<CoinType>(registry);
// If this asset is already registered, there should already
// be canonical info associated with this coin type.
assert!(
!token_registry::is_wrapped(&asset_info),
E_WRAPPED_ASSET
);
} else {
// Before we consider registering, we should not accidentally
// perform this registration that may be the `CoinMetadata` from
// `create_wrapped::prepare_registration`, which has empty fields.
assert!(
!create_wrapped::incomplete_metadata(coin_meta),
E_FROM_CREATE_WRAPPED
);
// Now register it.
token_registry::add_new_native(
state::borrow_mut_token_registry(
latest_only,
token_bridge_state
),
coin_meta
);
};
asset_meta::serialize(asset_meta::from_metadata(coin_meta))
}
#[test_only]
public fun serialize_asset_meta_test_only<CoinType>(
token_bridge_state: &mut State,
coin_metadata: &CoinMetadata<CoinType>,
): vector<u8> {
// This capability ensures that the current build version is used.
let latest_only = state::assert_latest_only(token_bridge_state);
serialize_asset_meta(&latest_only, token_bridge_state, coin_metadata)
}
}
#[test_only]
module token_bridge::attest_token_tests {
use std::ascii::{Self};
use std::string::{Self};
use sui::coin::{Self};
use sui::test_scenario::{Self};
use wormhole::publish_message::{Self};
use wormhole::state::{chain_id};
use token_bridge::asset_meta::{Self};
use token_bridge::attest_token::{Self};
use token_bridge::coin_native_10::{Self, COIN_NATIVE_10};
use token_bridge::coin_wrapped_7::{Self, COIN_WRAPPED_7};
use token_bridge::native_asset::{Self};
use token_bridge::state::{Self};
use token_bridge::token_bridge_scenario::{
person,
return_state,
set_up_wormhole_and_token_bridge,
take_state,
};
use token_bridge::token_registry::{Self};
#[test]
fun test_attest_token() {
use token_bridge::attest_token::{attest_token};
let user = person();
let my_scenario = test_scenario::begin(user);
let scenario = &mut my_scenario;
// Publish coin.
coin_native_10::init_test_only(test_scenario::ctx(scenario));
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Ignore effects.
test_scenario::next_tx(scenario, user);
let token_bridge_state = take_state(scenario);
let coin_meta = coin_native_10::take_metadata(scenario);
// Emit `AssetMeta` payload.
let prepared_msg =
attest_token(
&mut token_bridge_state,
&coin_meta,
1234, // nonce
);
// Ignore effects.
test_scenario::next_tx(scenario, user);
// Check that asset is registered.
{
let registry =
state::borrow_token_registry(&token_bridge_state);
let verified =
token_registry::verified_asset<COIN_NATIVE_10>(registry);
assert!(!token_registry::is_wrapped(&verified), 0);
let asset = token_registry::borrow_native<COIN_NATIVE_10>(registry);
let expected_token_address =
native_asset::canonical_address(&coin_meta);
assert!(
native_asset::token_address(asset) == expected_token_address,
0
);
assert!(native_asset::decimals(asset) == 10, 0);
let (
token_chain,
token_address
) = native_asset::canonical_info(asset);
assert!(token_chain == chain_id(), 0);
assert!(token_address == expected_token_address, 0);
assert!(native_asset::custody(asset) == 0, 0);
};
// Clean up for next call.
publish_message::destroy(prepared_msg);
// Update metadata.
let new_symbol = {
use std::vector::{Self};
let symbol = coin::get_symbol(&coin_meta);
let buf = ascii::into_bytes(symbol);
vector::reverse(&mut buf);
ascii::string(buf)
};
let new_name = coin::get_name(&coin_meta);
string::append(&mut new_name, string::utf8(b"??? and profit"));
let treasury_cap = coin_native_10::take_treasury_cap(scenario);
coin::update_symbol(&treasury_cap, &mut coin_meta, new_symbol);
coin::update_name(&treasury_cap, &mut coin_meta, new_name);
// We should be able to call `attest_token` any time after.
let prepared_msg =
attest_token(
&mut token_bridge_state,
&coin_meta,
1234, // nonce
);
// Clean up.
publish_message::destroy(prepared_msg);
return_state(token_bridge_state);
coin_native_10::return_globals(treasury_cap, coin_meta);
// Done.
test_scenario::end(my_scenario);
}
#[test]
fun test_serialize_asset_meta() {
use token_bridge::attest_token::{serialize_asset_meta_test_only};
let user = person();
let my_scenario = test_scenario::begin(user);
let scenario = &mut my_scenario;
// Publish coin.
coin_native_10::init_test_only(test_scenario::ctx(scenario));
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Proceed to next operation.
test_scenario::next_tx(scenario, user);
let token_bridge_state = take_state(scenario);
let coin_meta = coin_native_10::take_metadata(scenario);
// Emit `AssetMeta` payload.
let serialized =
serialize_asset_meta_test_only(&mut token_bridge_state, &coin_meta);
let expected_serialized =
asset_meta::serialize_test_only(
asset_meta::from_metadata_test_only(&coin_meta)
);
assert!(serialized == expected_serialized, 0);
// Update metadata.
let new_symbol = {
use std::vector::{Self};
let symbol = coin::get_symbol(&coin_meta);
let buf = ascii::into_bytes(symbol);
vector::reverse(&mut buf);
ascii::string(buf)
};
let new_name = coin::get_name(&coin_meta);
string::append(&mut new_name, string::utf8(b"??? and profit"));
let treasury_cap = coin_native_10::take_treasury_cap(scenario);
coin::update_symbol(&treasury_cap, &mut coin_meta, new_symbol);
coin::update_name(&treasury_cap, &mut coin_meta, new_name);
// Check that the new serialization reflects updated metadata.
let expected_serialized =
asset_meta::serialize_test_only(
asset_meta::from_metadata_test_only(&coin_meta)
);
assert!(serialized != expected_serialized, 0);
let updated_serialized =
serialize_asset_meta_test_only(&mut token_bridge_state, &coin_meta);
assert!(updated_serialized == expected_serialized, 0);
// Clean up.
return_state(token_bridge_state);
coin_native_10::return_globals(treasury_cap, coin_meta);
// Done.
test_scenario::end(my_scenario);
}
#[test]
#[expected_failure(abort_code = attest_token::E_FROM_CREATE_WRAPPED)]
fun test_cannot_attest_token_from_create_wrapped() {
use token_bridge::attest_token::{attest_token};
let user = person();
let my_scenario = test_scenario::begin(user);
let scenario = &mut my_scenario;
// Publish coin.
coin_wrapped_7::init_test_only(test_scenario::ctx(scenario));
// Ignore effects.
test_scenario::next_tx(scenario, user);
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Ignore effects.
test_scenario::next_tx(scenario, user);
let token_bridge_state = take_state(scenario);
let coin_meta = test_scenario::take_shared(scenario);
// You shall not pass!
let prepared_msg =
attest_token<COIN_WRAPPED_7>(
&mut token_bridge_state,
&coin_meta,
1234 // nonce
);
// Clean up.
publish_message::destroy(prepared_msg);
abort 42
}
#[test]
#[expected_failure(abort_code = wormhole::package_utils::E_NOT_CURRENT_VERSION)]
fun test_cannot_attest_token_outdated_version() {
use token_bridge::attest_token::{attest_token};
let user = person();
let my_scenario = test_scenario::begin(user);
let scenario = &mut my_scenario;
// Publish coin.
coin_wrapped_7::init_test_only(test_scenario::ctx(scenario));
// Ignore effects.
test_scenario::next_tx(scenario, user);
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Ignore effects.
test_scenario::next_tx(scenario, user);
let token_bridge_state = take_state(scenario);
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!
let prepared_msg =
attest_token<COIN_WRAPPED_7>(
&mut token_bridge_state,
&coin_meta,
1234 // nonce
);
// Clean up.
publish_message::destroy(prepared_msg);
abort 42
}
}