wormhole/sui/token_bridge/sources/bridge_state.move

511 lines
20 KiB
Plaintext

module token_bridge::bridge_state {
use std::option::{Self, Option};
use std::ascii::{Self};
use sui::object::{Self, UID};
use sui::vec_map::{Self, VecMap};
use sui::tx_context::{TxContext};
use sui::coin::{Self, Coin, TreasuryCap, CoinMetadata};
use sui::transfer::{Self};
use sui::tx_context::{Self};
use sui::sui::SUI;
use token_bridge::string32;
use token_bridge::dynamic_set;
use token_bridge::asset_meta::{Self, AssetMeta};
use wormhole::external_address::{Self, ExternalAddress};
use wormhole::myu16::{U16};
use wormhole::wormhole::{Self};
use wormhole::state::{Self as wormhole_state, State as WormholeState};
use wormhole::emitter::{EmitterCapability};
use wormhole::set::{Self, Set};
const E_IS_NOT_WRAPPED_ASSET: u64 = 0;
const E_IS_NOT_REGISTERED_NATIVE_ASSET: u64 = 1;
const E_COIN_TYPE_HAS_NO_REGISTERED_INTEGER_ADDRESS: u64 = 2;
const E_COIN_TYPE_HAS_REGISTERED_INTEGER_ADDRESS: u64 = 3;
const E_ORIGIN_CHAIN_MISMATCH: u64 = 4;
const E_ORIGIN_ADDRESS_MISMATCH: u64 = 5;
const E_IS_WRAPPED_ASSET: u64 = 6;
friend token_bridge::vaa;
friend token_bridge::register_chain;
friend token_bridge::wrapped;
friend token_bridge::complete_transfer;
friend token_bridge::transfer_tokens;
friend token_bridge::attest_token;
#[test_only]
friend token_bridge::bridge_state_test;
#[test_only]
friend token_bridge::complete_transfer_test;
#[test_only]
friend token_bridge::token_bridge_vaa_test;
/// Capability for creating a bridge state object, granted to sender when this
/// module is deployed
struct DeployerCapability has key, store {id: UID}
/// WrappedAssetInfo<CoinType> stores all the metadata about a wrapped asset
struct WrappedAssetInfo<phantom CoinType> has key, store {
id: UID,
token_chain: U16,
token_address: ExternalAddress,
treasury_cap: TreasuryCap<CoinType>,
}
struct NativeAssetInfo<phantom CoinType> has key, store {
id: UID,
// Even though we can look up token_chain at any time from wormhole state,
// it can be more efficient to store it here locally so we don't have to do lookups.
custody: Coin<CoinType>,
asset_meta: AssetMeta,
}
/// OriginInfo is a non-Sui object that stores info about a tokens native token
/// chain and address
struct OriginInfo<phantom CoinType> has store, copy, drop {
token_chain: U16,
token_address: ExternalAddress,
}
public fun get_token_chain_from_origin_info<CoinType>(origin_info: &OriginInfo<CoinType>): U16 {
return origin_info.token_chain
}
public fun get_token_address_from_origin_info<CoinType>(origin_info: &OriginInfo<CoinType>): ExternalAddress {
return origin_info.token_address
}
public fun get_origin_info_from_wrapped_asset_info<CoinType>(wrapped_asset_info: &WrappedAssetInfo<CoinType>): OriginInfo<CoinType> {
OriginInfo { token_chain: wrapped_asset_info.token_chain, token_address: wrapped_asset_info.token_address }
}
public fun get_origin_info_from_native_asset_info<CoinType>(native_asset_info: &NativeAssetInfo<CoinType>): OriginInfo<CoinType> {
let asset_meta = &native_asset_info.asset_meta;
let token_chain = asset_meta::get_token_chain(asset_meta);
let token_address = asset_meta::get_token_address(asset_meta);
OriginInfo { token_chain, token_address }
}
public(friend) fun create_wrapped_asset_info<CoinType>(
token_chain: U16,
token_address: ExternalAddress,
treasury_cap: TreasuryCap<CoinType>,
ctx: &mut TxContext
): WrappedAssetInfo<CoinType> {
return WrappedAssetInfo {
id: object::new(ctx),
token_chain,
token_address,
treasury_cap
}
}
// Integer label for coin types registered with Wormhole
struct NativeIdRegistry has key, store {
id: UID,
index: u64, // next index to use
}
fun next_native_id(registry: &mut NativeIdRegistry): ExternalAddress {
use wormhole::serialize::serialize_u64;
let cur_index = registry.index;
registry.index = cur_index + 1;
let bytes = std::vector::empty<u8>();
serialize_u64(&mut bytes, cur_index);
external_address::from_bytes(bytes)
}
// Treasury caps, token stores, consumed VAAs, registered emitters, etc.
// are stored as dynamic fields of bridge state.
struct BridgeState has key, store {
id: UID,
/// Set of consumed VAA hashes
consumed_vaas: Set<vector<u8>>,
/// Token bridge owned emitter capability
emitter_cap: EmitterCapability,
/// Mapping of bridge contracts on other chains
registered_emitters: VecMap<U16, ExternalAddress>,
native_id_registry: NativeIdRegistry,
}
fun init(ctx: &mut TxContext) {
transfer::transfer(DeployerCapability{id: object::new(ctx)}, tx_context::sender(ctx));
}
#[test_only]
public fun test_init(ctx: &mut TxContext) {
transfer::transfer(DeployerCapability{id: object::new(ctx)}, tx_context::sender(ctx));
}
// converts owned state object into a shared object, so that anyone can get a reference to &mut State
// and pass it into various functions
public entry fun init_and_share_state(
deployer: DeployerCapability,
emitter_cap: EmitterCapability,
ctx: &mut TxContext
) {
let DeployerCapability{ id } = deployer;
object::delete(id);
let state = BridgeState {
id: object::new(ctx),
consumed_vaas: set::new(ctx),
emitter_cap,
registered_emitters: vec_map::empty(),
native_id_registry: NativeIdRegistry {
id: object::new(ctx),
index: 1,
}
};
// permanently shares state
transfer::share_object(state);
}
public(friend) fun deposit<CoinType>(
bridge_state: &mut BridgeState,
coin: Coin<CoinType>,
) {
// TODO: create custom errors for each dynamic_set::borrow_mut
let native_asset = dynamic_set::borrow_mut<NativeAssetInfo<CoinType>>(&mut bridge_state.id);
coin::join<CoinType>(&mut native_asset.custody, coin);
}
public(friend) fun withdraw<CoinType>(
_verified_coin_witness: VerifiedCoinType<CoinType>,
bridge_state: &mut BridgeState,
value: u64,
ctx: &mut TxContext
): Coin<CoinType> {
let native_asset = dynamic_set::borrow_mut<NativeAssetInfo<CoinType>>(&mut bridge_state.id);
coin::split<CoinType>(&mut native_asset.custody, value, ctx)
}
public(friend) fun mint<CoinType>(
_verified_coin_witness: VerifiedCoinType<CoinType>,
bridge_state: &mut BridgeState,
value: u64,
ctx: &mut TxContext,
): Coin<CoinType> {
let wrapped_info = dynamic_set::borrow_mut<WrappedAssetInfo<CoinType>>(&mut bridge_state.id);
coin::mint<CoinType>(&mut wrapped_info.treasury_cap, value, ctx)
}
public(friend) fun burn<CoinType>(
bridge_state: &mut BridgeState,
coin: Coin<CoinType>,
) {
let wrapped_info = dynamic_set::borrow_mut<WrappedAssetInfo<CoinType>>(&mut bridge_state.id);
coin::burn<CoinType>(&mut wrapped_info.treasury_cap, coin);
}
public(friend) fun publish_message(
wormhole_state: &mut WormholeState,
bridge_state: &mut BridgeState,
nonce: u64,
payload: vector<u8>,
message_fee: Coin<SUI>,
): u64 {
wormhole::publish_message(
&mut bridge_state.emitter_cap,
wormhole_state,
nonce,
payload,
message_fee,
)
}
/// getters
public fun vaa_is_consumed(state: &BridgeState, hash: vector<u8>): bool {
set::contains(&state.consumed_vaas, hash)
}
public fun get_registered_emitter(state: &BridgeState, chain_id: &U16): Option<ExternalAddress> {
if (vec_map::contains(&state.registered_emitters, chain_id)) {
option::some(*vec_map::get(&state.registered_emitters, chain_id))
} else {
option::none()
}
}
public fun is_wrapped_asset<CoinType>(bridge_state: &BridgeState): bool {
dynamic_set::exists_<WrappedAssetInfo<CoinType>>(&bridge_state.id)
}
public fun is_registered_native_asset<CoinType>(bridge_state: &BridgeState): bool {
dynamic_set::exists_<NativeAssetInfo<CoinType>>(&bridge_state.id)
}
/// Returns the origin information for a CoinType
public fun origin_info<CoinType>(bridge_state: &BridgeState): OriginInfo<CoinType> {
if (is_wrapped_asset<CoinType>(bridge_state)) {
get_wrapped_asset_origin_info<CoinType>(bridge_state)
} else {
get_registered_native_asset_origin_info(bridge_state)
}
}
/// A value of type `VerifiedCoinType<T>` witnesses the fact that the type
/// `T` has been verified to correspond to a particular chain id and token
/// address (may be either a wrapped or native asset).
/// The verification is performed by `verify_coin_type`.
///
/// This is important because the coin type is an input to several
/// functions, and is thus untrusted. Most coin-related functionality
/// requires passing in a coin type generic argument.
/// When transferring tokens *out*, that type instantiation determines the
/// token bridge's behaviour, and thus we just take whatever was supplied.
/// When transferring tokens *in*, it's the transfer VAA that determines
/// which coin should be used via the origin chain and origin address
/// fields.
///
/// For technical reasons, the latter case still requires a type argument to
/// be passed in (since Move does not support existential types, so we must
/// rely on old school universal quantification). We must thus verify that
/// the supplied type corresponds to the origin info in the VAA.
///
/// Accordingly, the `mint` and `withdraw` operations are gated by this
/// witness type, since these two operations require a VAA to supply the
/// token information. This ensures that those two functions can't be called
/// without first verifying the `CoinType`.
struct VerifiedCoinType<phantom CoinType> has copy, drop {}
/// See the documentation for `VerifiedCoinType` above.
public fun verify_coin_type<CoinType>(
bridge_state: &BridgeState,
token_chain: U16,
token_address: ExternalAddress
): VerifiedCoinType<CoinType> {
let coin_origin = origin_info<CoinType>(bridge_state);
assert!(coin_origin.token_chain == token_chain, E_ORIGIN_CHAIN_MISMATCH);
assert!(coin_origin.token_address == token_address, E_ORIGIN_ADDRESS_MISMATCH);
VerifiedCoinType {}
}
public fun get_wrapped_asset_origin_info<CoinType>(bridge_state: &BridgeState): OriginInfo<CoinType> {
assert!(is_wrapped_asset<CoinType>(bridge_state), E_IS_NOT_WRAPPED_ASSET);
let wrapped_asset_info = dynamic_set::borrow<WrappedAssetInfo<CoinType>>(&bridge_state.id);
get_origin_info_from_wrapped_asset_info(wrapped_asset_info)
}
public fun get_registered_native_asset_origin_info<CoinType>(bridge_state: &BridgeState): OriginInfo<CoinType> {
let native_asset_info = dynamic_set::borrow<NativeAssetInfo<CoinType>>(&bridge_state.id);
get_origin_info_from_native_asset_info(native_asset_info)
}
/// setters
public(friend) fun set_registered_emitter(state: &mut BridgeState, chain_id: U16, emitter: ExternalAddress) {
if (vec_map::contains<U16, ExternalAddress>(&mut state.registered_emitters, &chain_id)){
vec_map::remove<U16, ExternalAddress>(&mut state.registered_emitters, &chain_id);
};
vec_map::insert<U16, ExternalAddress>(&mut state.registered_emitters, chain_id, emitter);
}
/// dynamic ops
public(friend) fun store_consumed_vaa(bridge_state: &mut BridgeState, vaa: vector<u8>) {
set::add(&mut bridge_state.consumed_vaas, vaa);
}
public(friend) fun register_wrapped_asset<CoinType>(bridge_state: &mut BridgeState, wrapped_asset_info: WrappedAssetInfo<CoinType>) {
dynamic_set::add<WrappedAssetInfo<CoinType>>(&mut bridge_state.id, wrapped_asset_info);
}
public(friend) fun register_native_asset<CoinType>(
wormhole_state: &WormholeState,
bridge_state: &mut BridgeState,
coin_meta: &CoinMetadata<CoinType>,
ctx: &mut TxContext
): AssetMeta {
assert!(!is_wrapped_asset<CoinType>(bridge_state), E_IS_WRAPPED_ASSET); // TODO - test
let asset_meta = asset_meta::create(
next_native_id(&mut bridge_state.native_id_registry),
wormhole_state::get_chain_id(wormhole_state), // TODO: should we just hardcode this?
coin::get_decimals<CoinType>(coin_meta), // decimals
string32::from_bytes(ascii::into_bytes(coin::get_symbol<CoinType>(coin_meta))), // symbol
string32::from_string(&coin::get_name<CoinType>(coin_meta)) // name
);
let native_asset_info = NativeAssetInfo<CoinType> {
id: object::new(ctx),
custody: coin::zero(ctx),
asset_meta,
};
dynamic_set::add<NativeAssetInfo<CoinType>>(&mut bridge_state.id, native_asset_info);
asset_meta
}
}
#[test_only]
module token_bridge::bridge_state_test{
use sui::test_scenario::{Self, Scenario, next_tx, ctx, take_from_address, take_shared, return_shared};
use sui::coin::{CoinMetadata};
use wormhole::state::{State};
use wormhole::test_state::{init_wormhole_state};
use wormhole::wormhole::{Self};
use wormhole::external_address::{Self};
use token_bridge::bridge_state::{Self as state, BridgeState, DeployerCapability};
use token_bridge::native_coin_witness::{Self, NATIVE_COIN_WITNESS};
use token_bridge::native_coin_witness_v2::{Self, NATIVE_COIN_WITNESS_V2};
fun scenario(): Scenario { test_scenario::begin(@0x123233) }
fun people(): (address, address, address) { (@0x124323, @0xE05, @0xFACE) }
#[test]
fun test_state_setters() {
test_state_setters_(scenario())
}
#[test]
fun test_coin_type_addressing(){
test_coin_type_addressing_(scenario())
}
#[test]
#[expected_failure(abort_code = 0, location=0000000000000000000000000000000000000002::dynamic_field)]
fun test_coin_type_addressing_failure_case(){
test_coin_type_addressing_failure_case_(scenario())
}
public fun set_up_wormhole_core_and_token_bridges(admin: address, test: Scenario): Scenario {
// init and share wormhole core bridge
test = init_wormhole_state(test, admin);
// call init for token bridge to get deployer cap
next_tx(&mut test, admin); {
state::test_init(ctx(&mut test));
};
// register for emitter cap and init_and_share token bridge
next_tx(&mut test, admin); {
let wormhole_state = take_shared<State>(&test);
let my_emitter = wormhole::register_emitter(&mut wormhole_state, ctx(&mut test));
let deployer = take_from_address<DeployerCapability>(&test, admin);
state::init_and_share_state(deployer, my_emitter, ctx(&mut test));
return_shared<State>(wormhole_state);
};
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
return_shared<BridgeState>(bridge_state);
};
return test
}
fun test_state_setters_(test: Scenario) {
let (admin, _, _) = people();
test = set_up_wormhole_core_and_token_bridges(admin, test);
//test BridgeState setter and getter functions
next_tx(&mut test, admin); {
let state = take_shared<BridgeState>(&test);
// test store consumed vaa
state::store_consumed_vaa(&mut state, x"1234");
assert!(state::vaa_is_consumed(&state, x"1234"), 0);
// TODO - test store coin store
// TODO - test store treasury cap
return_shared<BridgeState>(state);
};
test_scenario::end(test);
}
fun test_coin_type_addressing_(test: Scenario) {
let (admin, _, _) = people();
test = set_up_wormhole_core_and_token_bridges(admin, test);
//test coin type addressing
next_tx(&mut test, admin); {
native_coin_witness::test_init(ctx(&mut test));
native_coin_witness_v2::test_init(ctx(&mut test));
};
next_tx(&mut test, admin); {
let wormhole_state = take_shared<State>(&test);
let bridge_state = take_shared<BridgeState>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
state::register_native_asset<NATIVE_COIN_WITNESS>(
&mut wormhole_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
let origin_info = state::origin_info<NATIVE_COIN_WITNESS>(&bridge_state);
let address = state::get_token_address_from_origin_info(&origin_info);
assert!(address == external_address::from_bytes(x"01"), 0);
let coin_meta_v2 = take_shared<CoinMetadata<NATIVE_COIN_WITNESS_V2>>(&test);
state::register_native_asset<NATIVE_COIN_WITNESS_V2>(
&mut wormhole_state,
&mut bridge_state,
&coin_meta_v2,
ctx(&mut test)
);
let origin_info = state::origin_info<NATIVE_COIN_WITNESS_V2>(&bridge_state);
let address = state::get_token_address_from_origin_info(&origin_info);
assert!(address == external_address::from_bytes(x"02"), 0);
return_shared<State>(wormhole_state);
return_shared<BridgeState>(bridge_state);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS_V2>>(coin_meta_v2);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
};
test_scenario::end(test);
}
fun test_coin_type_addressing_failure_case_(test: Scenario) {
let (admin, _, _) = people();
test = set_up_wormhole_core_and_token_bridges(admin, test);
//test coin type addressing
next_tx(&mut test, admin); {
native_coin_witness::test_init(ctx(&mut test));
native_coin_witness_v2::test_init(ctx(&mut test));
};
next_tx(&mut test, admin); {
let wormhole_state = take_shared<State>(&test);
let bridge_state = take_shared<BridgeState>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
state::register_native_asset<NATIVE_COIN_WITNESS>(
&mut wormhole_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
let origin_info = state::origin_info<NATIVE_COIN_WITNESS>(&bridge_state);
let address = state::get_token_address_from_origin_info(&origin_info);
assert!(address == external_address::from_bytes(x"01"), 0);
// aborts because trying to re-register native coin
state::register_native_asset<NATIVE_COIN_WITNESS>(
&mut wormhole_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
return_shared<State>(wormhole_state);
return_shared<BridgeState>(bridge_state);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
};
test_scenario::end(test);
}
}