aptos/contracts: implement token bridge

This commit is contained in:
Csongor Kiss 2022-10-14 01:19:23 +00:00 committed by jumpsiegel
parent 3eb8daf492
commit be0f58a537
20 changed files with 2739 additions and 0 deletions

View File

@ -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

View File

@ -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 = "_"

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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 {
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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)
}
}

View File

@ -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(&register_chain);
let address = register_chain::get_emitter_address(&register_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);
}
}

View File

@ -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(),
}
);
}
}

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -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,
}
}
}

View File

@ -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
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}