wormhole/sui/token_bridge/sources/wrapped.move

133 lines
5.7 KiB
Plaintext

/// This module uses the one-time witness (OTW)
/// Sui one-time witness pattern reference: https://examples.sui.io/basics/one-time-witness.html
module token_bridge::wrapped {
use std::option::{Self};
use sui::tx_context::{TxContext};
use sui::coin::{TreasuryCap};
use sui::object::{Self, UID};
use sui::coin::{Self};
use sui::url::{Url};
use sui::transfer::{Self};
use token_bridge::bridge_state::{Self, BridgeState};
use token_bridge::asset_meta::{Self, AssetMeta};
use token_bridge::vaa;
use token_bridge::string32::{Self};
use wormhole::state::{Self as state, State as WormholeState};
use wormhole::myvaa as core_vaa;
const E_WRAPPING_NATIVE_COIN: u64 = 0;
const E_WRAPPING_REGISTERED_NATIVE_COIN: u64 = 1;
const E_WRAPPED_COIN_ALREADY_INITIALIZED: u64 = 2;
/// Wrapped assets are created in two steps.
/// 1) The coin is initialised by calling `create_wrapped_coin` in the
/// `init` function of a OTW module.
/// 2) The coin is registered in the token bridge in
/// `register_wrapped_coin`.
///
/// Since Step 1. takes places in an untrusted context, we want to remove
/// all degrees of freedom. To this end, `create_wrapped_coin` just takes a
/// VAA, and returns a `NewWrappedCoin` object. That's the only way to
/// create a `NewWrappedCoin` object. Then this object can be passed to
/// `register_wrapped_coin` in Step 2.
///
/// This setup ensures that we don't have to trust (or verify) that the OTW
/// initialiser did the right thing.
///
/// TODO: it would be nice if we could also enforce that the OTW struct's
/// name matches the token symbol being registered. Currently there's no way
/// to do this in the sui framework.
struct NewWrappedCoin<phantom CoinType> has key, store {
id: UID,
vaa_bytes: vector<u8>,
treasury_cap: TreasuryCap<CoinType>,
}
/// This function will be called from the `init` function of a module that
/// defines a OTW type. Due to the nature of `init` functions, this function
/// must be stateless.
/// This means that it performs no verification of the VAA beyond parsing
/// it. It is the responsbility of `register_wrapped_coin` to perform the
/// validation.
/// This function guarantees that if the VAA is valid, then a new currency
/// `CoinType` will be created such that:
/// 1) the asset metadata matches the VAA
/// 2) the treasury total supply will be 0
///
/// Thanks to the above properties, `register_wrapped_coin` does not need to
/// do any checks other than the VAA in `NewWrappedCoin` is valid.
public fun create_wrapped_coin<CoinType: drop>(
vaa_bytes: vector<u8>,
coin_witness: CoinType,
ctx: &mut TxContext
): NewWrappedCoin<CoinType> {
let payload = core_vaa::parse_and_get_payload(vaa_bytes);
let asset_meta: AssetMeta = asset_meta::parse(payload);
// The amounts in the token bridge payload are truncated to 8 decimals
// in each of the contracts when sending tokens out, so there's no
// precision beyond 10^-8. We could preserve the original number of
// decimals when creating wrapped assets, and "untruncate" the amounts
// on the way out by scaling back appropriately. This is what most other
// chains do, but untruncating from 8 decimals to 18 decimals loses
// log2(10^10) ~ 33 bits of precision, which we cannot afford on Aptos
// (and Solana), as the coin type only has 64bits to begin with.
// Contrast with Ethereum, where amounts are 256 bits.
// So we cap the maximum decimals at 8 when creating a wrapped token.
let max_decimals: u8 = 8;
let parsed_decimals = asset_meta::get_decimals(&asset_meta);
let symbol = asset_meta::get_symbol(&asset_meta);
let name = asset_meta::get_name(&asset_meta);
let decimals = if (max_decimals < parsed_decimals) max_decimals else parsed_decimals;
let (treasury_cap, coin_metadata) = coin::create_currency<CoinType>(
coin_witness,
decimals,
string32::to_bytes(&symbol),
string32::to_bytes(&name),
x"", //empty description
option::none<Url>(), //empty url
ctx
);
transfer::share_object(coin_metadata);
NewWrappedCoin { id: object::new(ctx), vaa_bytes, treasury_cap }
}
public entry fun register_wrapped_coin<CoinType>(
state: &mut WormholeState,
bridge_state: &mut BridgeState,
new_wrapped_coin: NewWrappedCoin<CoinType>,
ctx: &mut TxContext,
) {
let NewWrappedCoin { id, vaa_bytes, treasury_cap } = new_wrapped_coin;
object::delete(id);
let vaa = vaa::parse_verify_and_replay_protect(
state,
bridge_state,
vaa_bytes,
ctx
);
let payload = core_vaa::destroy(vaa);
let metadata = asset_meta::parse(payload);
let origin_chain = asset_meta::get_token_chain(&metadata);
let external_address = asset_meta::get_token_address(&metadata);
let wrapped_asset_info =
bridge_state::create_wrapped_asset_info(
origin_chain,
external_address,
treasury_cap,
ctx
);
assert!(origin_chain != state::get_chain_id(state), E_WRAPPING_NATIVE_COIN);
assert!(!bridge_state::is_registered_native_asset<CoinType>(bridge_state), E_WRAPPING_REGISTERED_NATIVE_COIN);
assert!(!bridge_state::is_wrapped_asset<CoinType>(bridge_state), E_WRAPPED_COIN_ALREADY_INITIALIZED);
bridge_state::register_wrapped_asset<CoinType>(bridge_state, wrapped_asset_info);
}
}