wormhole/sui/token_bridge/sources/resources/wrapped_asset.move

807 lines
26 KiB
Plaintext

// SPDX-License-Identifier: Apache 2
/// This module implements two custom types relating to Token Bridge wrapped
/// assets. These assets have been attested from foreign networks, whose
/// metadata is stored in `ForeignInfo`. The Token Bridge contract is the
/// only authority that can mint and burn these assets via `Supply`.
///
/// See `create_wrapped` and 'token_registry' modules for more details.
module token_bridge::wrapped_asset {
use std::string::{String};
use sui::balance::{Self, Balance};
use sui::coin::{Self, TreasuryCap, CoinMetadata};
use sui::package::{Self, UpgradeCap};
use wormhole::external_address::{ExternalAddress};
use wormhole::state::{chain_id};
use token_bridge::string_utils;
use token_bridge::asset_meta::{Self, AssetMeta};
use token_bridge::normalized_amount::{cap_decimals};
friend token_bridge::complete_transfer;
friend token_bridge::create_wrapped;
friend token_bridge::token_registry;
friend token_bridge::transfer_tokens;
/// Token chain ID matching Sui's are not allowed.
const E_SUI_CHAIN: u64 = 0;
/// Canonical token info does match `AssetMeta` payload.
const E_ASSET_META_MISMATCH: u64 = 1;
/// Coin decimals don't match the VAA.
const E_DECIMALS_MISMATCH: u64 = 2;
/// Container storing foreign asset info.
struct ForeignInfo<phantom C> has store {
token_chain: u16,
token_address: ExternalAddress,
native_decimals: u8,
symbol: String
}
/// Container managing `ForeignInfo` and `TreasuryCap` for a wrapped asset
/// coin type.
struct WrappedAsset<phantom C> has store {
info: ForeignInfo<C>,
treasury_cap: TreasuryCap<C>,
decimals: u8,
upgrade_cap: UpgradeCap
}
/// Create new `WrappedAsset`.
///
/// See `token_registry` module for more info.
public(friend) fun new<C>(
token_meta: AssetMeta,
coin_meta: &mut CoinMetadata<C>,
treasury_cap: TreasuryCap<C>,
upgrade_cap: UpgradeCap
): WrappedAsset<C> {
// Verify that the upgrade cap is from the same package as coin type.
// This cap should not have been modified prior to creating this asset
// (i.e. should have the default upgrade policy and build version == 1).
wormhole::package_utils::assert_package_upgrade_cap<C>(
&upgrade_cap,
package::compatible_policy(),
1
);
let (
token_address,
token_chain,
native_decimals,
symbol,
name
) = asset_meta::unpack(token_meta);
// Protect against adding `AssetMeta` which has Sui's chain ID.
assert!(token_chain != chain_id(), E_SUI_CHAIN);
// Set metadata.
coin::update_name(&mut treasury_cap, coin_meta, name);
coin::update_symbol(&mut treasury_cap, coin_meta, string_utils::to_ascii(&symbol));
let decimals = cap_decimals(native_decimals);
// Ensure that the `C` type has the right number of decimals. This is
// the only field in the coinmeta that cannot be changed after the fact,
// so we expect to receive one that already has the correct decimals
// set.
assert!(decimals == coin::get_decimals(coin_meta), E_DECIMALS_MISMATCH);
let info =
ForeignInfo {
token_address,
token_chain,
native_decimals,
symbol
};
WrappedAsset {
info,
treasury_cap,
decimals,
upgrade_cap
}
}
#[test_only]
public fun new_test_only<C>(
token_meta: AssetMeta,
coin_meta: &mut CoinMetadata<C>,
treasury_cap: TreasuryCap<C>,
upgrade_cap: UpgradeCap
): WrappedAsset<C> {
new(token_meta, coin_meta, treasury_cap, upgrade_cap)
}
/// Update existing `ForeignInfo` using new `AssetMeta`.
///
/// See `token_registry` module for more info.
public(friend) fun update_metadata<C>(
self: &mut WrappedAsset<C>,
coin_meta: &mut CoinMetadata<C>,
token_meta: AssetMeta
) {
// NOTE: We ignore `native_decimals` because we do not enforce that
// an asset's decimals on a foreign network needs to stay the same.
let (
token_address,
token_chain,
_native_decimals,
symbol,
name
) = asset_meta::unpack(token_meta);
// Verify canonical token info. Also check that the native decimals
// have not changed (because changing this info is not desirable, as
// this change means the supply changed on its native network).
//
// NOTE: This implicitly verifies that `token_chain` is not Sui's
// because this was checked already when the asset was first added.
let (expected_chain, expected_address) = canonical_info<C>(self);
assert!(
(
token_chain == expected_chain &&
token_address == expected_address
),
E_ASSET_META_MISMATCH
);
// Finally only update the name and symbol.
self.info.symbol = symbol;
coin::update_name(&mut self.treasury_cap, coin_meta, name);
coin::update_symbol(&mut self.treasury_cap, coin_meta, string_utils::to_ascii(&symbol));
}
#[test_only]
public fun update_metadata_test_only<C>(
self: &mut WrappedAsset<C>,
coin_meta: &mut CoinMetadata<C>,
token_meta: AssetMeta
) {
update_metadata(self, coin_meta, token_meta)
}
/// Retrieve immutable reference to `ForeignInfo`.
public fun info<C>(self: &WrappedAsset<C>): &ForeignInfo<C> {
&self.info
}
/// Retrieve canonical token chain ID from `ForeignInfo`.
public fun token_chain<C>(info: &ForeignInfo<C>): u16 {
info.token_chain
}
/// Retrieve canonical token address from `ForeignInfo`.
public fun token_address<C>(info: &ForeignInfo<C>): ExternalAddress {
info.token_address
}
/// Retrieve decimal amount from `ForeignInfo`.
///
/// NOTE: This is for informational purposes. This decimal amount is not
/// used for any calculations.
public fun native_decimals<C>(info: &ForeignInfo<C>): u8 {
info.native_decimals
}
/// Retrieve asset's symbol (UTF-8) from `ForeignMetadata`.
///
/// NOTE: This value can be updated.
public fun symbol<C>(info: &ForeignInfo<C>): String {
info.symbol
}
/// Retrieve total minted supply.
public fun total_supply<C>(self: &WrappedAsset<C>): u64 {
coin::total_supply(&self.treasury_cap)
}
/// Retrieve decimals for this wrapped asset. For any asset whose native
/// decimals is greater than the cap (8), this will be 8.
///
/// See `normalized_amount` module for more info.
public fun decimals<C>(self: &WrappedAsset<C>): u8 {
self.decimals
}
/// Retrieve canonical token chain ID and token address.
public fun canonical_info<C>(
self: &WrappedAsset<C>
): (u16, ExternalAddress) {
(self.info.token_chain, self.info.token_address)
}
/// Burn a given `Balance`. `Balance` originates from an outbound token
/// transfer for a wrapped asset.
///
/// See `transfer_tokens` module for more info.
public(friend) fun burn<C>(
self: &mut WrappedAsset<C>,
burned: Balance<C>
): u64 {
balance::decrease_supply(coin::supply_mut(&mut self.treasury_cap), burned)
}
#[test_only]
public fun burn_test_only<C>(
self: &mut WrappedAsset<C>,
burned: Balance<C>
): u64 {
burn(self, burned)
}
/// Mint a given amount. This amount is determined by an inbound token
/// transfer payload for a wrapped asset.
///
/// See `complete_transfer` module for more info.
public(friend) fun mint<C>(
self: &mut WrappedAsset<C>,
amount: u64
): Balance<C> {
coin::mint_balance(&mut self.treasury_cap, amount)
}
#[test_only]
public fun mint_test_only<C>(
self: &mut WrappedAsset<C>,
amount: u64
): Balance<C> {
mint(self, amount)
}
#[test_only]
public fun destroy<C>(asset: WrappedAsset<C>) {
let WrappedAsset {
info,
treasury_cap,
decimals: _,
upgrade_cap
} = asset;
sui::test_utils::destroy(treasury_cap);
let ForeignInfo {
token_chain: _,
token_address: _,
native_decimals: _,
symbol: _
} = info;
sui::package::make_immutable(upgrade_cap);
}
}
#[test_only]
module token_bridge::wrapped_asset_tests {
use std::string::{Self};
use sui::balance::{Self};
use sui::coin::{Self, CoinMetadata};
use sui::object::{Self};
use sui::package::{Self};
use sui::test_scenario::{Self};
use wormhole::external_address::{Self};
use wormhole::state::{chain_id};
use token_bridge::asset_meta::{Self};
use token_bridge::string_utils;
use token_bridge::coin_native_10::{COIN_NATIVE_10, Self};
use token_bridge::coin_wrapped_12::{COIN_WRAPPED_12, Self};
use token_bridge::coin_wrapped_7::{COIN_WRAPPED_7, Self};
use token_bridge::token_bridge_scenario::{person};
use token_bridge::wrapped_asset::{Self};
#[test]
fun test_wrapped_asset_7() {
let caller = person();
let my_scenario = test_scenario::begin(caller);
let scenario = &mut my_scenario;
let parsed_meta = coin_wrapped_7::token_meta();
let expected_token_chain = asset_meta::token_chain(&parsed_meta);
let expected_token_address = asset_meta::token_address(&parsed_meta);
let expected_native_decimals =
asset_meta::native_decimals(&parsed_meta);
let expected_symbol = asset_meta::symbol(&parsed_meta);
let expected_name = asset_meta::name(&parsed_meta);
// Publish coin.
let treasury_cap =
coin_wrapped_7::init_and_take_treasury_cap(
scenario,
caller
);
// Ignore effects.
test_scenario::next_tx(scenario, caller);
// Upgrade cap belonging to coin type.
let upgrade_cap =
package::test_publish(
object::id_from_address(@token_bridge),
test_scenario::ctx(scenario)
);
let coin_meta: CoinMetadata<COIN_WRAPPED_7> = test_scenario::take_shared(scenario);
// Make new.
let asset =
wrapped_asset::new_test_only(
parsed_meta,
&mut coin_meta,
treasury_cap,
upgrade_cap
);
// Verify members.
let info = wrapped_asset::info(&asset);
assert!(
wrapped_asset::token_chain(info) == expected_token_chain,
0
);
assert!(
wrapped_asset::token_address(info) == expected_token_address,
0
);
assert!(
wrapped_asset::native_decimals(info) == expected_native_decimals,
0
);
assert!(coin::get_symbol(&coin_meta) == string_utils::to_ascii(&expected_symbol), 0);
assert!(coin::get_name(&coin_meta) == expected_name, 0);
assert!(wrapped_asset::total_supply(&asset) == 0, 0);
let (token_chain, token_address) =
wrapped_asset::canonical_info(&asset);
assert!(token_chain == expected_token_chain, 0);
assert!(token_address == expected_token_address, 0);
// Decimals are read from `CoinMetadata`, but in this case will agree
// with the value encoded in the VAA.
assert!(wrapped_asset::decimals(&asset) == expected_native_decimals, 0);
assert!(coin::get_decimals(&coin_meta) == expected_native_decimals, 0);
// Change name and symbol for update.
let new_symbol = std::ascii::into_bytes(coin::get_symbol(&coin_meta));
std::vector::append(&mut new_symbol, b"??? and profit");
assert!(new_symbol != *string::bytes(&expected_symbol), 0);
let new_name = coin::get_name(&coin_meta);
string::append(&mut new_name, string::utf8(b"??? and profit"));
assert!(new_name != expected_name, 0);
let updated_meta =
asset_meta::new(
expected_token_address,
expected_token_chain,
expected_native_decimals,
string::utf8(new_symbol),
new_name
);
// Update metadata now.
wrapped_asset::update_metadata_test_only(&mut asset, &mut coin_meta, updated_meta);
assert!(coin::get_symbol(&coin_meta) == std::ascii::string(new_symbol), 0);
assert!(coin::get_name(&coin_meta) == new_name, 0);
// Try to mint.
let mint_amount = 420;
let collected = balance::zero();
let (i, n) = (0, 8);
while (i < n) {
let minted =
wrapped_asset::mint_test_only(&mut asset, mint_amount);
assert!(balance::value(&minted) == mint_amount, 0);
balance::join(&mut collected, minted);
i = i + 1;
};
assert!(balance::value(&collected) == n * mint_amount, 0);
assert!(
wrapped_asset::total_supply(&asset) == balance::value(&collected),
0
);
// Now try to burn.
let burn_amount = 69;
let i = 0;
while (i < n) {
let burned = balance::split(&mut collected, burn_amount);
let check_amount =
wrapped_asset::burn_test_only(&mut asset, burned);
assert!(check_amount == burn_amount, 0);
i = i + 1;
};
let remaining = n * mint_amount - n * burn_amount;
assert!(wrapped_asset::total_supply(&asset) == remaining, 0);
assert!(balance::value(&collected) == remaining, 0);
test_scenario::return_shared(coin_meta);
// Clean up.
balance::destroy_for_testing(collected);
wrapped_asset::destroy(asset);
// Done.
test_scenario::end(my_scenario);
}
#[test]
fun test_wrapped_asset_12() {
let caller = person();
let my_scenario = test_scenario::begin(caller);
let scenario = &mut my_scenario;
let parsed_meta = coin_wrapped_12::token_meta();
let expected_token_chain = asset_meta::token_chain(&parsed_meta);
let expected_token_address = asset_meta::token_address(&parsed_meta);
let expected_native_decimals =
asset_meta::native_decimals(&parsed_meta);
let expected_symbol = asset_meta::symbol(&parsed_meta);
let expected_name = asset_meta::name(&parsed_meta);
// Publish coin.
let treasury_cap =
coin_wrapped_12::init_and_take_treasury_cap(
scenario,
caller
);
// Ignore effects.
test_scenario::next_tx(scenario, caller);
// Upgrade cap belonging to coin type.
let upgrade_cap =
package::test_publish(
object::id_from_address(@token_bridge),
test_scenario::ctx(scenario)
);
let coin_meta: CoinMetadata<COIN_WRAPPED_12> = test_scenario::take_shared(scenario);
// Make new.
let asset =
wrapped_asset::new_test_only(
parsed_meta,
&mut coin_meta,
treasury_cap,
upgrade_cap
);
// Verify members.
let info = wrapped_asset::info(&asset);
assert!(
wrapped_asset::token_chain(info) == expected_token_chain,
0
);
assert!(
wrapped_asset::token_address(info) == expected_token_address,
0
);
assert!(
wrapped_asset::native_decimals(info) == expected_native_decimals,
0
);
assert!(coin::get_symbol(&coin_meta) == string_utils::to_ascii(&expected_symbol), 0);
assert!(coin::get_name(&coin_meta) == expected_name, 0);
assert!(wrapped_asset::total_supply(&asset) == 0, 0);
let (token_chain, token_address) =
wrapped_asset::canonical_info(&asset);
assert!(token_chain == expected_token_chain, 0);
assert!(token_address == expected_token_address, 0);
// Decimals are read from `CoinMetadata`, but in this case will not
// agree with the value encoded in the VAA.
assert!(wrapped_asset::decimals(&asset) == 8, 0);
assert!(
coin::get_decimals(&coin_meta) == wrapped_asset::decimals(&asset),
0
);
assert!(wrapped_asset::decimals(&asset) != expected_native_decimals, 0);
// Change name and symbol for update.
let new_symbol = std::ascii::into_bytes(coin::get_symbol(&coin_meta));
std::vector::append(&mut new_symbol, b"??? and profit");
assert!(new_symbol != *string::bytes(&expected_symbol), 0);
let new_name = coin::get_name(&coin_meta);
string::append(&mut new_name, string::utf8(b"??? and profit"));
assert!(new_name != expected_name, 0);
let updated_meta =
asset_meta::new(
expected_token_address,
expected_token_chain,
expected_native_decimals,
string::utf8(new_symbol),
new_name
);
// Update metadata now.
wrapped_asset::update_metadata_test_only(&mut asset, &mut coin_meta, updated_meta);
assert!(coin::get_symbol(&coin_meta) == std::ascii::string(new_symbol), 0);
assert!(coin::get_name(&coin_meta) == new_name, 0);
// Try to mint.
let mint_amount = 420;
let collected = balance::zero();
let (i, n) = (0, 8);
while (i < n) {
let minted =
wrapped_asset::mint_test_only(&mut asset, mint_amount);
assert!(balance::value(&minted) == mint_amount, 0);
balance::join(&mut collected, minted);
i = i + 1;
};
assert!(balance::value(&collected) == n * mint_amount, 0);
assert!(
wrapped_asset::total_supply(&asset) == balance::value(&collected),
0
);
// Now try to burn.
let burn_amount = 69;
let i = 0;
while (i < n) {
let burned = balance::split(&mut collected, burn_amount);
let check_amount =
wrapped_asset::burn_test_only(&mut asset, burned);
assert!(check_amount == burn_amount, 0);
i = i + 1;
};
let remaining = n * mint_amount - n * burn_amount;
assert!(wrapped_asset::total_supply(&asset) == remaining, 0);
assert!(balance::value(&collected) == remaining, 0);
// Clean up.
balance::destroy_for_testing(collected);
wrapped_asset::destroy(asset);
test_scenario::return_shared(coin_meta);
// Done.
test_scenario::end(my_scenario);
}
#[test]
#[expected_failure(abort_code = wrapped_asset::E_SUI_CHAIN)]
// In this negative test case, we attempt to register a native coin as a
// wrapped coin.
fun test_cannot_new_sui_chain() {
let caller = person();
let my_scenario = test_scenario::begin(caller);
let scenario = &mut my_scenario;
// Initialize new coin type.
coin_native_10::init_test_only(test_scenario::ctx(scenario));
// Ignore effects.
test_scenario::next_tx(scenario, caller);
// Sui's chain ID is not allowed.
let invalid_meta =
asset_meta::new(
external_address::default(),
chain_id(),
10,
string::utf8(b""),
string::utf8(b"")
);
// Upgrade cap belonging to coin type.
let upgrade_cap =
package::test_publish(
object::id_from_address(@token_bridge),
test_scenario::ctx(scenario)
);
let treasury_cap = test_scenario::take_shared<coin::TreasuryCap<COIN_NATIVE_10>>(scenario);
let coin_meta = test_scenario::take_shared<CoinMetadata<COIN_NATIVE_10>>(scenario);
// You shall not pass!
let asset =
wrapped_asset::new_test_only(
invalid_meta,
&mut coin_meta,
treasury_cap,
upgrade_cap
);
// Clean up.
wrapped_asset::destroy(asset);
abort 42
}
#[test]
#[expected_failure(abort_code = wrapped_asset::E_ASSET_META_MISMATCH)]
/// In this negative test case, we attempt to update with a mismatching
/// chain.
fun test_cannot_update_metadata_asset_meta_mismatch_token_address() {
let caller = person();
let my_scenario = test_scenario::begin(caller);
let scenario = &mut my_scenario;
let parsed_meta = coin_wrapped_12::token_meta();
let expected_token_chain = asset_meta::token_chain(&parsed_meta);
let expected_token_address = asset_meta::token_address(&parsed_meta);
let expected_native_decimals =
asset_meta::native_decimals(&parsed_meta);
// Publish coin.
let treasury_cap =
coin_wrapped_12::init_and_take_treasury_cap(
scenario,
caller
);
// Ignore effects.
test_scenario::next_tx(scenario, caller);
// Upgrade cap belonging to coin type.
let upgrade_cap =
package::test_publish(
object::id_from_address(@token_bridge),
test_scenario::ctx(scenario)
);
let coin_meta = test_scenario::take_shared(scenario);
// Make new.
let asset =
wrapped_asset::new_test_only(
parsed_meta,
&mut coin_meta,
treasury_cap,
upgrade_cap
);
let invalid_meta =
asset_meta::new(
external_address::default(),
expected_token_chain,
expected_native_decimals,
string::utf8(b""),
string::utf8(b""),
);
assert!(
asset_meta::token_address(&invalid_meta) != expected_token_address,
0
);
assert!(
asset_meta::token_chain(&invalid_meta) == expected_token_chain,
0
);
assert!(
asset_meta::native_decimals(&invalid_meta) == expected_native_decimals,
0
);
// You shall not pass!
wrapped_asset::update_metadata_test_only(&mut asset, &mut coin_meta, invalid_meta);
// Clean up.
wrapped_asset::destroy(asset);
abort 42
}
#[test]
#[expected_failure(abort_code = wrapped_asset::E_ASSET_META_MISMATCH)]
/// In this negative test case, we attempt to update with a mismatching
/// chain.
fun test_cannot_update_metadata_asset_meta_mismatch_token_chain() {
let caller = person();
let my_scenario = test_scenario::begin(caller);
let scenario = &mut my_scenario;
let parsed_meta = coin_wrapped_12::token_meta();
let expected_token_chain = asset_meta::token_chain(&parsed_meta);
let expected_token_address = asset_meta::token_address(&parsed_meta);
let expected_native_decimals =
asset_meta::native_decimals(&parsed_meta);
// Publish coin.
let treasury_cap =
coin_wrapped_12::init_and_take_treasury_cap(
scenario,
caller
);
// Ignore effects.
test_scenario::next_tx(scenario, caller);
// Upgrade cap belonging to coin type.
let upgrade_cap =
package::test_publish(
object::id_from_address(@token_bridge),
test_scenario::ctx(scenario)
);
let coin_meta = test_scenario::take_shared(scenario);
// Make new.
let asset =
wrapped_asset::new_test_only(
parsed_meta,
&mut coin_meta,
treasury_cap,
upgrade_cap
);
let invalid_meta =
asset_meta::new(
expected_token_address,
chain_id(),
expected_native_decimals,
string::utf8(b""),
string::utf8(b""),
);
assert!(
asset_meta::token_address(&invalid_meta) == expected_token_address,
0
);
assert!(
asset_meta::token_chain(&invalid_meta) != expected_token_chain,
0
);
assert!(
asset_meta::native_decimals(&invalid_meta) == expected_native_decimals,
0
);
// You shall not pass!
wrapped_asset::update_metadata_test_only(&mut asset, &mut coin_meta, invalid_meta);
// Clean up.
wrapped_asset::destroy(asset);
abort 42
}
#[test]
#[expected_failure(
abort_code = wormhole::package_utils::E_INVALID_UPGRADE_CAP
)]
fun test_cannot_new_upgrade_cap_mismatch() {
let caller = person();
let my_scenario = test_scenario::begin(caller);
let scenario = &mut my_scenario;
// Publish coin.
let treasury_cap =
coin_wrapped_12::init_and_take_treasury_cap(
scenario,
caller
);
// Ignore effects.
test_scenario::next_tx(scenario, caller);
// Upgrade cap belonging to coin type.
let upgrade_cap =
package::test_publish(
object::id_from_address(@0xbadc0de),
test_scenario::ctx(scenario)
);
let coin_meta = test_scenario::take_shared(scenario);
// You shall not pass!
let asset =
wrapped_asset::new_test_only(
coin_wrapped_12::token_meta(),
&mut coin_meta,
treasury_cap,
upgrade_cap
);
// Clean up.
wrapped_asset::destroy(asset);
abort 42
}
}