aptos/contracts: implement token bridge
This commit is contained in:
parent
3eb8daf492
commit
be0f58a537
|
@ -0,0 +1,14 @@
|
|||
-include ../../Makefile.help
|
||||
|
||||
.PHONY: artifacts
|
||||
artifacts: build
|
||||
|
||||
.PHONY: build
|
||||
## Build contract
|
||||
build:
|
||||
aptos move compile --save-metadata
|
||||
|
||||
.PHONY: test
|
||||
## Run tests
|
||||
test:
|
||||
aptos move test
|
|
@ -0,0 +1,27 @@
|
|||
[package]
|
||||
name = "TokenBridge"
|
||||
version = "0.0.1"
|
||||
|
||||
#TODO: pin versions before mainnet release
|
||||
[dependencies]
|
||||
AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework/", rev = "main" }
|
||||
MoveStdlib = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/move-stdlib/", rev = "main" }
|
||||
AptosStdlib = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-stdlib/", rev = "main" }
|
||||
AptosToken = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-token/", rev = "main" }
|
||||
Wormhole = { local = "../wormhole/" }
|
||||
Deployer = { local = "../deployer/" }
|
||||
|
||||
# only included in testing
|
||||
[dev-dependencies]
|
||||
WrappedCoin = { local = "../coin/" }
|
||||
|
||||
[dev-addresses]
|
||||
# derived address from sha3_256(deployer | "token_bridge" | 0xFF) by running
|
||||
# worm aptos derive-resource-account 0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b token_bridge
|
||||
token_bridge = "0x84a5f374d29fc77e370014dce4fd6a55b58ad608de8074b0be5571701724da31"
|
||||
deployer = "0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b"
|
||||
wrapped_coin = "0xf4f53cc591e5190eddbc43940746e2b5deea6e0e1562b2bba765d488504842c7"
|
||||
wormhole = "0xde0036a9600559e295d5f6802ef6f3f802f510366e0c23912b0655d972166017"
|
||||
|
||||
[addresses]
|
||||
token_bridge = "_"
|
|
@ -0,0 +1,143 @@
|
|||
module token_bridge::attest_token {
|
||||
use aptos_framework::aptos_coin::{AptosCoin};
|
||||
use aptos_framework::coin::{Self, Coin};
|
||||
|
||||
use token_bridge::asset_meta::{Self, AssetMeta};
|
||||
use token_bridge::state;
|
||||
use token_bridge::token_hash;
|
||||
use token_bridge::string32;
|
||||
|
||||
const E_COIN_IS_NOT_INITIALIZED: u64 = 0;
|
||||
/// Wrapped assets can't be attested
|
||||
const E_WRAPPED_ASSET: u64 = 1;
|
||||
|
||||
public entry fun attest_token_with_signer<CoinType>(user: &signer): u64 {
|
||||
let message_fee = wormhole::state::get_message_fee();
|
||||
let fee_coins = coin::withdraw<AptosCoin>(user, message_fee);
|
||||
attest_token<CoinType>(fee_coins)
|
||||
}
|
||||
|
||||
public fun attest_token<CoinType>(fee_coins: Coin<AptosCoin>): u64 {
|
||||
let asset_meta: AssetMeta = attest_token_internal<CoinType>();
|
||||
let payload: vector<u8> = asset_meta::encode(asset_meta);
|
||||
let nonce = 0;
|
||||
state::publish_message(
|
||||
nonce,
|
||||
payload,
|
||||
fee_coins
|
||||
)
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun attest_token_test<CoinType>(): AssetMeta {
|
||||
attest_token_internal<CoinType>()
|
||||
}
|
||||
|
||||
fun attest_token_internal<CoinType>(): AssetMeta {
|
||||
// wrapped assets and uninitialised type can't be attested.
|
||||
assert!(!state::is_wrapped_asset<CoinType>(), E_WRAPPED_ASSET);
|
||||
assert!(coin::is_coin_initialized<CoinType>(), E_COIN_IS_NOT_INITIALIZED); // not tested
|
||||
|
||||
let token_address = token_hash::derive<CoinType>();
|
||||
if (!state::is_registered_native_asset<CoinType>()) {
|
||||
// if native asset is not registered, register it in the reverse look-up map
|
||||
state::set_native_asset_type_info<CoinType>();
|
||||
};
|
||||
let token_chain = wormhole::state::get_chain_id();
|
||||
let decimals = coin::decimals<CoinType>();
|
||||
let symbol = string32::from_string(&coin::symbol<CoinType>());
|
||||
let name = string32::from_string(&coin::name<CoinType>());
|
||||
asset_meta::create(
|
||||
token_hash::get_external_address(&token_address),
|
||||
token_chain,
|
||||
decimals,
|
||||
symbol,
|
||||
name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::attest_token_test {
|
||||
use aptos_framework::coin;
|
||||
use aptos_framework::string::utf8;
|
||||
use aptos_framework::type_info::type_of;
|
||||
|
||||
use token_bridge::token_bridge::{Self as bridge};
|
||||
use token_bridge::state;
|
||||
use token_bridge::attest_token;
|
||||
use token_bridge::token_hash;
|
||||
use token_bridge::asset_meta;
|
||||
use token_bridge::string32;
|
||||
use token_bridge::wrapped_test;
|
||||
|
||||
struct MyCoin has key {}
|
||||
|
||||
fun setup(
|
||||
token_bridge: &signer,
|
||||
deployer: &signer,
|
||||
) {
|
||||
// we initialise the bridge with zero fees to avoid having to mint fee
|
||||
// tokens in these tests. The wormolhe fee handling is already tested
|
||||
// in wormhole.move, so it's unnecessary here.
|
||||
wormhole::wormhole_test::setup(0);
|
||||
bridge::init_test(deployer);
|
||||
|
||||
init_my_token(token_bridge);
|
||||
}
|
||||
|
||||
fun init_my_token(admin: &signer) {
|
||||
let name = utf8(b"Some test coin");
|
||||
let symbol = utf8(b"TEST");
|
||||
let decimals = 10;
|
||||
let monitor_supply = true;
|
||||
let (burn_cap, freeze_cap, mint_cap) = coin::initialize<MyCoin>(admin, name, symbol, decimals, monitor_supply);
|
||||
coin::destroy_burn_cap(burn_cap);
|
||||
coin::destroy_freeze_cap(freeze_cap);
|
||||
coin::destroy_mint_cap(mint_cap);
|
||||
}
|
||||
|
||||
#[test(token_bridge=@token_bridge, deployer=@deployer)]
|
||||
fun test_attest_token(token_bridge: &signer, deployer: &signer) {
|
||||
use std::string;
|
||||
|
||||
setup(token_bridge, deployer);
|
||||
let asset_meta = attest_token::attest_token_test<MyCoin>();
|
||||
|
||||
let token_address = asset_meta::get_token_address(&asset_meta);
|
||||
let token_chain = asset_meta::get_token_chain(&asset_meta);
|
||||
let decimals = asset_meta::get_decimals(&asset_meta);
|
||||
let symbol = string32::to_string(&asset_meta::get_symbol(&asset_meta));
|
||||
let name = string32::to_string(&asset_meta::get_name(&asset_meta));
|
||||
|
||||
assert!(token_address == token_hash::get_external_address(&token_hash::derive<MyCoin>()), 0);
|
||||
assert!(token_chain == wormhole::u16::from_u64(22), 0);
|
||||
assert!(decimals == 10, 0);
|
||||
assert!(name == string::utf8(b"Some test coin"), 0);
|
||||
assert!(symbol == string::utf8(b"TEST"), 0);
|
||||
}
|
||||
|
||||
#[test(token_bridge=@token_bridge, deployer=@deployer)]
|
||||
#[expected_failure(abort_code = 1)]
|
||||
fun test_attest_wrapped_token(token_bridge: &signer, deployer: &signer) {
|
||||
setup(token_bridge, deployer);
|
||||
wrapped_test::init_wrapped_token();
|
||||
// this should fail because T is a wrapped asset
|
||||
let _asset_meta = attest_token::attest_token_test<wrapped_coin::coin::T>();
|
||||
}
|
||||
|
||||
#[test(token_bridge=@token_bridge, deployer=@deployer)]
|
||||
fun test_attest_token_with_signer(token_bridge: &signer, deployer: &signer) {
|
||||
setup(token_bridge, deployer);
|
||||
let asset_meta1 = attest_token::attest_token_test<MyCoin>();
|
||||
|
||||
// check that native asset is registered with State
|
||||
let token_address = token_hash::derive<MyCoin>();
|
||||
assert!(state::native_asset_info(token_address) == type_of<MyCoin>(), 0);
|
||||
|
||||
// attest same token a second time, should have no change in behavior
|
||||
let asset_meta2 = attest_token::attest_token_test<MyCoin>();
|
||||
assert!(asset_meta1 == asset_meta2, 0);
|
||||
assert!(state::native_asset_info(token_address) == type_of<MyCoin>(), 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,376 @@
|
|||
module token_bridge::complete_transfer {
|
||||
use aptos_std::from_bcs;
|
||||
use aptos_framework::coin::{Self, Coin};
|
||||
|
||||
use token_bridge::vaa;
|
||||
use token_bridge::transfer::{Self, Transfer};
|
||||
use token_bridge::state;
|
||||
use token_bridge::wrapped;
|
||||
use token_bridge::normalized_amount;
|
||||
|
||||
use wormhole::external_address::get_bytes;
|
||||
|
||||
const E_INVALID_TARGET: u64 = 0;
|
||||
|
||||
public entry fun submit_vaa<CoinType>(vaa: vector<u8>, fee_recipient: address): Transfer {
|
||||
let vaa = vaa::parse_verify_and_replay_protect(vaa);
|
||||
let transfer = transfer::parse(wormhole::vaa::destroy(vaa));
|
||||
complete_transfer<CoinType>(&transfer, fee_recipient);
|
||||
transfer
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun test<CoinType>(transfer: &Transfer, fee_recipient: address) {
|
||||
complete_transfer<CoinType>(transfer, fee_recipient)
|
||||
}
|
||||
|
||||
fun complete_transfer<CoinType>(transfer: &Transfer, fee_recipient: address) {
|
||||
let to_chain = transfer::get_to_chain(transfer);
|
||||
assert!(to_chain == wormhole::state::get_chain_id(), E_INVALID_TARGET);
|
||||
|
||||
let token_chain = transfer::get_token_chain(transfer);
|
||||
let token_address = transfer::get_token_address(transfer);
|
||||
let origin_info = state::create_origin_info(token_chain, token_address);
|
||||
|
||||
state::assert_coin_origin_info<CoinType>(origin_info);
|
||||
|
||||
let decimals = coin::decimals<CoinType>();
|
||||
|
||||
let amount = normalized_amount::denormalize(transfer::get_amount(transfer), decimals);
|
||||
let fee_amount = normalized_amount::denormalize(transfer::get_fee(transfer), decimals);
|
||||
|
||||
let recipient = from_bcs::to_address(get_bytes(&transfer::get_to(transfer)));
|
||||
|
||||
let recipient_coins: Coin<CoinType>;
|
||||
|
||||
if (state::is_wrapped_asset<CoinType>()) {
|
||||
recipient_coins = wrapped::mint<CoinType>(amount);
|
||||
} else {
|
||||
let token_bridge = state::token_bridge_signer();
|
||||
recipient_coins = coin::withdraw<CoinType>(&token_bridge, amount);
|
||||
};
|
||||
|
||||
// take out fee from the recipient's coins. `extract` will revert
|
||||
// if fee > amount
|
||||
let fee_coins = coin::extract(&mut recipient_coins, fee_amount);
|
||||
coin::deposit(recipient, recipient_coins);
|
||||
coin::deposit(fee_recipient, fee_coins);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::complete_transfer_test {
|
||||
use std::bcs;
|
||||
use std::signer;
|
||||
use aptos_framework::coin;
|
||||
use aptos_framework::account;
|
||||
use aptos_framework::string::{utf8};
|
||||
|
||||
use token_bridge::transfer::{Self, Transfer};
|
||||
use token_bridge::transfer_tokens;
|
||||
use token_bridge::token_hash;
|
||||
use token_bridge::complete_transfer;
|
||||
use token_bridge::token_bridge;
|
||||
use token_bridge::wrapped;
|
||||
use token_bridge::transfer_result;
|
||||
use token_bridge::normalized_amount;
|
||||
|
||||
use token_bridge::wrapped_test;
|
||||
|
||||
use wormhole::state;
|
||||
use wormhole::wormhole_test;
|
||||
use wormhole::external_address;
|
||||
|
||||
struct MyCoin {}
|
||||
|
||||
struct OtherCoin {}
|
||||
|
||||
fun init_my_token(admin: &signer, decimals: u8, amount: u64) {
|
||||
let name = utf8(b"mycoindd");
|
||||
let symbol = utf8(b"MCdd");
|
||||
let monitor_supply = true;
|
||||
let (burn_cap, freeze_cap, mint_cap) = coin::initialize<MyCoin>(admin, name, symbol, decimals, monitor_supply);
|
||||
coin::destroy_freeze_cap(freeze_cap);
|
||||
coin::destroy_burn_cap(burn_cap);
|
||||
coin::register<MyCoin>(admin);
|
||||
coin::deposit(signer::address_of(admin), coin::mint(amount, &mint_cap));
|
||||
coin::destroy_mint_cap(mint_cap);
|
||||
}
|
||||
|
||||
public fun setup(
|
||||
deployer: &signer,
|
||||
token_bridge: &signer,
|
||||
to: address,
|
||||
fee_recipient: address,
|
||||
decimals: u8,
|
||||
amount: u64,
|
||||
) {
|
||||
// initialise wormhole and token bridge
|
||||
wormhole_test::setup(0);
|
||||
token_bridge::init_test(deployer);
|
||||
|
||||
// initialise MyToken
|
||||
init_my_token(token_bridge, decimals, amount);
|
||||
|
||||
// initialise 'to' and 'fee_recipient' and register them to accept MyCoins
|
||||
let to = &account::create_account_for_test(to);
|
||||
let fee_recipient = &account::create_account_for_test(fee_recipient);
|
||||
coin::register<MyCoin>(to);
|
||||
coin::register<MyCoin>(fee_recipient);
|
||||
|
||||
// initialise wrapped token
|
||||
wrapped_test::init_wrapped_token();
|
||||
coin::register<wrapped_coin::coin::T>(to);
|
||||
coin::register<wrapped_coin::coin::T>(fee_recipient);
|
||||
|
||||
}
|
||||
|
||||
|
||||
#[test(
|
||||
deployer = @deployer,
|
||||
token_bridge = @token_bridge,
|
||||
)]
|
||||
public fun test_native_transfer_10_decimals(
|
||||
deployer: &signer,
|
||||
token_bridge: &signer
|
||||
) {
|
||||
let to = @0x12;
|
||||
let fee_recipient = @0x32;
|
||||
// the dust at the end will be removed during normalisation/denormalisation
|
||||
let amount = 10010;
|
||||
let fee_amount = 4000;
|
||||
let decimals = 10;
|
||||
|
||||
setup(deployer, token_bridge, to, fee_recipient, decimals, amount);
|
||||
|
||||
let token_address = token_hash::get_external_address(&token_hash::derive<MyCoin>());
|
||||
let token_chain = state::get_chain_id();
|
||||
let to_chain = state::get_chain_id();
|
||||
let transfer: Transfer = transfer::create(
|
||||
normalized_amount::normalize(amount, decimals),
|
||||
token_address,
|
||||
token_chain,
|
||||
external_address::from_bytes(bcs::to_bytes(&to)),
|
||||
to_chain,
|
||||
normalized_amount::normalize(fee_amount, decimals),
|
||||
);
|
||||
|
||||
assert!(coin::balance<MyCoin>(to) == 0, 0);
|
||||
assert!(coin::balance<MyCoin>(fee_recipient) == 0, 0);
|
||||
complete_transfer::test<MyCoin>(&transfer, fee_recipient);
|
||||
assert!(coin::balance<MyCoin>(to) == 6000, 0);
|
||||
assert!(coin::balance<MyCoin>(fee_recipient) == 4000, 0);
|
||||
}
|
||||
|
||||
#[test(
|
||||
deployer = @deployer,
|
||||
token_bridge = @token_bridge,
|
||||
)]
|
||||
public fun test_native_transfer_4_decimals(
|
||||
deployer: &signer,
|
||||
token_bridge: &signer
|
||||
) {
|
||||
let to = @0x12;
|
||||
let fee_recipient = @0x32;
|
||||
let amount = 100;
|
||||
let fee_amount = 40;
|
||||
let decimals = 4;
|
||||
|
||||
// the token has 4 decimals, so no scaling is expected
|
||||
setup(deployer, token_bridge, to, fee_recipient, decimals, amount);
|
||||
|
||||
let token_address = token_hash::get_external_address(&token_hash::derive<MyCoin>());
|
||||
let token_chain = state::get_chain_id();
|
||||
let to_chain = state::get_chain_id();
|
||||
let transfer: Transfer = transfer::create(
|
||||
normalized_amount::normalize(amount, decimals),
|
||||
token_address,
|
||||
token_chain,
|
||||
external_address::from_bytes(bcs::to_bytes(&to)),
|
||||
to_chain,
|
||||
normalized_amount::normalize(fee_amount, decimals),
|
||||
);
|
||||
|
||||
assert!(coin::balance<MyCoin>(to) == 0, 0);
|
||||
assert!(coin::balance<MyCoin>(fee_recipient) == 0, 0);
|
||||
complete_transfer::test<MyCoin>(&transfer, fee_recipient);
|
||||
assert!(coin::balance<MyCoin>(to) == 60, 0);
|
||||
assert!(coin::balance<MyCoin>(fee_recipient) == 40, 0);
|
||||
}
|
||||
|
||||
#[test(
|
||||
deployer = @deployer,
|
||||
token_bridge = @token_bridge,
|
||||
)]
|
||||
#[expected_failure(abort_code = 65542)] // EINSUFFICIENT_BALANCE
|
||||
public fun test_native_too_much_fee(
|
||||
deployer: &signer,
|
||||
token_bridge: &signer
|
||||
) {
|
||||
let to = @0x12;
|
||||
let fee_recipient = @0x32;
|
||||
let amount = 100;
|
||||
let fee_amount = 101; // FAIL: too much fee
|
||||
let decimals = 8;
|
||||
|
||||
setup(deployer, token_bridge, to, fee_recipient, decimals, amount);
|
||||
|
||||
let token_address = token_hash::get_external_address(&token_hash::derive<MyCoin>());
|
||||
let token_chain = state::get_chain_id();
|
||||
let to_chain = state::get_chain_id();
|
||||
let transfer: Transfer = transfer::create(
|
||||
normalized_amount::normalize(amount, decimals),
|
||||
token_address,
|
||||
token_chain,
|
||||
external_address::from_bytes(bcs::to_bytes(&to)),
|
||||
to_chain,
|
||||
normalized_amount::normalize(fee_amount, decimals),
|
||||
);
|
||||
complete_transfer::test<MyCoin>(&transfer, fee_recipient);
|
||||
}
|
||||
|
||||
#[test(
|
||||
deployer = @deployer,
|
||||
token_bridge = @token_bridge,
|
||||
)]
|
||||
#[expected_failure(abort_code = 1)] // E_ORIGIN_ADDRESS_MISMATCH
|
||||
public fun test_native_wrong_coin(
|
||||
deployer: &signer,
|
||||
token_bridge: &signer
|
||||
) {
|
||||
let to = @0x12;
|
||||
let fee_recipient = @0x32;
|
||||
let amount = 100;
|
||||
let fee_amount = 40;
|
||||
let decimals = 8;
|
||||
|
||||
setup(deployer, token_bridge, to, fee_recipient, decimals, amount);
|
||||
|
||||
let token_address = token_hash::get_external_address(&token_hash::derive<MyCoin>());
|
||||
let token_chain = state::get_chain_id();
|
||||
let to_chain = state::get_chain_id();
|
||||
let transfer: Transfer = transfer::create(
|
||||
normalized_amount::normalize(amount, decimals),
|
||||
token_address,
|
||||
token_chain,
|
||||
external_address::from_bytes(bcs::to_bytes(&to)),
|
||||
to_chain,
|
||||
normalized_amount::normalize(fee_amount, decimals),
|
||||
);
|
||||
// FAIL: wrong type argument
|
||||
complete_transfer::test<OtherCoin>(&transfer, fee_recipient);
|
||||
}
|
||||
|
||||
#[test(
|
||||
deployer = @deployer,
|
||||
token_bridge = @token_bridge,
|
||||
)]
|
||||
#[expected_failure(abort_code = 0)] // E_ORIGIN_CHAIN_MISMATCH
|
||||
public fun test_native_wrong_origin_address(
|
||||
deployer: &signer,
|
||||
token_bridge: &signer
|
||||
) {
|
||||
let to = @0x12;
|
||||
let fee_recipient = @0x32;
|
||||
let amount = 100;
|
||||
let fee_amount = 40;
|
||||
let decimals = 8;
|
||||
|
||||
setup(deployer, token_bridge, to, fee_recipient, decimals, amount);
|
||||
|
||||
let token_address = token_hash::get_external_address(&token_hash::derive<MyCoin>());
|
||||
let token_chain = wormhole::u16::from_u64(10); // FAIL: wrong origin chain (MyCoin is native)
|
||||
let to_chain = state::get_chain_id();
|
||||
let transfer: Transfer = transfer::create(
|
||||
normalized_amount::normalize(amount, decimals),
|
||||
token_address,
|
||||
token_chain,
|
||||
external_address::from_bytes(bcs::to_bytes(&to)),
|
||||
to_chain,
|
||||
normalized_amount::normalize(fee_amount, decimals),
|
||||
);
|
||||
complete_transfer::test<MyCoin>(&transfer, fee_recipient);
|
||||
}
|
||||
|
||||
#[test(
|
||||
deployer = @deployer,
|
||||
token_bridge = @token_bridge,
|
||||
)]
|
||||
public fun test_wrapped_transfer_roundtrip(
|
||||
deployer: &signer,
|
||||
token_bridge: &signer
|
||||
) {
|
||||
let to = @0x12;
|
||||
let fee_recipient = @0x32;
|
||||
|
||||
setup(deployer, token_bridge, to, fee_recipient, 8, 0);
|
||||
|
||||
let beef_coins = wrapped::mint<wrapped_coin::coin::T>(100000);
|
||||
|
||||
let result = transfer_tokens::transfer_tokens_test<wrapped_coin::coin::T>(
|
||||
beef_coins,
|
||||
5000
|
||||
);
|
||||
|
||||
let (token_chain, token_address, normalized_amount, normalized_relayer_fee)
|
||||
= transfer_result::destroy(result);
|
||||
|
||||
|
||||
let to_chain = state::get_chain_id();
|
||||
let transfer: Transfer = transfer::create(
|
||||
normalized_amount,
|
||||
token_address,
|
||||
token_chain,
|
||||
external_address::from_bytes(bcs::to_bytes(&to)),
|
||||
to_chain,
|
||||
normalized_relayer_fee,
|
||||
);
|
||||
|
||||
assert!(coin::balance<wrapped_coin::coin::T>(to) == 0, 0);
|
||||
assert!(coin::balance<wrapped_coin::coin::T>(fee_recipient) == 0, 0);
|
||||
|
||||
complete_transfer::test<wrapped_coin::coin::T>(&transfer, fee_recipient);
|
||||
|
||||
assert!(coin::balance<wrapped_coin::coin::T>(to) == 95000, 0);
|
||||
assert!(coin::balance<wrapped_coin::coin::T>(fee_recipient) == 5000, 0);
|
||||
}
|
||||
|
||||
#[test(
|
||||
deployer = @deployer,
|
||||
token_bridge = @token_bridge,
|
||||
)]
|
||||
public fun test_wrapped_transfer(
|
||||
deployer: &signer,
|
||||
token_bridge: &signer
|
||||
) {
|
||||
let to = @0x12;
|
||||
let fee_recipient = @0x32;
|
||||
let amount = 100;
|
||||
let fee_amount = 40;
|
||||
let decimals = 8;
|
||||
|
||||
setup(deployer, token_bridge, to, fee_recipient, decimals, 0);
|
||||
|
||||
let token_address = external_address::from_bytes(x"deadbeef");
|
||||
let token_chain = wormhole::u16::from_u64(2);
|
||||
let to_chain = state::get_chain_id();
|
||||
let transfer: Transfer = transfer::create(
|
||||
normalized_amount::normalize(amount, decimals),
|
||||
token_address,
|
||||
token_chain,
|
||||
external_address::from_bytes(bcs::to_bytes(&to)),
|
||||
to_chain,
|
||||
normalized_amount::normalize(fee_amount, decimals),
|
||||
);
|
||||
|
||||
assert!(coin::balance<wrapped_coin::coin::T>(to) == 0, 0);
|
||||
assert!(coin::balance<wrapped_coin::coin::T>(fee_recipient) == 0, 0);
|
||||
|
||||
complete_transfer::test<wrapped_coin::coin::T>(&transfer, fee_recipient);
|
||||
|
||||
// the wrapped asset has 9 decimals (see wrapped_test::init_wrapped_token)
|
||||
assert!(coin::balance<wrapped_coin::coin::T>(to) == 600, 0);
|
||||
assert!(coin::balance<wrapped_coin::coin::T>(fee_recipient) == 400, 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
module token_bridge::complete_transfer_with_payload {
|
||||
use aptos_framework::coin::{Self, Coin};
|
||||
|
||||
use token_bridge::vaa;
|
||||
use token_bridge::transfer_with_payload::{Self as transfer, TransferWithPayload};
|
||||
use token_bridge::state;
|
||||
use token_bridge::wrapped;
|
||||
use token_bridge::normalized_amount;
|
||||
|
||||
use wormhole::emitter::{Self, EmitterCapability};
|
||||
|
||||
const E_INVALID_RECIPIENT: u64 = 0;
|
||||
const E_INVALID_TARGET: u64 = 1;
|
||||
|
||||
// TODO(csongor): document this, and create an example contract receiving
|
||||
// such a transfer
|
||||
public entry fun submit_vaa<CoinType>(
|
||||
vaa: vector<u8>,
|
||||
emitter_cap: &EmitterCapability
|
||||
): (Coin<CoinType>, TransferWithPayload) {
|
||||
let vaa = vaa::parse_verify_and_replay_protect(vaa);
|
||||
let transfer = transfer::parse(wormhole::vaa::destroy(vaa));
|
||||
|
||||
let to_chain = transfer::get_to_chain(&transfer);
|
||||
assert!(to_chain == wormhole::state::get_chain_id(), E_INVALID_TARGET);
|
||||
|
||||
let token_chain = transfer::get_token_chain(&transfer);
|
||||
let token_address = transfer::get_token_address(&transfer);
|
||||
let origin_info = state::create_origin_info(token_chain, token_address);
|
||||
|
||||
state::assert_coin_origin_info<CoinType>(origin_info);
|
||||
|
||||
let decimals = coin::decimals<CoinType>();
|
||||
|
||||
let amount = normalized_amount::denormalize(transfer::get_amount(&transfer), decimals);
|
||||
|
||||
// transfers with payload can only be redeemed by the recipient.
|
||||
let recipient = transfer::get_to(&transfer);
|
||||
assert!(
|
||||
recipient == emitter::get_external_address(emitter_cap),
|
||||
E_INVALID_RECIPIENT
|
||||
);
|
||||
|
||||
let recipient_coins: Coin<CoinType>;
|
||||
|
||||
if (state::is_wrapped_asset<CoinType>()) {
|
||||
recipient_coins = wrapped::mint<CoinType>(amount);
|
||||
} else {
|
||||
let token_bridge = state::token_bridge_signer();
|
||||
recipient_coins = coin::withdraw<CoinType>(&token_bridge, amount);
|
||||
};
|
||||
|
||||
(recipient_coins, transfer)
|
||||
}
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::complete_transfer_with_payload_test {
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
/// This module implements upgradeability for the token bridge contract.
|
||||
///
|
||||
/// Contract upgrades are authorised by governance, which means that performing
|
||||
/// an upgrade requires a governance VAA signed by a supermajority of the
|
||||
/// wormhole guardians.
|
||||
///
|
||||
/// Upgrades are performed in a commit-reveal scheme, where submitting the VAA
|
||||
/// authorises a particular contract hash. Then in a subsequent transaction, the
|
||||
/// bytecode is uploaded, and if the hash of the bytecode matches the committed
|
||||
/// hash, then the upgrade proceeds.
|
||||
///
|
||||
/// This two-phase process has the advantage that even if the bytecode can't be
|
||||
/// upgraded to for whatever reason, the governance VAA won't be possible to
|
||||
/// replay in the future, since the commit transaction replay protects it.
|
||||
///
|
||||
/// Additionally, there is an optional migration step that may include one-off
|
||||
/// logic to be executed after the upgrade. This has to be done in a separate
|
||||
/// transaction, because the transaction that uploads bytecode cannot execute
|
||||
/// it.
|
||||
module token_bridge::contract_upgrade {
|
||||
use std::vector;
|
||||
use aptos_framework::code;
|
||||
use wormhole::deserialize;
|
||||
use wormhole::cursor;
|
||||
use wormhole::vaa;
|
||||
use wormhole::state as core;
|
||||
use wormhole::keccak256::keccak256;
|
||||
|
||||
use token_bridge::vaa as token_bridge_vaa;
|
||||
use token_bridge::state;
|
||||
|
||||
/// "TokenBridge" (left padded)
|
||||
const TOKEN_BRIDGE: vector<u8> = x"000000000000000000000000000000000000000000546f6b656e427269646765";
|
||||
|
||||
const E_UPGRADE_UNAUTHORIZED: u64 = 0;
|
||||
const E_UNEXPECTED_HASH: u64 = 1;
|
||||
const E_INVALID_MODULE: u64 = 2;
|
||||
const E_INVALID_ACTION: u64 = 3;
|
||||
const E_INVALID_TARGET: u64 = 4;
|
||||
const E_NOT_MIGRATING: u64 = 5;
|
||||
|
||||
/// The `UpgradeAuthorized` type in the global storage represents the fact
|
||||
/// there is an ongoing approved upgrade.
|
||||
/// When the upgrade is finalised in `upgrade`, this object is deleted.
|
||||
struct UpgradeAuthorized has key {
|
||||
hash: vector<u8>
|
||||
}
|
||||
|
||||
struct Hash {
|
||||
hash: vector<u8>
|
||||
}
|
||||
|
||||
fun parse_payload(payload: vector<u8>): Hash {
|
||||
let cur = cursor::init(payload);
|
||||
let target_module = deserialize::deserialize_vector(&mut cur, 32);
|
||||
|
||||
assert!(target_module == TOKEN_BRIDGE, E_INVALID_MODULE);
|
||||
|
||||
let action = deserialize::deserialize_u8(&mut cur);
|
||||
assert!(action == 0x02, E_INVALID_ACTION);
|
||||
|
||||
let chain = deserialize::deserialize_u16(&mut cur);
|
||||
assert!(chain == core::get_chain_id(), E_INVALID_TARGET);
|
||||
|
||||
let hash = deserialize::deserialize_vector(&mut cur, 32);
|
||||
|
||||
cursor::destroy_empty(cur);
|
||||
|
||||
Hash { hash }
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Commit
|
||||
|
||||
public entry fun submit_vaa(vaa: vector<u8>) acquires UpgradeAuthorized {
|
||||
let vaa = vaa::parse_and_verify(vaa);
|
||||
vaa::assert_governance(&vaa);
|
||||
token_bridge_vaa::replay_protect(&vaa);
|
||||
|
||||
authorize_upgrade(parse_payload(vaa::destroy(vaa)));
|
||||
}
|
||||
|
||||
fun authorize_upgrade(hash: Hash) acquires UpgradeAuthorized {
|
||||
let Hash { hash } = hash;
|
||||
let token_bridge = state::token_bridge_signer();
|
||||
if (exists<UpgradeAuthorized>(@token_bridge)) {
|
||||
// TODO(csongor): here we're dropping the upgrade hash, in case an
|
||||
// upgrade fails for some reason. Should we emit a log or something?
|
||||
let UpgradeAuthorized { hash: _ } = move_from<UpgradeAuthorized>(@token_bridge);
|
||||
};
|
||||
move_to(&token_bridge, UpgradeAuthorized { hash });
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun authorized_hash(): vector<u8> acquires UpgradeAuthorized {
|
||||
let u = borrow_global<UpgradeAuthorized>(@token_bridge);
|
||||
u.hash
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Reveal
|
||||
|
||||
public entry fun upgrade(
|
||||
metadata_serialized: vector<u8>,
|
||||
code: vector<vector<u8>>
|
||||
) acquires UpgradeAuthorized {
|
||||
assert!(exists<UpgradeAuthorized>(@token_bridge), E_UPGRADE_UNAUTHORIZED);
|
||||
let UpgradeAuthorized { hash } = move_from<UpgradeAuthorized>(@token_bridge);
|
||||
|
||||
// we compute the hash of hashes of the metadata and the bytecodes.
|
||||
// the aptos framework appears to perform no validation of the metadata,
|
||||
// so we check it here too.
|
||||
let c = copy code;
|
||||
vector::reverse(&mut c);
|
||||
let a = keccak256(metadata_serialized);
|
||||
while (!vector::is_empty(&c)) vector::append(&mut a, keccak256(vector::pop_back(&mut c)));
|
||||
assert!(keccak256(a) == hash, E_UNEXPECTED_HASH);
|
||||
|
||||
let token_bridge = state::token_bridge_signer();
|
||||
code::publish_package_txn(&token_bridge, metadata_serialized, code);
|
||||
|
||||
// allow migration to be run.
|
||||
if (!exists<Migrating>(@token_bridge)) {
|
||||
move_to(&token_bridge, Migrating {});
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Migration
|
||||
|
||||
struct Migrating has key {}
|
||||
|
||||
public fun is_migrating(): bool {
|
||||
exists<Migrating>(@token_bridge)
|
||||
}
|
||||
|
||||
public entry fun migrate() acquires Migrating {
|
||||
assert!(exists<Migrating>(@token_bridge), E_NOT_MIGRATING);
|
||||
let Migrating { } = move_from<Migrating>(@token_bridge);
|
||||
|
||||
// NOTE: put any one-off migration logic here.
|
||||
// Most upgrades likely won't need to do anything, in which case the
|
||||
// rest of this function's body may be empty.
|
||||
// Make sure to delete it after the migration has gone through
|
||||
// successfully.
|
||||
// WARNING: the migration does *not* proceed atomically with the
|
||||
// upgrade (as they are done in separate transactions).
|
||||
// If the nature of your migration absolutely requires the migration to
|
||||
// happen before certain other functionality is available, then guard
|
||||
// that functionality with `assert!(!is_migrating())` (from above).
|
||||
}
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::contract_upgrade_test {
|
||||
use wormhole::wormhole;
|
||||
|
||||
use token_bridge::contract_upgrade;
|
||||
use token_bridge::token_bridge;
|
||||
|
||||
/// A token bridge upgrade VAA that upgrades to 0x10263f154c466b139fda0bf2caa08fd9819d8ded3810446274a99399f886fc76
|
||||
const UPGRADE_VAA: vector<u8> = x"01000000000100b5ebfcccb84d740684429622f2fbc16638fb01222e4a580a6d2049227f37a31a7162d32770f72398fe10d160a968c94256eae9225a3da9c69ab7a41d7b307ede010000000100000001000100000000000000000000000000000000000000000000000000000000000000040000000001f96c9900000000000000000000000000000000000000000000546f6b656e42726964676502001610263f154c466b139fda0bf2caa08fd9819d8ded3810446274a99399f886fc76";
|
||||
|
||||
/// A token bridge upgrade VAA that targets ethereum
|
||||
const ETH_UPGRADE: vector<u8> = x"0100000000010090014add41120b33eb4a03c5dce613815071d18b69a185bf322f327cc79cc52d7d133a59515d13ccfb030f9cc26a86b2bcd4dbe34d8ca6c4cc83299efb3e9b430100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000030a9ea600000000000000000000000000000000000000000000546f6b656e42726964676502000210263f154c466b139fda0bf2caa08fd9819d8ded3810446274a99399f886fc76";
|
||||
|
||||
fun setup(deployer: &signer) {
|
||||
let aptos_framework = std::account::create_account_for_test(@aptos_framework);
|
||||
std::timestamp::set_time_has_started_for_testing(&aptos_framework);
|
||||
wormhole::init_test(
|
||||
22,
|
||||
1,
|
||||
x"0000000000000000000000000000000000000000000000000000000000000004",
|
||||
x"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe",
|
||||
0
|
||||
);
|
||||
token_bridge::init_test(deployer);
|
||||
}
|
||||
|
||||
#[test(deployer = @deployer)]
|
||||
public fun test_contract_upgrade_authorize(deployer: &signer) {
|
||||
setup(deployer);
|
||||
|
||||
contract_upgrade::submit_vaa(UPGRADE_VAA);
|
||||
let expected_hash = x"10263f154c466b139fda0bf2caa08fd9819d8ded3810446274a99399f886fc76";
|
||||
|
||||
assert!(contract_upgrade::authorized_hash() == expected_hash, 0);
|
||||
}
|
||||
|
||||
#[test(deployer = @deployer)]
|
||||
#[expected_failure(abort_code = 0x6407)]
|
||||
public fun test_contract_upgrade_double(deployer: &signer) {
|
||||
setup(deployer);
|
||||
|
||||
// make sure we can't replay a VAA
|
||||
contract_upgrade::submit_vaa(UPGRADE_VAA);
|
||||
contract_upgrade::submit_vaa(UPGRADE_VAA);
|
||||
}
|
||||
|
||||
#[test(deployer = @deployer)]
|
||||
#[expected_failure(abort_code = 4)]
|
||||
public fun test_contract_upgrade_wrong_chain(deployer: &signer) {
|
||||
setup(deployer);
|
||||
|
||||
contract_upgrade::submit_vaa(ETH_UPGRADE);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// This module is for dynamically deploying a module/CoinType on-chain,
|
||||
// as opposed to submitting a tx from off-chain to publish a module containing the CoinType.
|
||||
//
|
||||
// Specifically, we wish to dynamically deploy the the following module
|
||||
//
|
||||
// module deployer::coin {
|
||||
// struct T has key {}
|
||||
// }
|
||||
//
|
||||
// where deployer is a fixed address, but which will be spliced out on-chain.
|
||||
//
|
||||
// We create a Move package [INSERT_LINK_HERE], compile it using "aptos move compile --save-metadata", obtain the
|
||||
// package_metadata.bcs and coin.mv files, then upload them on-chain in byte format (see the deployCoin function below).
|
||||
//
|
||||
// We replace the deployer address embedded in the source code with the new deployer address and call publish_package_txn
|
||||
// to publish the code at the new deployer's account.
|
||||
//
|
||||
//
|
||||
// TODO: find out if we need to update the source_digest in the package metadata as well
|
||||
|
||||
module token_bridge::deploy_coin {
|
||||
use std::signer::{Self};
|
||||
use std::vector::{Self};
|
||||
use std::code::{publish_package_txn};
|
||||
use std::bcs::{Self};
|
||||
|
||||
public entry fun deploy_coin(deployer: &signer) {
|
||||
let addr = signer::address_of(deployer);
|
||||
let addr_bytes = bcs::to_bytes(&addr);
|
||||
let code = x"a11ceb0b05000000050100020202040706130819200a390500000001080004636f696e01540b64756d6d795f6669656c64";
|
||||
vector::append(&mut code, addr_bytes);
|
||||
vector::append(&mut code, x"000201020100");
|
||||
let metadata_serialized: vector<u8> = x"0b57726170706564436f696e010000000000000000404237423334333744324439304635433830464246313246423231423543424543383332443335453138364542373539304431434134324338334631333639324586021f8b08000000000002ffb590316bc330108577fd0a21af8d9dae810ea5a55bc9d0408710cac9ba388765e990649752fadf2b252e5d0299bce9ddbd77ef437b86b6870e0fc2c180f241aaf700cc689e3c3925260c91bc2bf375bdaeef9518b90b60f083bda5f6ab2c5a3f3024d2169510d56efbbcdd482627e76c941a8f3ea01c809cc324035a8488a2da1b6474065d4b180fa27ae4e4e34bc81c9f3ef4f9f4b7ec28958a534a1c374d93e569d4756e6ca0985716749c9f6deea8b341ddc9386a43a1042fabc14fd81cff0ecffe7f9d1301a76237386542257f44f59a336fc958d2cb8114b98ae792eb10e71f599ae232bc89b1f33dbaa5295229b906f10b3085ae64a80200000104636f696e731f8b08000000000002ff0dc0dd0984300c00e0f74e91116a9a26c5396e81246db9e37e8453411177d7ef3bd5f5d3206e28d235e66cac929cd02333d68c899db3172677eb5849ade2d007214dd18b49e1a656c8c6d1a7d70f8e00b779f9afbec0039e3ac3bbed709ce10c176825e8506c00000000000000";
|
||||
let code_array = vector::empty<vector<u8>>();
|
||||
vector::push_back(&mut code_array, code);
|
||||
publish_package_txn(deployer, metadata_serialized, code_array);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/// Amounts in represented in token bridge VAAs are capped at 8 decimals. This
|
||||
/// means that any amount that's given as having more decimals is truncated to 8
|
||||
/// decimals. On the way out, these amount have to be scaled back to the
|
||||
/// original decimal amount. This module defines `NormalizedAmount`, which
|
||||
/// represents amounts that have been capped at 8 decimals.
|
||||
///
|
||||
/// The functions `normalize` and `denormalize` take care of convertion to/from
|
||||
/// this type given the original amount's decimals.
|
||||
module token_bridge::normalized_amount {
|
||||
use wormhole::cursor::Cursor;
|
||||
use wormhole::deserialize;
|
||||
use wormhole::serialize;
|
||||
|
||||
struct NormalizedAmount has store, copy, drop {
|
||||
amount: u64
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun get_amount(n: NormalizedAmount): u64 {
|
||||
n.amount
|
||||
}
|
||||
|
||||
public fun normalize(amount: u64, decimals: u8): NormalizedAmount {
|
||||
if (decimals > 8) {
|
||||
let n = decimals - 8;
|
||||
while (n > 0){
|
||||
amount = amount / 10;
|
||||
n = n - 1;
|
||||
}
|
||||
};
|
||||
NormalizedAmount { amount }
|
||||
}
|
||||
|
||||
public fun denormalize(amount: NormalizedAmount, decimals: u8): u64 {
|
||||
let NormalizedAmount { amount } = amount;
|
||||
if (decimals > 8) {
|
||||
let n = decimals - 8;
|
||||
while (n > 0){
|
||||
amount = amount * 10;
|
||||
n = n - 1;
|
||||
}
|
||||
};
|
||||
amount
|
||||
}
|
||||
|
||||
public fun deserialize(cur: &mut Cursor<u8>): NormalizedAmount {
|
||||
// in the VAA wire format, amounts are 32 bytes.
|
||||
let amount = deserialize::deserialize_u256(cur);
|
||||
NormalizedAmount { amount: wormhole::u256::as_u64(amount) }
|
||||
}
|
||||
|
||||
public fun serialize(buf: &mut vector<u8>, e: NormalizedAmount) {
|
||||
let NormalizedAmount { amount } = e;
|
||||
serialize::serialize_u256(buf, wormhole::u256::from_u64(amount))
|
||||
}
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::normalized_amount_test {
|
||||
use token_bridge::normalized_amount;
|
||||
|
||||
#[test]
|
||||
fun test_normalize_denormalize_amount() {
|
||||
let a = 12345678910111;
|
||||
let b = normalized_amount::normalize(a, 9);
|
||||
let c = normalized_amount::denormalize(b, 9);
|
||||
assert!(c == 12345678910110, 0);
|
||||
|
||||
let x = 12345678910111;
|
||||
let y = normalized_amount::normalize(x, 5);
|
||||
let z = normalized_amount::denormalize(y, 5);
|
||||
assert!(z == x, 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
/// The `string32` module defines the `String32` type which represents UTF8
|
||||
/// encoded strings that are guaranteed to be 32 bytes long, with 0 padding on
|
||||
/// the right.
|
||||
module token_bridge::string32 {
|
||||
|
||||
use std::string::{Self, String};
|
||||
use std::option;
|
||||
use std::vector;
|
||||
|
||||
use wormhole::cursor::Cursor;
|
||||
use wormhole::deserialize;
|
||||
use wormhole::serialize;
|
||||
|
||||
const E_STRING_TOO_LONG: u64 = 0;
|
||||
|
||||
/// A `String32` holds a ut8 string which is guaranteed to be 32 bytes long.
|
||||
struct String32 has copy, drop, store {
|
||||
string: String
|
||||
}
|
||||
|
||||
spec String32 {
|
||||
invariant string::length(string) == 32;
|
||||
}
|
||||
|
||||
/// Right-pads a `String` to a `String32` with 0 bytes.
|
||||
/// Aborts if the string is longer than 32 bytes.
|
||||
public fun right_pad(s: &String): String32 {
|
||||
let length = string::length(s);
|
||||
assert!(length <= 32, E_STRING_TOO_LONG);
|
||||
let string = *string::bytes(s);
|
||||
let zeros = 32 - length;
|
||||
while ({
|
||||
spec {
|
||||
invariant zeros + vector::length(string) == 32;
|
||||
};
|
||||
zeros > 0
|
||||
}) {
|
||||
vector::push_back(&mut string, 0);
|
||||
zeros = zeros - 1;
|
||||
};
|
||||
String32 { string: string::utf8(string) }
|
||||
}
|
||||
|
||||
/// Internal function to take the first 32 bytes of a byte sequence and
|
||||
/// convert to a utf8 `String`.
|
||||
/// Takes the longest prefix that's valid utf8 and maximum 32 bytes.
|
||||
///
|
||||
/// Even if the input is valid utf8, the result might be shorter than 32
|
||||
/// bytes, because the original string might have a multi-byte utf8
|
||||
/// character at the 32 byte boundary, which, when split, results in an
|
||||
/// invalid code point, so we remove it.
|
||||
fun take(bytes: vector<u8>, n: u64): String {
|
||||
while (vector::length(&bytes) > n) {
|
||||
vector::pop_back(&mut bytes);
|
||||
};
|
||||
|
||||
let utf8 = string::try_utf8(bytes);
|
||||
while (option::is_none(&utf8)) {
|
||||
vector::pop_back(&mut bytes);
|
||||
utf8 = string::try_utf8(bytes);
|
||||
};
|
||||
option::extract(&mut utf8)
|
||||
}
|
||||
|
||||
/// Takes the first `n` bytes of a `String`.
|
||||
///
|
||||
/// Even if the input string is longer than `n`, the resulting string might
|
||||
/// be shorter because the original string might have a multi-byte utf8
|
||||
/// character at the byte boundary, which, when split, results in an invalid
|
||||
/// code point, so we remove it.
|
||||
public fun take_utf8(str: String, n: u64): String {
|
||||
take(*string::bytes(&str), n)
|
||||
}
|
||||
|
||||
/// Truncates or right-pads a `String` to a `String32`.
|
||||
/// Does not abort.
|
||||
public fun from_string(s: &String): String32 {
|
||||
right_pad(&take(*string::bytes(s), 32))
|
||||
}
|
||||
|
||||
/// Truncates or right-pads a byte vector to a `String32`.
|
||||
/// Does not abort.
|
||||
public fun from_bytes(b: vector<u8>): String32 {
|
||||
right_pad(&take(b, 32))
|
||||
}
|
||||
|
||||
/// Converts `String32` to `String`, removing trailing 0s.
|
||||
public fun to_string(s: &String32): String {
|
||||
let String32 { string } = s;
|
||||
let bytes = *string::bytes(string);
|
||||
// keep dropping the last character while it's 0
|
||||
while (!vector::is_empty(&bytes) &&
|
||||
*vector::borrow(&bytes, vector::length(&bytes) - 1) == 0
|
||||
) {
|
||||
vector::pop_back(&mut bytes);
|
||||
};
|
||||
string::utf8(bytes)
|
||||
}
|
||||
|
||||
/// Converts `String32` to a byte vector of length 32.
|
||||
public fun to_bytes(s: &String32): vector<u8> {
|
||||
*string::bytes(&s.string)
|
||||
}
|
||||
|
||||
public fun deserialize(cur: &mut Cursor<u8>): String32 {
|
||||
let bytes = deserialize::deserialize_vector(cur, 32);
|
||||
from_bytes(bytes)
|
||||
}
|
||||
|
||||
public fun serialize(buf: &mut vector<u8>, e: String32) {
|
||||
serialize::serialize_vector(buf, to_bytes(&e))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::string32_test {
|
||||
use std::string;
|
||||
use std::vector;
|
||||
use token_bridge::string32;
|
||||
|
||||
#[test]
|
||||
public fun test_right_pad() {
|
||||
let result = string32::right_pad(&string::utf8(b"hello"));
|
||||
assert!(string32::to_string(&result) == string::utf8(b"hello"), 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(abort_code = 0)]
|
||||
public fun test_right_pad_fail() {
|
||||
let too_long = string::utf8(b"this string is very very very very very very very very very very very very very very very long");
|
||||
string32::right_pad(&too_long);
|
||||
}
|
||||
|
||||
#[test]
|
||||
public fun test_from_string_short() {
|
||||
let result = string32::from_string(&string::utf8(b"hello"));
|
||||
assert!(string32::to_string(&result) == string::utf8(b"hello"), 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
public fun test_from_string_long() {
|
||||
let long = string32::from_string(&string::utf8(b"this string is very very very very very very very very very very very very very very very long"));
|
||||
assert!(string32::to_string(&long) == string::utf8(b"this string is very very very ve"), 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
public fun test_from_string_weird_utf8() {
|
||||
let string = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
assert!(vector::length(&string) == 31, 0);
|
||||
// append the samaritan letter Alaf, a 3-byte utf8 character the move
|
||||
// parser only allows ascii characters unfortunately (the character
|
||||
// looks nice)
|
||||
vector::append(&mut string, x"e0a080");
|
||||
// it's valid utf8
|
||||
let string = string::utf8(string);
|
||||
// string length is bytes, not characters
|
||||
assert!(string::length(&string) == 34, 0);
|
||||
let padded = string32::from_string(&string);
|
||||
// notice that the e0 byte got dropped at the end
|
||||
assert!(string32::to_string(&padded) == string::utf8(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
public fun test_from_bytes_invalid_utf8() {
|
||||
// invalid utf8
|
||||
let bytes = x"e0a0";
|
||||
let result = string::utf8(b"");
|
||||
assert!(string32::to_string(&string32::from_bytes(bytes)) == result, 0)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
module token_bridge::register_chain {
|
||||
|
||||
use wormhole::u16::{Self, U16};
|
||||
use wormhole::cursor;
|
||||
use wormhole::deserialize;
|
||||
use wormhole::vaa;
|
||||
use wormhole::external_address::{Self, ExternalAddress};
|
||||
|
||||
use token_bridge::vaa as token_bridge_vaa;
|
||||
use token_bridge::state;
|
||||
|
||||
/// "TokenBridge" (left padded)
|
||||
const TOKEN_BRIDGE: vector<u8> = x"000000000000000000000000000000000000000000546f6b656e427269646765";
|
||||
|
||||
const E_INVALID_MODULE: u64 = 0;
|
||||
const E_INVALID_ACTION: u64 = 1;
|
||||
const E_INVALID_TARGET: u64 = 2;
|
||||
|
||||
struct RegisterChain has copy, drop {
|
||||
/// Chain ID
|
||||
emitter_chain_id: U16,
|
||||
/// Emitter address. Left-zero-padded if shorter than 32 bytes
|
||||
emitter_address: ExternalAddress,
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun parse_payload_test(payload: vector<u8>): RegisterChain {
|
||||
parse_payload(payload)
|
||||
}
|
||||
|
||||
fun parse_payload(payload: vector<u8>): RegisterChain {
|
||||
let cur = cursor::init(payload);
|
||||
let target_module = deserialize::deserialize_vector(&mut cur, 32);
|
||||
|
||||
assert!(target_module == TOKEN_BRIDGE, E_INVALID_MODULE);
|
||||
|
||||
let action = deserialize::deserialize_u8(&mut cur);
|
||||
assert!(action == 0x01, E_INVALID_ACTION);
|
||||
|
||||
// TODO(csongor): should we also accept a VAA targeting aptos directly?
|
||||
// why would a registration VAA target a specific chain?
|
||||
let target_chain = deserialize::deserialize_u16(&mut cur);
|
||||
assert!(target_chain == u16::from_u64(0x0), E_INVALID_TARGET);
|
||||
|
||||
let emitter_chain_id = deserialize::deserialize_u16(&mut cur);
|
||||
|
||||
let emitter_address = external_address::deserialize(&mut cur);
|
||||
|
||||
cursor::destroy_empty(cur);
|
||||
|
||||
RegisterChain { emitter_chain_id, emitter_address }
|
||||
}
|
||||
|
||||
public entry fun submit_vaa(vaa: vector<u8>) {
|
||||
let vaa = vaa::parse_and_verify(vaa);
|
||||
vaa::assert_governance(&vaa); // not tested
|
||||
token_bridge_vaa::replay_protect(&vaa);
|
||||
|
||||
let RegisterChain { emitter_chain_id, emitter_address } = parse_payload(vaa::destroy(vaa));
|
||||
|
||||
state::set_registered_emitter(emitter_chain_id, emitter_address)
|
||||
}
|
||||
|
||||
public fun get_emitter_chain_id(a: &RegisterChain): U16 {
|
||||
a.emitter_chain_id
|
||||
}
|
||||
|
||||
public fun get_emitter_address(a: &RegisterChain): ExternalAddress {
|
||||
a.emitter_address
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::register_chain_test {
|
||||
use std::option;
|
||||
use wormhole::u16;
|
||||
use token_bridge::register_chain;
|
||||
use wormhole::vaa;
|
||||
use wormhole::wormhole;
|
||||
use wormhole::external_address;
|
||||
use token_bridge::token_bridge;
|
||||
use token_bridge::state;
|
||||
|
||||
/// Registration VAA for the etheruem token bridge 0xdeadbeef
|
||||
const ETHEREUM_TOKEN_REG: vector<u8> = x"0100000000010015d405c74be6d93c3c33ed6b48d8db70dfb31e0981f8098b2a6c7583083e0c3343d4a1abeb3fc1559674fa067b0c0e2e9de2fafeaecdfeae132de2c33c9d27cc0100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000016911ae00000000000000000000000000000000000000000000546f6b656e427269646765010000000200000000000000000000000000000000000000000000000000000000deadbeef";
|
||||
|
||||
/// Another registration VAA for the ethereum token bridge, 0xbeefface
|
||||
const ETHEREUM_TOKEN_REG_2:vector<u8> = x"01000000000100c2157fa1c14957dff26d891e4ad0d993ad527f1d94f603e3d2bb1e37541e2fbe45855ffda1efc7eb2eb24009a1585fa25a267815db97e4a9d4a5eb31987b5fb40100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000017ca43300000000000000000000000000000000000000000000546f6b656e427269646765010000000200000000000000000000000000000000000000000000000000000000beefface";
|
||||
|
||||
/// Registration VAA for the etheruem NFT bridge 0xdeadbeef
|
||||
const ETHEREUM_NFT_REG: vector<u8> = x"0100000000010066cce2cb12d88c97d4975cba858bb3c35d6430003e97fced46a158216f3ca01710fd16cc394441a08fef978108ed80c653437f43bb2ca039226974d9512298b10000000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000018483540000000000000000000000000000000000000000000000004e4654427269646765010000000200000000000000000000000000000000000000000000000000000000deadbeef";
|
||||
|
||||
const ETH_ID: u64 = 2;
|
||||
|
||||
fun setup(deployer: &signer) {
|
||||
let aptos_framework = std::account::create_account_for_test(@aptos_framework);
|
||||
std::timestamp::set_time_has_started_for_testing(&aptos_framework);
|
||||
wormhole::init_test(
|
||||
22,
|
||||
1,
|
||||
x"0000000000000000000000000000000000000000000000000000000000000004",
|
||||
x"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe",
|
||||
0
|
||||
);
|
||||
token_bridge::init_test(deployer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
public fun test_parse() {
|
||||
let vaa = vaa::parse_test(ETHEREUM_TOKEN_REG);
|
||||
let register_chain = register_chain::parse_payload_test(vaa::destroy(vaa));
|
||||
let chain = register_chain::get_emitter_chain_id(®ister_chain);
|
||||
let address = register_chain::get_emitter_address(®ister_chain);
|
||||
|
||||
assert!(chain == u16::from_u64(ETH_ID), 0);
|
||||
assert!(address == external_address::from_bytes(x"deadbeef"), 0);
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(abort_code = 0)]
|
||||
public fun test_parse_fail() {
|
||||
let vaa = vaa::parse_test(ETHEREUM_NFT_REG);
|
||||
// this should fail because it's an NFT registration
|
||||
let _register_chain = register_chain::parse_payload_test(vaa::destroy(vaa));
|
||||
|
||||
}
|
||||
|
||||
#[test(deployer = @deployer)]
|
||||
public fun test_registration(deployer: &signer) {
|
||||
setup(deployer);
|
||||
|
||||
register_chain::submit_vaa(ETHEREUM_TOKEN_REG);
|
||||
let address = state::get_registered_emitter(u16::from_u64(ETH_ID));
|
||||
assert!(address == option::some(external_address::from_bytes(x"deadbeef")), 0);
|
||||
}
|
||||
|
||||
#[test(deployer = @deployer)]
|
||||
#[expected_failure(abort_code = 25607)]
|
||||
public fun test_replay_protect(deployer: &signer) {
|
||||
setup(deployer);
|
||||
|
||||
register_chain::submit_vaa(ETHEREUM_TOKEN_REG);
|
||||
register_chain::submit_vaa(ETHEREUM_TOKEN_REG);
|
||||
}
|
||||
|
||||
#[test(deployer = @deployer)]
|
||||
public fun test_re_registration(deployer: &signer) {
|
||||
test_registration(deployer);
|
||||
|
||||
// TODO(csongor): we register ethereum again, which overrides the
|
||||
// previous one. This deviates from other chains (where this is
|
||||
// rejected), but I think this is the right behaviour.
|
||||
// Easy to change, should be discussed.
|
||||
register_chain::submit_vaa(ETHEREUM_TOKEN_REG_2);
|
||||
let address = state::get_registered_emitter(u16::from_u64(ETH_ID));
|
||||
assert!(address == option::some(external_address::from_bytes(x"beefface")), 0);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,257 @@
|
|||
module token_bridge::state {
|
||||
use std::table::{Self, Table};
|
||||
use std::option::{Self, Option};
|
||||
use aptos_framework::type_info::{Self, TypeInfo, type_of};
|
||||
use aptos_framework::account::{Self, SignerCapability};
|
||||
use aptos_framework::aptos_coin::AptosCoin;
|
||||
use aptos_framework::coin::Coin;
|
||||
|
||||
use wormhole::u16::U16;
|
||||
use wormhole::emitter::EmitterCapability;
|
||||
use wormhole::state;
|
||||
use wormhole::wormhole;
|
||||
use wormhole::set::{Self, Set};
|
||||
use wormhole::external_address::ExternalAddress;
|
||||
|
||||
use token_bridge::token_hash::{Self, TokenHash};
|
||||
|
||||
friend token_bridge::contract_upgrade;
|
||||
friend token_bridge::register_chain;
|
||||
friend token_bridge::token_bridge;
|
||||
friend token_bridge::vaa;
|
||||
friend token_bridge::attest_token;
|
||||
friend token_bridge::wrapped;
|
||||
friend token_bridge::complete_transfer;
|
||||
friend token_bridge::complete_transfer_with_payload;
|
||||
friend token_bridge::transfer_tokens;
|
||||
|
||||
#[test_only]
|
||||
friend token_bridge::wrapped_test;
|
||||
#[test_only]
|
||||
friend token_bridge::vaa_test;
|
||||
|
||||
const E_ORIGIN_CHAIN_MISMATCH: u64 = 0;
|
||||
const E_ORIGIN_ADDRESS_MISMATCH: u64 = 1;
|
||||
const E_WRAPPING_NATIVE_COIN: u64 = 2;
|
||||
const E_WRAPPED_ASSET_NOT_INITIALIZED: u64 = 3;
|
||||
|
||||
/// The origin chain and address of a token. In case of native tokens
|
||||
/// (where the chain is aptos), the token_address is the hash of the token
|
||||
/// info (see token_hash.move for more details)
|
||||
struct OriginInfo has key, store, copy, drop {
|
||||
token_chain: U16,
|
||||
token_address: ExternalAddress,
|
||||
}
|
||||
|
||||
public fun get_origin_info_token_address(info: &OriginInfo): ExternalAddress {
|
||||
info.token_address
|
||||
}
|
||||
|
||||
public fun get_origin_info_token_chain(info: &OriginInfo): U16 {
|
||||
info.token_chain
|
||||
}
|
||||
|
||||
public(friend) fun create_origin_info(
|
||||
token_chain: U16,
|
||||
token_address: ExternalAddress,
|
||||
): OriginInfo {
|
||||
OriginInfo { token_address: token_address, token_chain: token_chain }
|
||||
}
|
||||
|
||||
struct WrappedInfo has store {
|
||||
type_info: Option<TypeInfo>,
|
||||
signer_cap: SignerCapability
|
||||
}
|
||||
|
||||
struct State has key, store {
|
||||
governance_chain_id: U16,
|
||||
governance_contract: ExternalAddress,
|
||||
|
||||
/// Set of consumed VAA hashes
|
||||
consumed_vaas: Set<vector<u8>>,
|
||||
|
||||
/// Mapping of wrapped assets ((chain_id, origin_address) => wrapped_asset info)
|
||||
wrapped_infos: Table<OriginInfo, WrappedInfo>,
|
||||
|
||||
/// Reverse mapping of hash(TypeInfo) for native tokens, so their
|
||||
/// information can be looked up externally by knowing their hash (which
|
||||
/// is the 32 byte "address" that goes into the VAA).
|
||||
native_infos: Table<TokenHash, TypeInfo>,
|
||||
|
||||
signer_cap: SignerCapability,
|
||||
|
||||
emitter_cap: EmitterCapability,
|
||||
|
||||
// Mapping of bridge contracts on other chains
|
||||
registered_emitters: Table<U16, ExternalAddress>,
|
||||
}
|
||||
|
||||
// getters
|
||||
|
||||
public fun vaa_is_consumed(hash: vector<u8>): bool acquires State {
|
||||
let state = borrow_global<State>(@token_bridge);
|
||||
set::contains(&state.consumed_vaas, hash)
|
||||
}
|
||||
|
||||
public fun governance_chain_id(): U16 acquires State {
|
||||
let state = borrow_global<State>(@token_bridge);
|
||||
return state.governance_chain_id
|
||||
}
|
||||
|
||||
public fun governance_contract(): ExternalAddress acquires State {
|
||||
let state = borrow_global<State>(@token_bridge);
|
||||
return state.governance_contract
|
||||
}
|
||||
|
||||
public fun wrapped_asset_info(native_info: OriginInfo): TypeInfo acquires State {
|
||||
let wrapped_infos = &borrow_global<State>(@token_bridge).wrapped_infos;
|
||||
let type_info = table::borrow(wrapped_infos, native_info).type_info;
|
||||
assert!(option::is_some(&type_info), E_WRAPPED_ASSET_NOT_INITIALIZED);
|
||||
option::extract(&mut type_info)
|
||||
}
|
||||
|
||||
public fun native_asset_info(token_address: TokenHash): TypeInfo acquires State {
|
||||
let native_infos = &borrow_global<State>(@token_bridge).native_infos;
|
||||
*table::borrow(native_infos, token_address)
|
||||
}
|
||||
|
||||
/// Returns the origin information for a CoinType
|
||||
public fun origin_info<CoinType>(): OriginInfo acquires OriginInfo {
|
||||
if (is_wrapped_asset<CoinType>()) {
|
||||
*borrow_global<OriginInfo>(type_info::account_address(&type_of<CoinType>()))
|
||||
} else {
|
||||
let token_chain = state::get_chain_id();
|
||||
let token_address = token_hash::get_external_address(&token_hash::derive<CoinType>());
|
||||
OriginInfo { token_chain, token_address }
|
||||
}
|
||||
}
|
||||
|
||||
public fun get_registered_emitter(chain_id: U16): Option<ExternalAddress> acquires State {
|
||||
let state = borrow_global<State>(@token_bridge);
|
||||
if (table::contains(&state.registered_emitters, chain_id)) {
|
||||
option::some(*table::borrow(&state.registered_emitters, chain_id))
|
||||
} else {
|
||||
option::none()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// given the hash of the TypeInfo of a Coin, this tells us if it is registered with Token Bridge
|
||||
public fun is_registered_native_asset<CoinType>(): bool acquires State {
|
||||
let token = token_hash::derive<CoinType>();
|
||||
let native_infos = &borrow_global<State>(@token_bridge).native_infos;
|
||||
!is_wrapped_asset<CoinType>() && table::contains(native_infos, token)
|
||||
}
|
||||
|
||||
public fun is_wrapped_asset<CoinType>(): bool {
|
||||
exists<OriginInfo>(type_info::account_address(&type_of<CoinType>()))
|
||||
}
|
||||
|
||||
public(friend) fun setup_wrapped<CoinType>(
|
||||
origin_info: OriginInfo
|
||||
) acquires State {
|
||||
assert!(origin_info.token_chain != state::get_chain_id(), E_WRAPPING_NATIVE_COIN);
|
||||
let wrapped_infos = &mut borrow_global_mut<State>(@token_bridge).wrapped_infos;
|
||||
let wrapped_info = table::borrow_mut(wrapped_infos, origin_info);
|
||||
|
||||
let coin_signer = account::create_signer_with_capability(&wrapped_info.signer_cap);
|
||||
move_to(&coin_signer, origin_info);
|
||||
|
||||
wrapped_info.type_info = option::some(type_of<CoinType>());
|
||||
|
||||
}
|
||||
|
||||
public fun assert_coin_origin_info<CoinType>(origin: OriginInfo) acquires OriginInfo {
|
||||
let coin_origin = origin_info<CoinType>();
|
||||
assert!(coin_origin.token_chain == origin.token_chain, E_ORIGIN_CHAIN_MISMATCH);
|
||||
assert!(coin_origin.token_address == origin.token_address, E_ORIGIN_ADDRESS_MISMATCH);
|
||||
}
|
||||
|
||||
public(friend) fun publish_message(
|
||||
nonce: u64,
|
||||
payload: vector<u8>,
|
||||
message_fee: Coin<AptosCoin>,
|
||||
): u64 acquires State {
|
||||
let emitter_cap = &mut borrow_global_mut<State>(@token_bridge).emitter_cap;
|
||||
wormhole::publish_message(
|
||||
emitter_cap,
|
||||
nonce,
|
||||
payload,
|
||||
message_fee
|
||||
)
|
||||
}
|
||||
|
||||
public(friend) fun token_bridge_signer(): signer acquires State {
|
||||
account::create_signer_with_capability(&borrow_global<State>(@token_bridge).signer_cap)
|
||||
}
|
||||
|
||||
// setters
|
||||
|
||||
public(friend) fun set_vaa_consumed(hash: vector<u8>) acquires State {
|
||||
let state = borrow_global_mut<State>(@token_bridge);
|
||||
set::add(&mut state.consumed_vaas, hash);
|
||||
}
|
||||
|
||||
public(friend) fun set_governance_chain_id(governance_chain_id: U16) acquires State {
|
||||
let state = borrow_global_mut<State>(@token_bridge);
|
||||
state.governance_chain_id = governance_chain_id;
|
||||
}
|
||||
|
||||
public(friend) fun set_governance_contract(governance_contract: ExternalAddress) acquires State {
|
||||
let state = borrow_global_mut<State>(@token_bridge);
|
||||
state.governance_contract = governance_contract;
|
||||
}
|
||||
|
||||
public(friend) fun set_registered_emitter(chain_id: U16, bridge_contract: ExternalAddress) acquires State {
|
||||
let state = borrow_global_mut<State>(@token_bridge);
|
||||
table::upsert(&mut state.registered_emitters, chain_id, bridge_contract);
|
||||
}
|
||||
|
||||
// 32-byte native asset address => type info
|
||||
public(friend) fun set_native_asset_type_info<CoinType>() acquires State {
|
||||
let token_address = token_hash::derive<CoinType>();
|
||||
let type_info = type_of<CoinType>();
|
||||
|
||||
let state = borrow_global_mut<State>(@token_bridge);
|
||||
let native_infos = &mut state.native_infos;
|
||||
if (table::contains(native_infos, token_address)){
|
||||
//TODO: throw error, because we should only be able to set native asset type info once?
|
||||
table::remove(native_infos, token_address);
|
||||
};
|
||||
table::add(native_infos, token_address, type_info);
|
||||
}
|
||||
|
||||
public(friend) fun set_wrapped_asset_signer_capability(token: OriginInfo, signer_cap: SignerCapability) acquires State {
|
||||
let state = borrow_global_mut<State>(@token_bridge);
|
||||
let wrapped_info = WrappedInfo {
|
||||
type_info: option::none(),
|
||||
signer_cap
|
||||
};
|
||||
table::add(&mut state.wrapped_infos, token, wrapped_info);
|
||||
}
|
||||
|
||||
public(friend) fun get_wrapped_asset_signer(origin_info: OriginInfo): signer acquires State {
|
||||
let wrapped_coin_signer_caps
|
||||
= &borrow_global<State>(@token_bridge).wrapped_infos;
|
||||
let wrapped_info = table::borrow(wrapped_coin_signer_caps, origin_info);
|
||||
account::create_signer_with_capability(&wrapped_info.signer_cap)
|
||||
}
|
||||
|
||||
public(friend) fun init_token_bridge_state(
|
||||
signer_cap: SignerCapability,
|
||||
emitter_cap: EmitterCapability
|
||||
) {
|
||||
let token_bridge = account::create_signer_with_capability(&signer_cap);
|
||||
move_to(&token_bridge, State {
|
||||
governance_chain_id: state::get_chain_id(),
|
||||
governance_contract: state::get_governance_contract(),
|
||||
consumed_vaas: set::new<vector<u8>>(),
|
||||
wrapped_infos: table::new(),
|
||||
native_infos: table::new(),
|
||||
signer_cap: signer_cap,
|
||||
emitter_cap: emitter_cap,
|
||||
registered_emitters: table::new(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
module token_bridge::asset_meta {
|
||||
use std::vector::{Self};
|
||||
use wormhole::serialize::{serialize_u8, serialize_u16, serialize_vector};
|
||||
use wormhole::deserialize::{deserialize_u8, deserialize_u16, deserialize_vector};
|
||||
use wormhole::cursor::{Self};
|
||||
|
||||
use wormhole::u16::{U16};
|
||||
use wormhole::external_address::{Self, ExternalAddress};
|
||||
|
||||
use token_bridge::string32::{Self, String32};
|
||||
|
||||
friend token_bridge::attest_token;
|
||||
friend token_bridge::wrapped;
|
||||
|
||||
#[test_only]
|
||||
friend token_bridge::wrapped_test;
|
||||
|
||||
const E_INVALID_ACTION: u64 = 0;
|
||||
|
||||
struct AssetMeta has key, store, drop {
|
||||
/// Address of the token. Left-zero-padded if shorter than 32 bytes
|
||||
token_address: ExternalAddress,
|
||||
/// Chain ID of the token
|
||||
token_chain: U16,
|
||||
/// Number of decimals of the token (big-endian uint256)
|
||||
decimals: u8,
|
||||
/// Symbol of the token (UTF-8)
|
||||
symbol: String32,
|
||||
/// Name of the token (UTF-8)
|
||||
name: String32,
|
||||
}
|
||||
|
||||
public fun get_token_address(a: &AssetMeta): ExternalAddress {
|
||||
a.token_address
|
||||
}
|
||||
|
||||
public fun get_token_chain(a: &AssetMeta): U16 {
|
||||
a.token_chain
|
||||
}
|
||||
|
||||
public fun get_decimals(a: &AssetMeta): u8 {
|
||||
a.decimals
|
||||
}
|
||||
|
||||
public fun get_symbol(a: &AssetMeta): String32 {
|
||||
a.symbol
|
||||
}
|
||||
|
||||
public fun get_name(a: &AssetMeta): String32 {
|
||||
a.name
|
||||
}
|
||||
|
||||
public(friend) fun create(
|
||||
token_address: ExternalAddress,
|
||||
token_chain: U16,
|
||||
decimals: u8,
|
||||
symbol: String32,
|
||||
name: String32,
|
||||
): AssetMeta {
|
||||
AssetMeta {
|
||||
token_address,
|
||||
token_chain,
|
||||
decimals,
|
||||
symbol,
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
public fun encode(meta: AssetMeta): vector<u8> {
|
||||
let encoded = vector::empty<u8>();
|
||||
serialize_u8(&mut encoded, 2);
|
||||
serialize_vector(&mut encoded, external_address::get_bytes(&meta.token_address));
|
||||
serialize_u16(&mut encoded, meta.token_chain);
|
||||
serialize_u8(&mut encoded, meta.decimals);
|
||||
string32::serialize(&mut encoded, meta.symbol);
|
||||
string32::serialize(&mut encoded, meta.name);
|
||||
encoded
|
||||
}
|
||||
|
||||
public fun parse(meta: vector<u8>): AssetMeta {
|
||||
let cur = cursor::init(meta);
|
||||
let action = deserialize_u8(&mut cur);
|
||||
assert!(action == 2, E_INVALID_ACTION);
|
||||
let token_address = deserialize_vector(&mut cur, 32);
|
||||
let token_chain = deserialize_u16(&mut cur);
|
||||
let decimals = deserialize_u8(&mut cur);
|
||||
let symbol = string32::deserialize(&mut cur);
|
||||
let name = string32::deserialize(&mut cur);
|
||||
cursor::destroy_empty(cur);
|
||||
AssetMeta {
|
||||
token_address: external_address::from_bytes(token_address),
|
||||
token_chain,
|
||||
decimals,
|
||||
symbol,
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
// Construct a seed using AssetMeta fields for creating a new resource account
|
||||
// N.B. seed is product of coin native chain and native address
|
||||
// TODO(csongor): technically this only requires the OriginInfo, so we could
|
||||
// perhaps make this a function of that instead of the whole AssetMeta.
|
||||
public(friend) fun create_seed(asset_meta: &AssetMeta): vector<u8>{
|
||||
let token_chain = get_token_chain(asset_meta);
|
||||
let token_address = get_token_address(asset_meta);
|
||||
let seed = vector::empty<u8>();
|
||||
serialize_u16(&mut seed, token_chain);
|
||||
// TODO(csongor): why do we need '::' here? The seed is binary anyway,
|
||||
// but appending '::' suggests that it might be ASCII, which is
|
||||
// confusing. We should either make it ASCII, or just drop these
|
||||
// characters.
|
||||
serialize_vector(&mut seed, b"::");
|
||||
external_address::serialize(&mut seed, token_address);
|
||||
seed
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
module token_bridge::transfer {
|
||||
use std::vector;
|
||||
use wormhole::serialize::{
|
||||
serialize_u8,
|
||||
serialize_u16,
|
||||
};
|
||||
use wormhole::deserialize::{
|
||||
deserialize_u8,
|
||||
deserialize_u16,
|
||||
};
|
||||
use wormhole::cursor;
|
||||
use wormhole::external_address::{Self, ExternalAddress};
|
||||
use wormhole::u16::U16;
|
||||
|
||||
use token_bridge::normalized_amount::{Self, NormalizedAmount};
|
||||
|
||||
friend token_bridge::transfer_tokens;
|
||||
|
||||
#[test_only]
|
||||
friend token_bridge::complete_transfer_test;
|
||||
#[test_only]
|
||||
friend token_bridge::transfer_test;
|
||||
|
||||
const E_INVALID_ACTION: u64 = 0;
|
||||
|
||||
struct Transfer has drop {
|
||||
/// Amount being transferred
|
||||
amount: NormalizedAmount,
|
||||
/// Address of the token. Left-zero-padded if shorter than 32 bytes
|
||||
token_address: ExternalAddress,
|
||||
/// Chain ID of the token
|
||||
token_chain: U16,
|
||||
/// Address of the recipient. Left-zero-padded if shorter than 32 bytes
|
||||
to: ExternalAddress,
|
||||
/// Chain ID of the recipient
|
||||
to_chain: U16,
|
||||
/// Amount of tokens that the user is willing to pay as relayer fee. Must be <= Amount.
|
||||
fee: NormalizedAmount,
|
||||
}
|
||||
|
||||
public fun get_amount(a: &Transfer): NormalizedAmount {
|
||||
a.amount
|
||||
}
|
||||
|
||||
public fun get_token_address(a: &Transfer): ExternalAddress {
|
||||
a.token_address
|
||||
}
|
||||
|
||||
public fun get_token_chain(a: &Transfer): U16 {
|
||||
a.token_chain
|
||||
}
|
||||
|
||||
public fun get_to(a: &Transfer): ExternalAddress {
|
||||
a.to
|
||||
}
|
||||
|
||||
public fun get_to_chain(a: &Transfer): U16 {
|
||||
a.to_chain
|
||||
}
|
||||
|
||||
public fun get_fee(a: &Transfer): NormalizedAmount {
|
||||
a.fee
|
||||
}
|
||||
|
||||
public(friend) fun create(
|
||||
amount: NormalizedAmount,
|
||||
token_address: ExternalAddress,
|
||||
token_chain: U16,
|
||||
to: ExternalAddress,
|
||||
to_chain: U16,
|
||||
fee: NormalizedAmount,
|
||||
): Transfer {
|
||||
Transfer {
|
||||
amount,
|
||||
token_address,
|
||||
token_chain,
|
||||
to,
|
||||
to_chain,
|
||||
fee,
|
||||
}
|
||||
}
|
||||
|
||||
public fun parse(transfer: vector<u8>): Transfer {
|
||||
let cur = cursor::init(transfer);
|
||||
let action = deserialize_u8(&mut cur);
|
||||
assert!(action == 1, E_INVALID_ACTION);
|
||||
let amount = normalized_amount::deserialize(&mut cur);
|
||||
let token_address = external_address::deserialize(&mut cur);
|
||||
let token_chain = deserialize_u16(&mut cur);
|
||||
let to = external_address::deserialize(&mut cur);
|
||||
let to_chain = deserialize_u16(&mut cur);
|
||||
let fee = normalized_amount::deserialize(&mut cur);
|
||||
cursor::destroy_empty(cur);
|
||||
Transfer {
|
||||
amount,
|
||||
token_address,
|
||||
token_chain,
|
||||
to,
|
||||
to_chain,
|
||||
fee,
|
||||
}
|
||||
}
|
||||
|
||||
public fun encode(transfer: Transfer): vector<u8> {
|
||||
let encoded = vector::empty<u8>();
|
||||
serialize_u8(&mut encoded, 1);
|
||||
normalized_amount::serialize(&mut encoded, transfer.amount);
|
||||
external_address::serialize(&mut encoded, transfer.token_address);
|
||||
serialize_u16(&mut encoded, transfer.token_chain);
|
||||
external_address::serialize(&mut encoded, transfer.to);
|
||||
serialize_u16(&mut encoded, transfer.to_chain);
|
||||
normalized_amount::serialize(&mut encoded, transfer.fee);
|
||||
encoded
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::transfer_test {
|
||||
use token_bridge::transfer;
|
||||
use token_bridge::normalized_amount;
|
||||
use wormhole::external_address;
|
||||
use wormhole::u16;
|
||||
|
||||
#[test]
|
||||
public fun parse_roundtrip() {
|
||||
let amount = normalized_amount::normalize(100, 8);
|
||||
let token_address = external_address::from_bytes(x"beef");
|
||||
let token_chain = u16::from_u64(1);
|
||||
let to = external_address::from_bytes(x"cafe");
|
||||
let to_chain = u16::from_u64(7);
|
||||
let fee = normalized_amount::normalize(50, 8);
|
||||
let transfer = transfer::create(
|
||||
amount,
|
||||
token_address,
|
||||
token_chain,
|
||||
to,
|
||||
to_chain,
|
||||
fee,
|
||||
);
|
||||
let transfer = transfer::parse(transfer::encode(transfer));
|
||||
assert!(transfer::get_amount(&transfer) == amount, 0);
|
||||
assert!(transfer::get_token_address(&transfer) == token_address, 0);
|
||||
assert!(transfer::get_token_chain(&transfer) == token_chain, 0);
|
||||
assert!(transfer::get_to(&transfer) == to, 0);
|
||||
assert!(transfer::get_to_chain(&transfer) == to_chain, 0);
|
||||
assert!(transfer::get_fee(&transfer) == fee, 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
module token_bridge::transfer_result {
|
||||
use wormhole::u16::U16;
|
||||
use wormhole::external_address::ExternalAddress;
|
||||
|
||||
use token_bridge::normalized_amount::NormalizedAmount;
|
||||
|
||||
friend token_bridge::transfer_tokens;
|
||||
|
||||
struct TransferResult {
|
||||
/// Chain ID of the token
|
||||
token_chain: U16,
|
||||
/// Address of the token. Left-zero-padded if shorter than 32 bytes
|
||||
token_address: ExternalAddress,
|
||||
/// Amount being transferred
|
||||
normalized_amount: NormalizedAmount,
|
||||
/// Amount of tokens that the user is willing to pay as relayer fee. Must be <= Amount.
|
||||
normalized_relayer_fee: NormalizedAmount,
|
||||
}
|
||||
|
||||
public fun destroy(a: TransferResult): (U16, ExternalAddress, NormalizedAmount, NormalizedAmount) {
|
||||
let TransferResult {
|
||||
token_chain,
|
||||
token_address,
|
||||
normalized_amount,
|
||||
normalized_relayer_fee
|
||||
} = a;
|
||||
(token_chain, token_address, normalized_amount, normalized_relayer_fee)
|
||||
}
|
||||
|
||||
public(friend) fun create(
|
||||
token_chain: U16,
|
||||
token_address: ExternalAddress,
|
||||
normalized_amount: NormalizedAmount,
|
||||
normalized_relayer_fee: NormalizedAmount,
|
||||
): TransferResult {
|
||||
TransferResult {
|
||||
token_chain,
|
||||
token_address,
|
||||
normalized_amount,
|
||||
normalized_relayer_fee,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
module token_bridge::transfer_with_payload {
|
||||
use std::vector;
|
||||
use wormhole::serialize::{
|
||||
serialize_u8,
|
||||
serialize_u16,
|
||||
serialize_vector,
|
||||
};
|
||||
use wormhole::deserialize::{
|
||||
deserialize_u8,
|
||||
deserialize_u16,
|
||||
};
|
||||
use wormhole::cursor;
|
||||
|
||||
use wormhole::u16::U16;
|
||||
use wormhole::external_address::{Self, ExternalAddress};
|
||||
|
||||
use token_bridge::normalized_amount::{Self, NormalizedAmount};
|
||||
|
||||
friend token_bridge::transfer_tokens;
|
||||
|
||||
const E_INVALID_ACTION: u64 = 0;
|
||||
|
||||
struct TransferWithPayload has key, store, drop {
|
||||
/// Amount being transferred (big-endian uint256)
|
||||
amount: NormalizedAmount,
|
||||
/// Address of the token. Left-zero-padded if shorter than 32 bytes
|
||||
token_address: ExternalAddress,
|
||||
/// Chain ID of the token
|
||||
token_chain: U16,
|
||||
/// Address of the recipient. Left-zero-padded if shorter than 32 bytes
|
||||
to: ExternalAddress,
|
||||
/// Chain ID of the recipient
|
||||
to_chain: U16,
|
||||
/// Address of the message sender. Left-zero-padded if shorter than 32 bytes
|
||||
from_address: ExternalAddress,
|
||||
/// An arbitrary payload
|
||||
payload: vector<u8>,
|
||||
}
|
||||
|
||||
public fun get_amount(a: &TransferWithPayload): NormalizedAmount {
|
||||
a.amount
|
||||
}
|
||||
|
||||
public fun get_token_address(a: &TransferWithPayload): ExternalAddress {
|
||||
a.token_address
|
||||
}
|
||||
|
||||
public fun get_token_chain(a: &TransferWithPayload): U16 {
|
||||
a.token_chain
|
||||
}
|
||||
|
||||
public fun get_to(a: &TransferWithPayload): ExternalAddress {
|
||||
a.to
|
||||
}
|
||||
|
||||
public fun get_to_chain(a: &TransferWithPayload): U16 {
|
||||
a.to_chain
|
||||
}
|
||||
|
||||
public fun get_from_address(a: &TransferWithPayload): ExternalAddress {
|
||||
a.from_address
|
||||
}
|
||||
|
||||
public fun get_payload(a: &TransferWithPayload): vector<u8> {
|
||||
a.payload
|
||||
}
|
||||
|
||||
public(friend) fun create(
|
||||
amount: NormalizedAmount,
|
||||
token_address: ExternalAddress,
|
||||
token_chain: U16,
|
||||
to: ExternalAddress,
|
||||
to_chain: U16,
|
||||
from_address: ExternalAddress,
|
||||
payload: vector<u8>
|
||||
): TransferWithPayload {
|
||||
TransferWithPayload {
|
||||
amount,
|
||||
token_address,
|
||||
token_chain,
|
||||
to,
|
||||
to_chain,
|
||||
from_address,
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
public fun encode(transfer: TransferWithPayload): vector<u8> {
|
||||
let encoded = vector::empty<u8>();
|
||||
serialize_u8(&mut encoded, 3);
|
||||
normalized_amount::serialize(&mut encoded, transfer.amount);
|
||||
external_address::serialize(&mut encoded, transfer.token_address);
|
||||
serialize_u16(&mut encoded, transfer.token_chain);
|
||||
external_address::serialize(&mut encoded, transfer.to);
|
||||
serialize_u16(&mut encoded, transfer.to_chain);
|
||||
external_address::serialize(&mut encoded, transfer.from_address);
|
||||
serialize_vector(&mut encoded, transfer.payload);
|
||||
encoded
|
||||
}
|
||||
|
||||
public fun parse(transfer: vector<u8>): TransferWithPayload {
|
||||
let cur = cursor::init(transfer);
|
||||
let action = deserialize_u8(&mut cur);
|
||||
assert!(action == 3, E_INVALID_ACTION);
|
||||
let amount = normalized_amount::deserialize(&mut cur);
|
||||
let token_address = external_address::deserialize(&mut cur);
|
||||
let token_chain = deserialize_u16(&mut cur);
|
||||
let to = external_address::deserialize(&mut cur);
|
||||
let to_chain = deserialize_u16(&mut cur);
|
||||
let from_address = external_address::deserialize(&mut cur);
|
||||
let payload = cursor::rest(cur);
|
||||
TransferWithPayload {
|
||||
amount,
|
||||
token_address,
|
||||
token_chain,
|
||||
to,
|
||||
to_chain,
|
||||
from_address,
|
||||
payload
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
module token_bridge::token_bridge {
|
||||
#[test_only]
|
||||
use aptos_framework::account::{Self};
|
||||
use aptos_framework::account::{SignerCapability};
|
||||
use deployer::deployer::{claim_signer_capability};
|
||||
use token_bridge::state::{init_token_bridge_state};
|
||||
use wormhole::wormhole;
|
||||
|
||||
/// Initializes the contract.
|
||||
/// The native `init_module` cannot be used, because it runs on each upgrade
|
||||
/// (oddly).
|
||||
/// TODO: the above behaviour has been remedied in the Aptos VM, so we could
|
||||
/// use `init_module` now. Let's reconsider before the mainnet launch.
|
||||
/// Can only be called by the deployer (checked by the
|
||||
/// `deployer::claim_signer_capability` function).
|
||||
public entry fun init(deployer: &signer) {
|
||||
let signer_cap = claim_signer_capability(deployer, @token_bridge);
|
||||
init_internal(signer_cap);
|
||||
}
|
||||
|
||||
fun init_internal(signer_cap: SignerCapability){
|
||||
let emitter_cap = wormhole::register_emitter();
|
||||
init_token_bridge_state(signer_cap, emitter_cap);
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
/// Initialise contracts for testing
|
||||
/// Returns the token_bridge signer and wormhole signer
|
||||
public fun init_test(deployer: &signer) {
|
||||
let (_token_bridge, signer_cap) = account::create_resource_account(deployer, b"token_bridge");
|
||||
init_internal(signer_cap);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/// 32 byte hash representing an arbitrary Aptos token, to be used in VAAs to
|
||||
/// refer to coins.
|
||||
module token_bridge::token_hash {
|
||||
use aptos_framework::type_info;
|
||||
use std::hash;
|
||||
use std::string;
|
||||
|
||||
use wormhole::external_address::{Self, ExternalAddress};
|
||||
|
||||
struct TokenHash has drop, copy, store {
|
||||
// 32 bytes
|
||||
hash: vector<u8>,
|
||||
}
|
||||
|
||||
public fun get_external_address(a: &TokenHash): ExternalAddress {
|
||||
external_address::from_bytes(a.hash)
|
||||
}
|
||||
|
||||
/// Get the 32 token address of an arbitary CoinType
|
||||
public fun derive<CoinType>(): TokenHash {
|
||||
let type_name = type_info::type_name<CoinType>();
|
||||
let hash = hash::sha3_256(*string::bytes(&type_name));
|
||||
TokenHash { hash }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::token_hash_test {
|
||||
use token_bridge::token_hash;
|
||||
use wormhole::external_address;
|
||||
use wrapped_coin::coin;
|
||||
|
||||
use std::type_info;
|
||||
use std::string;
|
||||
|
||||
struct MyCoin {}
|
||||
|
||||
#[test]
|
||||
public fun test_type_name() {
|
||||
let t = type_info::type_name<MyCoin>();
|
||||
assert!(*string::bytes(&t) == b"0x84a5f374d29fc77e370014dce4fd6a55b58ad608de8074b0be5571701724da31::token_hash_test::MyCoin", 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
public fun test_derive() {
|
||||
let t = token_hash::derive<MyCoin>();
|
||||
let expected = x"4f69c5d0be57aee780277b1179e4833d61f0563869145e971d24a4e49fcd9302";
|
||||
assert!(token_hash::get_external_address(&t) == external_address::from_bytes(expected), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
public fun test_type_name_T() {
|
||||
let t = type_info::type_name<coin::T>();
|
||||
assert!(*string::bytes(&t) == b"0xf4f53cc591e5190eddbc43940746e2b5deea6e0e1562b2bba765d488504842c7::coin::T", 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
public fun test_derive_T() {
|
||||
let t = token_hash::derive<coin::T>();
|
||||
let expected = x"f0dcbf26a2d59b2196630ed6d5fb5c5bc4fd33996c9f31f19d29389d0c8e7ec2";
|
||||
assert!(token_hash::get_external_address(&t) == external_address::from_bytes(expected), 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,286 @@
|
|||
module token_bridge::transfer_tokens {
|
||||
use aptos_framework::aptos_coin::{AptosCoin};
|
||||
use aptos_framework::coin::{Self, Coin};
|
||||
|
||||
use wormhole::u16::{Self, U16};
|
||||
use wormhole::external_address::{Self, ExternalAddress};
|
||||
use wormhole::emitter::{Self, EmitterCapability};
|
||||
|
||||
use token_bridge::state;
|
||||
use token_bridge::transfer;
|
||||
use token_bridge::transfer_result::{Self, TransferResult};
|
||||
use token_bridge::transfer_with_payload;
|
||||
use token_bridge::normalized_amount;
|
||||
use token_bridge::wrapped;
|
||||
|
||||
const E_TOO_MUCH_RELAYER_FEE: u64 = 0;
|
||||
|
||||
public entry fun transfer_tokens_with_signer<CoinType>(
|
||||
sender: &signer,
|
||||
amount: u64,
|
||||
recipient_chain: u64,
|
||||
recipient: vector<u8>,
|
||||
relayer_fee: u64,
|
||||
wormhole_fee: u64,
|
||||
nonce: u64
|
||||
): u64 {
|
||||
let coins = coin::withdraw<CoinType>(sender, amount);
|
||||
let wormhole_fee_coins = coin::withdraw<AptosCoin>(sender, wormhole_fee);
|
||||
transfer_tokens<CoinType>(
|
||||
coins,
|
||||
wormhole_fee_coins,
|
||||
u16::from_u64(recipient_chain),
|
||||
external_address::from_bytes(recipient),
|
||||
relayer_fee,
|
||||
nonce
|
||||
)
|
||||
}
|
||||
|
||||
public fun transfer_tokens<CoinType>(
|
||||
coins: Coin<CoinType>,
|
||||
wormhole_fee_coins: Coin<AptosCoin>,
|
||||
recipient_chain: U16,
|
||||
recipient: ExternalAddress,
|
||||
relayer_fee: u64,
|
||||
nonce: u64
|
||||
): u64 {
|
||||
let result = transfer_tokens_internal<CoinType>(coins, relayer_fee);
|
||||
let (token_chain, token_address, normalized_amount, normalized_relayer_fee)
|
||||
= transfer_result::destroy(result);
|
||||
let transfer = transfer::create(
|
||||
normalized_amount,
|
||||
token_address,
|
||||
token_chain,
|
||||
recipient,
|
||||
recipient_chain,
|
||||
normalized_relayer_fee,
|
||||
);
|
||||
state::publish_message(
|
||||
nonce,
|
||||
transfer::encode(transfer),
|
||||
wormhole_fee_coins,
|
||||
)
|
||||
}
|
||||
|
||||
public fun transfer_tokens_with_payload<CoinType>(
|
||||
emitter_cap: &EmitterCapability,
|
||||
coins: Coin<CoinType>,
|
||||
wormhole_fee_coins: Coin<AptosCoin>,
|
||||
recipient_chain: U16,
|
||||
recipient: ExternalAddress,
|
||||
nonce: u64,
|
||||
payload: vector<u8>
|
||||
): u64 {
|
||||
let result = transfer_tokens_internal<CoinType>(coins, 0);
|
||||
let (token_chain, token_address, normalized_amount, _)
|
||||
= transfer_result::destroy(result);
|
||||
|
||||
let transfer = transfer_with_payload::create(
|
||||
normalized_amount,
|
||||
token_address,
|
||||
token_chain,
|
||||
recipient,
|
||||
recipient_chain,
|
||||
emitter::get_external_address(emitter_cap),
|
||||
payload
|
||||
);
|
||||
let payload = transfer_with_payload::encode(transfer);
|
||||
state::publish_message(
|
||||
nonce,
|
||||
payload,
|
||||
wormhole_fee_coins,
|
||||
)
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun transfer_tokens_test<CoinType>(
|
||||
coins: Coin<CoinType>,
|
||||
relayer_fee: u64,
|
||||
): TransferResult {
|
||||
transfer_tokens_internal(coins, relayer_fee)
|
||||
}
|
||||
|
||||
// transfer a native or wraped token from sender to token_bridge
|
||||
fun transfer_tokens_internal<CoinType>(
|
||||
coins: Coin<CoinType>,
|
||||
relayer_fee: u64,
|
||||
): TransferResult {
|
||||
|
||||
// transfer coin to token_bridge
|
||||
if (!coin::is_account_registered<CoinType>(@token_bridge)){
|
||||
coin::register<CoinType>(&state::token_bridge_signer());
|
||||
};
|
||||
if (!coin::is_account_registered<AptosCoin>(@token_bridge)){
|
||||
coin::register<AptosCoin>(&state::token_bridge_signer());
|
||||
};
|
||||
|
||||
let amount = coin::value<CoinType>(&coins);
|
||||
assert!(relayer_fee <= amount, E_TOO_MUCH_RELAYER_FEE);
|
||||
|
||||
if (state::is_wrapped_asset<CoinType>()) {
|
||||
// now we burn the wrapped coins to remove them from circulation
|
||||
wrapped::burn<CoinType>(coins);
|
||||
} else {
|
||||
coin::deposit<CoinType>(@token_bridge, coins);
|
||||
// if we're seeing this native token for the first time, store its
|
||||
// type info
|
||||
if (!state::is_registered_native_asset<CoinType>()) {
|
||||
state::set_native_asset_type_info<CoinType>();
|
||||
};
|
||||
};
|
||||
|
||||
let origin_info = state::origin_info<CoinType>();
|
||||
let token_chain = state::get_origin_info_token_chain(&origin_info);
|
||||
let token_address = state::get_origin_info_token_address(&origin_info);
|
||||
|
||||
let decimals_token = coin::decimals<CoinType>();
|
||||
|
||||
let normalized_amount = normalized_amount::normalize(amount, decimals_token);
|
||||
let normalized_relayer_fee = normalized_amount::normalize(relayer_fee, decimals_token);
|
||||
|
||||
let transfer_result: TransferResult = transfer_result::create(
|
||||
token_chain,
|
||||
token_address,
|
||||
normalized_amount,
|
||||
normalized_relayer_fee,
|
||||
);
|
||||
transfer_result
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::transfer_tokens_test {
|
||||
use aptos_framework::coin::{Self, Coin};
|
||||
use aptos_framework::string::{utf8};
|
||||
use aptos_framework::aptos_coin::{Self, AptosCoin};
|
||||
|
||||
use token_bridge::token_bridge::{Self as bridge};
|
||||
use token_bridge::transfer_tokens;
|
||||
use token_bridge::wrapped;
|
||||
use token_bridge::transfer_result;
|
||||
use token_bridge::token_hash;
|
||||
use token_bridge::register_chain;
|
||||
use token_bridge::normalized_amount;
|
||||
|
||||
use wormhole::external_address::{Self};
|
||||
|
||||
use wrapped_coin::coin::T;
|
||||
|
||||
/// Registration VAA for the etheruem token bridge 0xdeadbeef
|
||||
const ETHEREUM_TOKEN_REG: vector<u8> = x"0100000000010015d405c74be6d93c3c33ed6b48d8db70dfb31e0981f8098b2a6c7583083e0c3343d4a1abeb3fc1559674fa067b0c0e2e9de2fafeaecdfeae132de2c33c9d27cc0100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000016911ae00000000000000000000000000000000000000000000546f6b656e427269646765010000000200000000000000000000000000000000000000000000000000000000deadbeef";
|
||||
|
||||
/// Attestation VAA sent from the ethereum token bridge 0xdeadbeef
|
||||
const ATTESTATION_VAA: vector<u8> = x"0100000000010080366065746148420220f25a6275097370e8db40984529a6676b7a5fc9feb11755ec49ca626b858ddfde88d15601f85ab7683c5f161413b0412143241c700aff010000000100000001000200000000000000000000000000000000000000000000000000000000deadbeef000000000150eb23000200000000000000000000000000000000000000000000000000000000beefface00020c424545460000000000000000000000000000000000000000000000000000000042656566206661636520546f6b656e0000000000000000000000000000000000";
|
||||
|
||||
struct MyCoin has key {}
|
||||
|
||||
fun init_my_token(admin: &signer, amount: u64): Coin<MyCoin> {
|
||||
let name = utf8(b"mycoindd");
|
||||
let symbol = utf8(b"MCdd");
|
||||
let decimals = 6;
|
||||
let monitor_supply = true;
|
||||
let (burn_cap, freeze_cap, mint_cap) = coin::initialize<MyCoin>(admin, name, symbol, decimals, monitor_supply);
|
||||
let coins = coin::mint<MyCoin>(amount, &mint_cap);
|
||||
coin::destroy_burn_cap(burn_cap);
|
||||
coin::destroy_mint_cap(mint_cap);
|
||||
coin::destroy_freeze_cap(freeze_cap);
|
||||
coins
|
||||
}
|
||||
|
||||
fun setup(
|
||||
aptos_framework: &signer,
|
||||
token_bridge: &signer,
|
||||
deployer: &signer,
|
||||
) {
|
||||
// we initialise the bridge with zero fees to avoid having to mint fee
|
||||
// tokens in these tests. The wormolhe fee handling is already tested
|
||||
// in wormhole.move, so it's unnecessary here.
|
||||
let (burn_cap, mint_cap) = aptos_coin::initialize_for_test(aptos_framework);
|
||||
wormhole::wormhole_test::setup(0);
|
||||
bridge::init_test(deployer);
|
||||
|
||||
coin::register<AptosCoin>(deployer);
|
||||
coin::register<AptosCoin>(token_bridge); //how important is this registration step and where to check it?
|
||||
coin::destroy_burn_cap(burn_cap);
|
||||
coin::destroy_mint_cap(mint_cap);
|
||||
}
|
||||
|
||||
// test transfer wrapped coin
|
||||
#[test(aptos_framework = @aptos_framework, token_bridge=@token_bridge, deployer=@deployer)]
|
||||
fun test_transfer_wrapped_token(aptos_framework: &signer, token_bridge: &signer, deployer: &signer) {
|
||||
setup(aptos_framework, token_bridge, deployer);
|
||||
register_chain::submit_vaa(ETHEREUM_TOKEN_REG);
|
||||
// TODO(csongor): create a better error message when attestation is missing
|
||||
wrapped::create_wrapped_coin_type(ATTESTATION_VAA);
|
||||
// TODO(csongor): write a blurb about why this test works (something
|
||||
// something static linking)
|
||||
// initialize coin using type T, move caps to token_bridge, sets bridge state variables
|
||||
wrapped::create_wrapped_coin<T>(ATTESTATION_VAA);
|
||||
|
||||
// test transfer wrapped tokens
|
||||
let beef_coins = wrapped::mint<T>(100000);
|
||||
assert!(coin::supply<T>() == std::option::some(100000), 0);
|
||||
let result = transfer_tokens::transfer_tokens_test<T>(
|
||||
beef_coins,
|
||||
2,
|
||||
);
|
||||
let (token_chain, token_address, normalized_amount, normalized_relayer_fee)
|
||||
= transfer_result::destroy(result);
|
||||
|
||||
// make sure the wrapped assets have been burned
|
||||
assert!(coin::supply<T>() == std::option::some(0), 0);
|
||||
|
||||
assert!(token_chain == wormhole::u16::from_u64(2), 0);
|
||||
assert!(external_address::get_bytes(&token_address) == x"00000000000000000000000000000000000000000000000000000000beefface", 0);
|
||||
// the coin has 12 decimals, so the amount gets scaled by a factor 10^-4
|
||||
// since the normalised amounts are 8 decimals
|
||||
assert!(normalized_amount::get_amount(normalized_amount) == 10, 0);
|
||||
assert!(normalized_amount::get_amount(normalized_relayer_fee) == 0, 0);
|
||||
}
|
||||
|
||||
#[test(aptos_framework = @aptos_framework, token_bridge=@token_bridge, deployer=@deployer)]
|
||||
#[expected_failure(abort_code = 0)]
|
||||
fun test_transfer_wrapped_token_too_much_relayer_fee(
|
||||
aptos_framework: &signer,
|
||||
token_bridge: &signer,
|
||||
deployer: &signer
|
||||
) {
|
||||
setup(aptos_framework, token_bridge, deployer);
|
||||
register_chain::submit_vaa(ETHEREUM_TOKEN_REG);
|
||||
wrapped::create_wrapped_coin_type(ATTESTATION_VAA);
|
||||
wrapped::create_wrapped_coin<T>(ATTESTATION_VAA);
|
||||
|
||||
// this will fail because the relayer fee exceeds the amount
|
||||
let beef_coins = wrapped::mint<T>(100000);
|
||||
assert!(coin::supply<T>() == std::option::some(100000), 0);
|
||||
let result = transfer_tokens::transfer_tokens_test<T>(beef_coins, 200000);
|
||||
let (_, _, _, _) = transfer_result::destroy(result);
|
||||
}
|
||||
|
||||
// test transfer native coin
|
||||
#[test(aptos_framework = @aptos_framework, token_bridge=@token_bridge, deployer=@deployer)]
|
||||
fun test_transfer_native_token(aptos_framework: &signer, token_bridge: &signer, deployer: &signer) {
|
||||
setup(aptos_framework, token_bridge, deployer);
|
||||
|
||||
let my_coins = init_my_token(token_bridge, 10000);
|
||||
|
||||
// make sure the token bridge is not registered yet for this coin
|
||||
assert!(!coin::is_account_registered<MyCoin>(@token_bridge), 0);
|
||||
|
||||
let result = transfer_tokens::transfer_tokens_test<MyCoin>(my_coins, 500);
|
||||
|
||||
// the token bridge should now be registered and hold the balance
|
||||
assert!(coin::balance<MyCoin>(@token_bridge) == 10000, 0);
|
||||
|
||||
let (token_chain, token_address, normalized_amount, normalized_relayer_fee)
|
||||
= transfer_result::destroy(result);
|
||||
|
||||
assert!(token_chain == wormhole::state::get_chain_id(), 0);
|
||||
assert!(token_address == token_hash::get_external_address(&token_hash::derive<MyCoin>()), 0);
|
||||
// the coin has 6 decimals, so the amount doesn't get scaled
|
||||
assert!(normalized_amount::get_amount(normalized_amount) == 10000, 0);
|
||||
assert!(normalized_amount::get_amount(normalized_relayer_fee) == 500, 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
/// Token Bridge VAA utilities
|
||||
module token_bridge::vaa {
|
||||
use std::option;
|
||||
use wormhole::vaa::{Self, VAA};
|
||||
use token_bridge::state;
|
||||
|
||||
friend token_bridge::complete_transfer;
|
||||
friend token_bridge::complete_transfer_with_payload;
|
||||
friend token_bridge::contract_upgrade;
|
||||
friend token_bridge::register_chain;
|
||||
friend token_bridge::wrapped;
|
||||
|
||||
#[test_only]
|
||||
friend token_bridge::vaa_test;
|
||||
|
||||
/// We have no registration for this chain
|
||||
const E_UNKNOWN_CHAIN: u64 = 0;
|
||||
/// We have a registration, but it's different from what's given
|
||||
const E_UNKNOWN_EMITTER: u64 = 1;
|
||||
|
||||
/// Aborts if the VAA has already been consumed. Marks the VAA as consumed
|
||||
/// the first time around.
|
||||
public(friend) fun replay_protect(vaa: &VAA) {
|
||||
// this calls set::add which aborts if the element already exists
|
||||
state::set_vaa_consumed(vaa::get_hash(vaa));
|
||||
}
|
||||
|
||||
/// Asserts that the VAA is from a known token bridge.
|
||||
public fun assert_known_emitter(vm: &VAA) {
|
||||
let maybe_emitter = state::get_registered_emitter(vaa::get_emitter_chain(vm));
|
||||
assert!(option::is_some(&maybe_emitter), E_UNKNOWN_CHAIN);
|
||||
|
||||
let emitter = option::extract(&mut maybe_emitter);
|
||||
assert!(emitter == vaa::get_emitter_address(vm), E_UNKNOWN_EMITTER);
|
||||
}
|
||||
|
||||
/// Parses, verifies, and replay protects a token bridge VAA.
|
||||
/// Aborts if the VAA is not from a known token bridge emitter.
|
||||
///
|
||||
/// Has a 'friend' visibility so that it's only callable by the token bridge
|
||||
/// (otherwise the replay protection could be abused to DoS the bridge)
|
||||
public(friend) fun parse_verify_and_replay_protect(vaa: vector<u8>): VAA {
|
||||
let vaa = parse_and_verify(vaa);
|
||||
replay_protect(&vaa);
|
||||
vaa
|
||||
}
|
||||
|
||||
/// Parses, and verifies a token bridge VAA.
|
||||
/// Aborts if the VAA is not from a known token bridge emitter.
|
||||
public fun parse_and_verify(vaa: vector<u8>): VAA {
|
||||
let vaa = vaa::parse_and_verify(vaa);
|
||||
assert_known_emitter(&vaa);
|
||||
vaa
|
||||
}
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::vaa_test {
|
||||
use token_bridge::vaa;
|
||||
use token_bridge::state;
|
||||
use token_bridge::token_bridge;
|
||||
|
||||
use wormhole::vaa as core_vaa;
|
||||
use wormhole::wormhole;
|
||||
use wormhole::u16;
|
||||
use wormhole::external_address;
|
||||
|
||||
/// VAA sent from the ethereum token bridge 0xdeadbeef
|
||||
const VAA: vector<u8> = x"01000000000100102d399190fa61daccb11c2ea4f7a3db3a9365e5936bcda4cded87c1b9eeb095173514f226256d5579af71d4089eb89496befb998075ba94cd1d4460c5c57b84000000000100000001000200000000000000000000000000000000000000000000000000000000deadbeef0000000002634973000200000000000000000000000000000000000000000000000000000000beefface00020c0000000000000000000000000000000000000000000000000000000042454546000000000000000000000000000000000042656566206661636520546f6b656e";
|
||||
|
||||
fun setup(deployer: &signer) {
|
||||
let aptos_framework = std::account::create_account_for_test(@aptos_framework);
|
||||
std::timestamp::set_time_has_started_for_testing(&aptos_framework);
|
||||
wormhole::init_test(
|
||||
22,
|
||||
1,
|
||||
x"0000000000000000000000000000000000000000000000000000000000000004",
|
||||
x"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe",
|
||||
0
|
||||
);
|
||||
token_bridge::init_test(deployer);
|
||||
}
|
||||
|
||||
#[test(deployer = @deployer)]
|
||||
#[expected_failure(abort_code = 0)] // E_UNKNOWN_CHAIN
|
||||
public fun test_unknown_chain(deployer: &signer) {
|
||||
setup(deployer);
|
||||
let vaa = vaa::parse_verify_and_replay_protect(VAA);
|
||||
core_vaa::destroy(vaa);
|
||||
}
|
||||
|
||||
#[test(deployer = @deployer)]
|
||||
#[expected_failure(abort_code = 1)] // E_UNKNOWN_EMITTER
|
||||
public fun test_unknown_emitter(deployer: &signer) {
|
||||
setup(deployer);
|
||||
state::set_registered_emitter(
|
||||
u16::from_u64(2),
|
||||
external_address::from_bytes(x"deadbeed"), // not deadbeef
|
||||
);
|
||||
let vaa = vaa::parse_verify_and_replay_protect(VAA);
|
||||
core_vaa::destroy(vaa);
|
||||
}
|
||||
|
||||
#[test(deployer = @deployer)]
|
||||
public fun test_known_emitter(deployer: &signer) {
|
||||
setup(deployer);
|
||||
state::set_registered_emitter(
|
||||
u16::from_u64(2),
|
||||
external_address::from_bytes(x"deadbeef"),
|
||||
);
|
||||
let vaa = vaa::parse_verify_and_replay_protect(VAA);
|
||||
core_vaa::destroy(vaa);
|
||||
}
|
||||
|
||||
#[test(deployer = @deployer)]
|
||||
#[expected_failure(abort_code = 25607)] // add_box error
|
||||
public fun test_replay_protect(deployer: &signer) {
|
||||
setup(deployer);
|
||||
state::set_registered_emitter(
|
||||
u16::from_u64(2),
|
||||
external_address::from_bytes(x"deadbeef"),
|
||||
);
|
||||
let vaa = vaa::parse_verify_and_replay_protect(VAA);
|
||||
core_vaa::destroy(vaa);
|
||||
let vaa = vaa::parse_verify_and_replay_protect(VAA);
|
||||
core_vaa::destroy(vaa);
|
||||
}
|
||||
|
||||
#[test(deployer = @deployer)]
|
||||
public fun test_can_verify_after_replay_protect(deployer: &signer) {
|
||||
setup(deployer);
|
||||
state::set_registered_emitter(
|
||||
u16::from_u64(2),
|
||||
external_address::from_bytes(x"deadbeef"),
|
||||
);
|
||||
let vaa = vaa::parse_verify_and_replay_protect(VAA);
|
||||
core_vaa::destroy(vaa);
|
||||
let vaa = vaa::parse_and_verify(VAA);
|
||||
core_vaa::destroy(vaa);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,255 @@
|
|||
module token_bridge::wrapped {
|
||||
use aptos_framework::account;
|
||||
use aptos_framework::coin::{Self, Coin, MintCapability, BurnCapability, FreezeCapability};
|
||||
|
||||
use wormhole::vaa;
|
||||
|
||||
use token_bridge::state;
|
||||
use token_bridge::asset_meta::{Self, AssetMeta};
|
||||
use token_bridge::deploy_coin::{deploy_coin};
|
||||
use token_bridge::vaa as token_bridge_vaa;
|
||||
use token_bridge::string32;
|
||||
|
||||
friend token_bridge::complete_transfer;
|
||||
friend token_bridge::complete_transfer_with_payload;
|
||||
friend token_bridge::transfer_tokens;
|
||||
|
||||
#[test_only]
|
||||
friend token_bridge::transfer_tokens_test;
|
||||
#[test_only]
|
||||
friend token_bridge::wrapped_test;
|
||||
#[test_only]
|
||||
friend token_bridge::complete_transfer_test;
|
||||
|
||||
const E_IS_NOT_WRAPPED_ASSET: u64 = 0;
|
||||
const E_COIN_CAP_DOES_NOT_EXIST: u64 = 1;
|
||||
|
||||
struct CoinCapabilities<phantom CoinType> has key, store {
|
||||
mint_cap: MintCapability<CoinType>,
|
||||
freeze_cap: FreezeCapability<CoinType>,
|
||||
burn_cap: BurnCapability<CoinType>,
|
||||
}
|
||||
|
||||
// this function is called before create_wrapped_coin
|
||||
// TODO(csongor): document why these two are in separate transactions
|
||||
public entry fun create_wrapped_coin_type(vaa: vector<u8>) {
|
||||
// NOTE: we do not do replay protection here, only verify that the VAA
|
||||
// comes from a known emitter. This is because `create_wrapped_coin`
|
||||
// itself will need to verify the VAA again in a separate transaction,
|
||||
// and it itself will perform the replay protection.
|
||||
// This function cannot be called twice with the same VAA because it
|
||||
// creates a resource account, which will fail the second time if the
|
||||
// account already exists.
|
||||
// TODO(csongor): should we implement a more explicit replay protection
|
||||
// for this function?
|
||||
let vaa = token_bridge_vaa::parse_and_verify(vaa);
|
||||
let asset_meta = asset_meta::parse(vaa::destroy(vaa));
|
||||
let seed = asset_meta::create_seed(&asset_meta);
|
||||
|
||||
//create resource account
|
||||
let token_bridge_signer = state::token_bridge_signer();
|
||||
let (new_signer, new_cap) = account::create_resource_account(&token_bridge_signer, seed);
|
||||
|
||||
let token_address = asset_meta::get_token_address(&asset_meta);
|
||||
let token_chain = asset_meta::get_token_chain(&asset_meta);
|
||||
let origin_info = state::create_origin_info(token_chain, token_address);
|
||||
|
||||
deploy_coin(&new_signer);
|
||||
state::set_wrapped_asset_signer_capability(origin_info, new_cap);
|
||||
}
|
||||
|
||||
// this function is called in tandem with bridge_implementation::create_wrapped_coin_type
|
||||
// initializes a coin for CoinType, updates mappings in State
|
||||
public entry fun create_wrapped_coin<CoinType>(vaa: vector<u8>) {
|
||||
let vaa = token_bridge_vaa::parse_verify_and_replay_protect(vaa);
|
||||
let asset_meta: AssetMeta = asset_meta::parse(vaa::destroy(vaa));
|
||||
|
||||
let native_token_address = asset_meta::get_token_address(&asset_meta);
|
||||
let native_token_chain = asset_meta::get_token_chain(&asset_meta);
|
||||
let origin_info = state::create_origin_info(native_token_chain, native_token_address);
|
||||
|
||||
// The CoinType type variable is instantiated by the caller of the
|
||||
// function, so a malicious actor could try and pass in something other
|
||||
// than what we're expecting based on the VAA. So how do we protect
|
||||
// against this? The signer capability is keyed by the origin info of
|
||||
// the token, and a coin can only be initialised by the signer that owns
|
||||
// the module that defines the CoinType.
|
||||
// See the `test_create_wrapped_coin_bad_type` negative test below.
|
||||
let coin_signer = state::get_wrapped_asset_signer(origin_info);
|
||||
init_wrapped_coin<CoinType>(&coin_signer, &asset_meta)
|
||||
}
|
||||
|
||||
public(friend) fun init_wrapped_coin<CoinType>(
|
||||
coin_signer: &signer,
|
||||
asset_meta: &AssetMeta,
|
||||
) {
|
||||
// initialize new coin using CoinType
|
||||
let name = asset_meta::get_name(asset_meta);
|
||||
let symbol = asset_meta::get_symbol(asset_meta);
|
||||
let decimals = asset_meta::get_decimals(asset_meta);
|
||||
let monitor_supply = true;
|
||||
let (burn_cap, freeze_cap, mint_cap)
|
||||
= coin::initialize<CoinType>(
|
||||
coin_signer,
|
||||
string32::to_string(&name),
|
||||
// take the first 10 characters of the symbol (maximum in aptos)
|
||||
string32::take_utf8(string32::to_string(&symbol), 10),
|
||||
decimals,
|
||||
monitor_supply
|
||||
);
|
||||
|
||||
let token_address = asset_meta::get_token_address(asset_meta);
|
||||
let token_chain = asset_meta::get_token_chain(asset_meta);
|
||||
let origin_info = state::create_origin_info(token_chain, token_address);
|
||||
|
||||
// update the following two mappings in State
|
||||
// 1. (native chain, native address) => wrapped address
|
||||
// 2. wrapped address => (native chain, native address)
|
||||
state::setup_wrapped<CoinType>(origin_info);
|
||||
|
||||
// store coin capabilities
|
||||
let token_bridge = state::token_bridge_signer();
|
||||
move_to(&token_bridge, CoinCapabilities { mint_cap, freeze_cap, burn_cap });
|
||||
}
|
||||
|
||||
public(friend) fun mint<CoinType>(amount: u64): Coin<CoinType> acquires CoinCapabilities {
|
||||
assert!(state::is_wrapped_asset<CoinType>(), E_IS_NOT_WRAPPED_ASSET);
|
||||
assert!(exists<CoinCapabilities<CoinType>>(@token_bridge), E_COIN_CAP_DOES_NOT_EXIST);
|
||||
let caps = borrow_global<CoinCapabilities<CoinType>>(@token_bridge);
|
||||
let mint_cap = &caps.mint_cap;
|
||||
let coins = coin::mint<CoinType>(amount, mint_cap);
|
||||
coins
|
||||
}
|
||||
|
||||
public(friend) fun burn<CoinType>(coins: Coin<CoinType>) acquires CoinCapabilities {
|
||||
assert!(state::is_wrapped_asset<CoinType>(), E_IS_NOT_WRAPPED_ASSET);
|
||||
assert!(exists<CoinCapabilities<CoinType>>(@token_bridge), E_COIN_CAP_DOES_NOT_EXIST);
|
||||
let caps = borrow_global<CoinCapabilities<CoinType>>(@token_bridge);
|
||||
let burn_cap = &caps.burn_cap;
|
||||
coin::burn<CoinType>(coins, burn_cap);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::wrapped_test {
|
||||
use aptos_framework::account;
|
||||
use aptos_framework::coin;
|
||||
use aptos_framework::string::{utf8};
|
||||
use aptos_framework::type_info::{type_of};
|
||||
use aptos_framework::option;
|
||||
|
||||
use token_bridge::token_bridge::{Self as bridge};
|
||||
use token_bridge::state;
|
||||
use token_bridge::wrapped;
|
||||
use token_bridge::asset_meta;
|
||||
use token_bridge::string32;
|
||||
|
||||
use token_bridge::register_chain;
|
||||
|
||||
use wormhole::u16::{Self};
|
||||
use wrapped_coin::coin::T;
|
||||
use wormhole::external_address::{Self};
|
||||
|
||||
/// Registration VAA for the etheruem token bridge 0xdeadbeef
|
||||
const ETHEREUM_TOKEN_REG: vector<u8> = x"0100000000010015d405c74be6d93c3c33ed6b48d8db70dfb31e0981f8098b2a6c7583083e0c3343d4a1abeb3fc1559674fa067b0c0e2e9de2fafeaecdfeae132de2c33c9d27cc0100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000016911ae00000000000000000000000000000000000000000000546f6b656e427269646765010000000200000000000000000000000000000000000000000000000000000000deadbeef";
|
||||
|
||||
/// Attestation VAA sent from the ethereum token bridge 0xdeadbeef
|
||||
const ATTESTATION_VAA: vector<u8> = x"0100000000010080366065746148420220f25a6275097370e8db40984529a6676b7a5fc9feb11755ec49ca626b858ddfde88d15601f85ab7683c5f161413b0412143241c700aff010000000100000001000200000000000000000000000000000000000000000000000000000000deadbeef000000000150eb23000200000000000000000000000000000000000000000000000000000000beefface00020c424545460000000000000000000000000000000000000000000000000000000042656566206661636520546f6b656e0000000000000000000000000000000000";
|
||||
|
||||
fun setup(
|
||||
deployer: &signer,
|
||||
) {
|
||||
wormhole::wormhole_test::setup(0);
|
||||
bridge::init_test(deployer);
|
||||
}
|
||||
|
||||
public fun init_wrapped_token() {
|
||||
let chain = wormhole::u16::from_u64(2);
|
||||
let token_address = external_address::from_bytes(x"deadbeef");
|
||||
let asset_meta = asset_meta::create(
|
||||
token_address,
|
||||
chain,
|
||||
9,
|
||||
string32::from_bytes(b"foo"),
|
||||
string32::from_bytes(b"Foo bar token")
|
||||
);
|
||||
let wrapped_coin = account::create_account_for_test(@wrapped_coin);
|
||||
|
||||
// set up the signer capability first
|
||||
let signer_cap = account::create_test_signer_cap(@wrapped_coin);
|
||||
let origin_info = state::create_origin_info(chain, token_address);
|
||||
state::set_wrapped_asset_signer_capability(origin_info, signer_cap);
|
||||
|
||||
wrapped::init_wrapped_coin<wrapped_coin::coin::T>(&wrapped_coin, &asset_meta);
|
||||
}
|
||||
|
||||
|
||||
#[test(deployer=@deployer)]
|
||||
#[expected_failure(abort_code = 0)]
|
||||
fun test_create_wrapped_coin_unregistered(deployer: &signer) {
|
||||
setup(deployer);
|
||||
|
||||
wrapped::create_wrapped_coin_type(ATTESTATION_VAA);
|
||||
}
|
||||
|
||||
struct YourCoin {}
|
||||
|
||||
// This test ensures that I can't take a valid attestation VAA and trick the
|
||||
// token bridge to register my own type. I think what that could lead to is
|
||||
// a denial of service in case the 3rd party type is belongs to a module
|
||||
// with an 'arbitrary' upgrade policy which can be deleted in the future.
|
||||
// This upgrade policy is not enabled in the VM as of writing, but that
|
||||
// might well change in the future, so we future proof ourselves here.
|
||||
#[test(deployer=@deployer)]
|
||||
#[expected_failure(abort_code = 65537)] // ECOIN_INFO_ADDRESS_MISMATCH
|
||||
fun test_create_wrapped_coin_bad_type(deployer: &signer) {
|
||||
setup(deployer);
|
||||
register_chain::submit_vaa(ETHEREUM_TOKEN_REG);
|
||||
wrapped::create_wrapped_coin_type(ATTESTATION_VAA);
|
||||
|
||||
// initialize coin using type T, move caps to token_bridge, sets bridge state variables
|
||||
wrapped::create_wrapped_coin<YourCoin>(ATTESTATION_VAA);
|
||||
}
|
||||
|
||||
// test create_wrapped_coin_type and create_wrapped_coin
|
||||
#[test(deployer=@deployer)]
|
||||
fun test_create_wrapped_coin(deployer: &signer) {
|
||||
setup(deployer);
|
||||
register_chain::submit_vaa(ETHEREUM_TOKEN_REG);
|
||||
|
||||
wrapped::create_wrapped_coin_type(ATTESTATION_VAA);
|
||||
|
||||
// assert coin is NOT initialized
|
||||
assert!(!coin::is_coin_initialized<T>(), 0);
|
||||
|
||||
// initialize coin using type T, move caps to token_bridge, sets bridge state variables
|
||||
wrapped::create_wrapped_coin<T>(ATTESTATION_VAA);
|
||||
|
||||
// assert that coin IS initialized
|
||||
assert!(coin::is_coin_initialized<T>(), 0);
|
||||
|
||||
// assert coin info is correct
|
||||
assert!(coin::name<T>() == utf8(b"Beef face Token"), 0);
|
||||
assert!(coin::symbol<T>() == utf8(b"BEEF"), 0);
|
||||
assert!(coin::decimals<T>() == 12, 0);
|
||||
|
||||
// assert origin address, chain, type_info, is_wrapped are correct
|
||||
let origin_info = state::origin_info<T>();
|
||||
let origin_token_address = state::get_origin_info_token_address(&origin_info);
|
||||
let origin_token_chain = state::get_origin_info_token_chain(&origin_info);
|
||||
let wrapped_asset_type_info = state::wrapped_asset_info(origin_info);
|
||||
let is_wrapped_asset = state::is_wrapped_asset<T>();
|
||||
assert!(type_of<T>() == wrapped_asset_type_info, 0); //utf8(b"0xf4f53cc591e5190eddbc43940746e2b5deea6e0e1562b2bba765d488504842c7::coin::T"), 0);
|
||||
assert!(origin_token_chain == u16::from_u64(2), 0);
|
||||
assert!(external_address::get_bytes(&origin_token_address) == x"00000000000000000000000000000000000000000000000000000000beefface", 0);
|
||||
assert!(is_wrapped_asset, 0);
|
||||
|
||||
// load beef face token cap and mint some beef face coins, then burn
|
||||
let beef_coins = wrapped::mint<T>(10000);
|
||||
assert!(coin::value(&beef_coins)==10000, 0);
|
||||
assert!(coin::supply<T>() == option::some(10000), 0);
|
||||
wrapped::burn<T>(beef_coins);
|
||||
assert!(coin::supply<T>() == option::some(0), 0);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue