wormhole/sui/token_bridge/sources/complete_transfer.move

1228 lines
44 KiB
Plaintext

// SPDX-License-Identifier: Apache 2
/// This module implements two methods: `authorize_transfer` and
/// `redeem_relayer_payout`, which are to be executed in a transaction block in
/// this order.
///
/// `authorize_transfer` allows a contract to complete a Token Bridge transfer,
/// sending assets to the encoded recipient. The coin payout incentive in
/// redeeming the transfer is packaged in a `RelayerReceipt`.
///
/// `redeem_relayer_payout` unpacks the `RelayerReceipt` to release the coin
/// containing the relayer fee amount.
///
/// The purpose of splitting this transfer redemption into two steps is in case
/// Token Bridge needs to be upgraded and there is a breaking change for this
/// module, an integrator would not be left broken. It is discouraged to put
/// `authorize_transfer` in an integrator's package logic. Otherwise, this
/// integrator needs to be prepared to upgrade his contract to handle the latest
/// version of `complete_transfer`.
///
/// Instead, an integrator is encouraged to execute a transaction block, which
/// executes `authorize_transfer` using the latest Token Bridge package ID and
/// to implement `redeem_relayer_payout` in his contract to consume this receipt.
/// This is similar to how an integrator with Wormhole is not meant to use
/// `vaa::parse_and_verify` in his contract in case the `vaa` module needs to
/// be upgraded due to a breaking change.
///
/// See `transfer` module for serialization and deserialization of Wormhole
/// message payload.
module token_bridge::complete_transfer {
use sui::balance::{Self, Balance};
use sui::coin::{Self, Coin};
use sui::tx_context::{Self, TxContext};
use wormhole::external_address::{Self, ExternalAddress};
use token_bridge::native_asset::{Self};
use token_bridge::normalized_amount::{Self, NormalizedAmount};
use token_bridge::state::{Self, State, LatestOnly};
use token_bridge::token_registry::{Self, VerifiedAsset};
use token_bridge::transfer::{Self};
use token_bridge::vaa::{Self, TokenBridgeMessage};
use token_bridge::wrapped_asset::{Self};
// Requires `handle_complete_transfer`.
friend token_bridge::complete_transfer_with_payload;
/// Transfer not intended to be received on Sui.
const E_TARGET_NOT_SUI: u64 = 0;
/// Input token info does not match registered info.
const E_CANONICAL_TOKEN_INFO_MISMATCH: u64 = 1;
/// Event reflecting when a transfer via `complete_transfer` or
/// `complete_transfer_with_payload` is successfully executed.
struct TransferRedeemed has drop, copy {
emitter_chain: u16,
emitter_address: ExternalAddress,
sequence: u64
}
/// This type is only generated from `authorize_transfer` and can only be
/// redeemed using `redeem_relayer_payout`. Integrators running relayer
/// contracts are expected to implement `redeem_relayer_payout` within their
/// contracts and call `authorize_transfer` in a transaction block preceding
/// the method that consumes this receipt.
struct RelayerReceipt<phantom CoinType> {
/// Coin of relayer fee payout.
payout: Coin<CoinType>
}
/// `authorize_transfer` deserializes a token transfer VAA payload. Once the
/// transfer is authorized, an event (`TransferRedeemed`) is emitted to
/// reflect which Token Bridge this transfer originated from. The
/// `RelayerReceipt` returned wraps a `Coin` object containing a payout that
/// incentivizes someone to execute a transaction on behalf of the encoded
/// recipient.
///
/// NOTE: This method is guarded by a minimum build version check. This
/// method could break backward compatibility on an upgrade.
///
/// It is important for integrators to refrain from calling this method
/// within their contracts. This method is meant to be called in a
/// transaction block, passing the `RelayerReceipt` to a method which calls
/// `redeem_relayer_payout` within a contract. If in a circumstance where
/// this module has a breaking change in an upgrade, `redeem_relayer_payout`
/// will not be affected by this change.
///
/// See `redeem_relayer_payout` for more details.
public fun authorize_transfer<CoinType>(
token_bridge_state: &mut State,
msg: TokenBridgeMessage,
ctx: &mut TxContext
): RelayerReceipt<CoinType> {
// This capability ensures that the current build version is used.
let latest_only = state::assert_latest_only(token_bridge_state);
// Emitting the transfer being redeemed (and disregard return value).
emit_transfer_redeemed(&msg);
// Deserialize transfer message and process.
handle_complete_transfer<CoinType>(
&latest_only,
token_bridge_state,
vaa::take_payload(msg),
ctx
)
}
/// After a transfer is authorized, a relayer contract may unpack the
/// `RelayerReceipt` using this method. Coin representing the relaying
/// incentive from this receipt is returned. This method is meant to be
/// simple. It allows for a coordination with calling `authorize_upgrade`
/// before a method that implements `redeem_relayer_payout` in a transaction
/// block to consume this receipt.
///
/// NOTE: Integrators of Token Bridge collecting relayer fee payouts from
/// these token transfers should be calling only this method from their
/// contracts. This method is not guarded by version control (thus not
/// requiring a reference to the Token Bridge `State` object), so it is
/// intended to work for any package version.
public fun redeem_relayer_payout<CoinType>(
receipt: RelayerReceipt<CoinType>
): Coin<CoinType> {
let RelayerReceipt { payout } = receipt;
payout
}
/// This is a privileged method only used by `complete_transfer` and
/// `complete_transfer_with_payload` modules. This method validates the
/// encoded token info with the passed in coin type via the `TokenRegistry`.
/// The transfer amount is denormalized and either mints balance of
/// wrapped asset or withdraws balance from native asset custody.
///
/// Depending on whether this coin is a Token Bridge wrapped asset or a
/// natively existing asset on Sui, the coin is either minted or withdrawn
/// from Token Bridge's custody.
public(friend) fun verify_and_bridge_out<CoinType>(
latest_only: &LatestOnly,
token_bridge_state: &mut State,
token_chain: u16,
token_address: ExternalAddress,
target_chain: u16,
amount: NormalizedAmount
): (
VerifiedAsset<CoinType>,
Balance<CoinType>
) {
// Verify that the intended chain ID for this transfer is for Sui.
assert!(
target_chain == wormhole::state::chain_id(),
E_TARGET_NOT_SUI
);
let asset_info = state::verified_asset<CoinType>(token_bridge_state);
assert!(
(
token_chain == token_registry::token_chain(&asset_info) &&
token_address == token_registry::token_address(&asset_info)
),
E_CANONICAL_TOKEN_INFO_MISMATCH
);
// De-normalize amount in preparation to take `Balance`.
let raw_amount =
normalized_amount::to_raw(
amount,
token_registry::coin_decimals(&asset_info)
);
// If the token is wrapped by Token Bridge, we will mint these tokens.
// Otherwise, we will withdraw from custody.
let bridged_out = {
let registry =
state::borrow_mut_token_registry(
latest_only,
token_bridge_state
);
if (token_registry::is_wrapped(&asset_info)) {
wrapped_asset::mint(
token_registry::borrow_mut_wrapped(registry),
raw_amount
)
} else {
native_asset::withdraw(
token_registry::borrow_mut_native(registry),
raw_amount
)
}
};
(asset_info, bridged_out)
}
/// This method emits source information of the token transfer. Off-chain
/// processes may want to observe when transfers have been redeemed.
public(friend) fun emit_transfer_redeemed(msg: &TokenBridgeMessage): u16 {
let emitter_chain = vaa::emitter_chain(msg);
// Emit Sui event with `TransferRedeemed`.
sui::event::emit(
TransferRedeemed {
emitter_chain,
emitter_address: vaa::emitter_address(msg),
sequence: vaa::sequence(msg)
}
);
emitter_chain
}
fun handle_complete_transfer<CoinType>(
latest_only: &LatestOnly,
token_bridge_state: &mut State,
transfer_vaa_payload: vector<u8>,
ctx: &mut TxContext
): RelayerReceipt<CoinType> {
let (
amount,
token_address,
token_chain,
recipient,
recipient_chain,
relayer_fee
) = transfer::unpack(transfer::deserialize(transfer_vaa_payload));
let (
asset_info,
bridged_out
) =
verify_and_bridge_out(
latest_only,
token_bridge_state,
token_chain,
token_address,
recipient_chain,
amount
);
let recipient = external_address::to_address(recipient);
// If the recipient did not redeem his own transfer, Token Bridge will
// split the withdrawn coins and send a portion to the transaction
// relayer.
let payout = if (
normalized_amount::value(&relayer_fee) == 0 ||
recipient == tx_context::sender(ctx)
) {
balance::zero()
} else {
let payout_amount =
normalized_amount::to_raw(
relayer_fee,
token_registry::coin_decimals(&asset_info)
);
balance::split(&mut bridged_out, payout_amount)
};
// Transfer tokens to the recipient.
sui::transfer::public_transfer(
coin::from_balance(bridged_out, ctx),
recipient
);
// Finally produce the receipt that a relayer can consume via
// `redeem_relayer_payout`.
RelayerReceipt {
payout: coin::from_balance(payout, ctx)
}
}
#[test_only]
public fun burn<CoinType>(receipt: RelayerReceipt<CoinType>) {
coin::burn_for_testing(redeem_relayer_payout(receipt));
}
}
#[test_only]
module token_bridge::complete_transfer_tests {
use sui::coin::{Self, Coin};
use sui::test_scenario::{Self};
use wormhole::state::{chain_id};
use wormhole::wormhole_scenario::{parse_and_verify_vaa};
use token_bridge::coin_wrapped_12::{Self, COIN_WRAPPED_12};
use token_bridge::coin_wrapped_7::{Self, COIN_WRAPPED_7};
use token_bridge::coin_native_10::{Self, COIN_NATIVE_10};
use token_bridge::coin_native_4::{Self, COIN_NATIVE_4};
use token_bridge::complete_transfer::{Self};
use token_bridge::dummy_message::{Self};
use token_bridge::native_asset::{Self};
use token_bridge::state::{Self};
use token_bridge::token_bridge_scenario::{
set_up_wormhole_and_token_bridge,
register_dummy_emitter,
return_state,
take_state,
three_people,
two_people
};
use token_bridge::token_registry::{Self};
use token_bridge::transfer::{Self};
use token_bridge::vaa::{Self};
use token_bridge::wrapped_asset::{Self};
struct OTHER_COIN_WITNESS has drop {}
#[test]
/// An end-to-end test for complete transfer native with VAA.
fun test_complete_transfer_native_10_relayer_fee() {
use token_bridge::complete_transfer::{
authorize_transfer,
redeem_relayer_payout
};
let transfer_vaa =
dummy_message::encoded_transfer_vaa_native_with_fee();
let (expected_recipient, tx_relayer, coin_deployer) = three_people();
let my_scenario = test_scenario::begin(tx_relayer);
let scenario = &mut my_scenario;
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Register foreign emitter on chain ID == 2.
let expected_source_chain = 2;
register_dummy_emitter(scenario, expected_source_chain);
let custody_amount = 500000;
coin_native_10::init_register_and_deposit(
scenario,
coin_deployer,
custody_amount
);
// Ignore effects.
test_scenario::next_tx(scenario, tx_relayer);
let token_bridge_state = take_state(scenario);
// These will be checked later.
let expected_relayer_fee = 100000;
let expected_recipient_amount = 200000;
let expected_amount = expected_relayer_fee + expected_recipient_amount;
// Scope to allow immutable reference to `TokenRegistry`.
{
let registry = state::borrow_token_registry(&token_bridge_state);
let asset = token_registry::borrow_native<COIN_NATIVE_10>(registry);
assert!(native_asset::custody(asset) == custody_amount, 0);
// Verify transfer parameters.
let parsed =
transfer::deserialize_test_only(
wormhole::vaa::take_payload(
parse_and_verify_vaa(scenario, transfer_vaa)
)
);
let asset_info =
token_registry::verified_asset<COIN_NATIVE_10>(registry);
let expected_token_chain = token_registry::token_chain(&asset_info);
let expected_token_address =
token_registry::token_address(&asset_info);
assert!(transfer::token_chain(&parsed) == expected_token_chain, 0);
assert!(
transfer::token_address(&parsed) == expected_token_address,
0
);
let coin_meta = test_scenario::take_shared(scenario);
let decimals = coin::get_decimals<COIN_NATIVE_10>(&coin_meta);
test_scenario::return_shared(coin_meta);
assert!(
transfer::raw_amount(&parsed, decimals) == expected_amount,
0
);
assert!(
transfer::raw_relayer_fee(&parsed, decimals) == expected_relayer_fee,
0
);
assert!(
transfer::recipient_as_address(&parsed) == expected_recipient,
0
);
assert!(transfer::recipient_chain(&parsed) == chain_id(), 0);
// Clean up.
transfer::destroy(parsed);
};
let verified_vaa = parse_and_verify_vaa(scenario, transfer_vaa);
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
// Ignore effects.
test_scenario::next_tx(scenario, tx_relayer);
let receipt =
authorize_transfer<COIN_NATIVE_10>(
&mut token_bridge_state,
msg,
test_scenario::ctx(scenario)
);
let payout = redeem_relayer_payout(receipt);
assert!(coin::value(&payout) == expected_relayer_fee, 0);
// TODO: Check for one event? `TransferRedeemed`.
let _effects = test_scenario::next_tx(scenario, tx_relayer);
// Check recipient's `Coin`.
let received =
test_scenario::take_from_address<Coin<COIN_NATIVE_10>>(
scenario,
expected_recipient
);
assert!(coin::value(&received) == expected_recipient_amount, 0);
// And check remaining amount in custody.
let registry = state::borrow_token_registry(&token_bridge_state);
let remaining = custody_amount - expected_amount;
{
let asset = token_registry::borrow_native<COIN_NATIVE_10>(registry);
assert!(native_asset::custody(asset) == remaining, 0);
};
// Clean up.
coin::burn_for_testing(payout);
coin::burn_for_testing(received);
return_state(token_bridge_state);
// Done.
test_scenario::end(my_scenario);
}
#[test]
/// An end-to-end test for complete transfer native with VAA.
fun test_complete_transfer_native_4_relayer_fee() {
use token_bridge::complete_transfer::{
authorize_transfer,
redeem_relayer_payout
};
let transfer_vaa =
dummy_message::encoded_transfer_vaa_native_with_fee();
let (expected_recipient, tx_relayer, coin_deployer) = three_people();
let my_scenario = test_scenario::begin(tx_relayer);
let scenario = &mut my_scenario;
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Register foreign emitter on chain ID == 2.
let expected_source_chain = 2;
register_dummy_emitter(scenario, expected_source_chain);
let custody_amount = 5000;
coin_native_4::init_register_and_deposit(
scenario,
coin_deployer,
custody_amount
);
// Ignore effects.
test_scenario::next_tx(scenario, tx_relayer);
let token_bridge_state = take_state(scenario);
// These will be checked later.
let expected_relayer_fee = 1000;
let expected_recipient_amount = 2000;
let expected_amount = expected_relayer_fee + expected_recipient_amount;
// Scope to allow immutable reference to `TokenRegistry`.
{
let registry = state::borrow_token_registry(&token_bridge_state);
let asset = token_registry::borrow_native<COIN_NATIVE_4>(registry);
assert!(native_asset::custody(asset) == custody_amount, 0);
// Verify transfer parameters.
let parsed =
transfer::deserialize_test_only(
wormhole::vaa::take_payload(
parse_and_verify_vaa(scenario, transfer_vaa)
)
);
let asset_info =
token_registry::verified_asset<COIN_NATIVE_4>(registry);
let expected_token_chain = token_registry::token_chain(&asset_info);
let expected_token_address =
token_registry::token_address(&asset_info);
assert!(transfer::token_chain(&parsed) == expected_token_chain, 0);
assert!(
transfer::token_address(&parsed) == expected_token_address,
0
);
let coin_meta = test_scenario::take_shared(scenario);
let decimals = coin::get_decimals<COIN_NATIVE_4>(&coin_meta);
test_scenario::return_shared(coin_meta);
assert!(
transfer::raw_amount(&parsed, decimals) == expected_amount,
0
);
assert!(
transfer::raw_relayer_fee(&parsed, decimals) == expected_relayer_fee,
0
);
assert!(
transfer::recipient_as_address(&parsed) == expected_recipient,
0
);
assert!(transfer::recipient_chain(&parsed) == chain_id(), 0);
// Clean up.
transfer::destroy(parsed);
};
let verified_vaa = parse_and_verify_vaa(scenario, transfer_vaa);
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
// Ignore effects.
test_scenario::next_tx(scenario, tx_relayer);
let receipt =
authorize_transfer<COIN_NATIVE_4>(
&mut token_bridge_state,
msg,
test_scenario::ctx(scenario)
);
let payout = redeem_relayer_payout(receipt);
assert!(coin::value(&payout) == expected_relayer_fee, 0);
// TODO: Check for one event? `TransferRedeemed`.
let _effects = test_scenario::next_tx(scenario, tx_relayer);
// Check recipient's `Coin`.
let received =
test_scenario::take_from_address<Coin<COIN_NATIVE_4>>(
scenario,
expected_recipient
);
assert!(coin::value(&received) == expected_recipient_amount, 0);
// And check remaining amount in custody.
let registry = state::borrow_token_registry(&token_bridge_state);
let remaining = custody_amount - expected_amount;
{
let asset = token_registry::borrow_native<COIN_NATIVE_4>(registry);
assert!(native_asset::custody(asset) == remaining, 0);
};
// Clean up.
coin::burn_for_testing(payout);
coin::burn_for_testing(received);
return_state(token_bridge_state);
// Done.
test_scenario::end(my_scenario);
}
#[test]
/// An end-to-end test for complete transfer wrapped with VAA.
fun test_complete_transfer_wrapped_7_relayer_fee() {
use token_bridge::complete_transfer::{
authorize_transfer,
redeem_relayer_payout
};
let transfer_vaa =
dummy_message::encoded_transfer_vaa_wrapped_7_with_fee();
let (expected_recipient, tx_relayer, coin_deployer) = three_people();
let my_scenario = test_scenario::begin(tx_relayer);
let scenario = &mut my_scenario;
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Register foreign emitter on chain ID == 2.
let expected_source_chain = 2;
register_dummy_emitter(scenario, expected_source_chain);
coin_wrapped_7::init_and_register(scenario, coin_deployer);
// Ignore effects.
test_scenario::next_tx(scenario, tx_relayer);
let token_bridge_state = take_state(scenario);
// These will be checked later.
let expected_relayer_fee = 1000;
let expected_recipient_amount = 2000;
let expected_amount = expected_relayer_fee + expected_recipient_amount;
// Scope to allow immutable reference to `TokenRegistry`.
{
let registry = state::borrow_token_registry(&token_bridge_state);
let asset =
token_registry::borrow_wrapped<COIN_WRAPPED_7>(registry);
assert!(wrapped_asset::total_supply(asset) == 0, 0);
// Verify transfer parameters.
let parsed =
transfer::deserialize_test_only(
wormhole::vaa::take_payload(
parse_and_verify_vaa(scenario, transfer_vaa)
)
);
let asset_info =
token_registry::verified_asset<COIN_WRAPPED_7>(registry);
let expected_token_chain = token_registry::token_chain(&asset_info);
let expected_token_address =
token_registry::token_address(&asset_info);
assert!(transfer::token_chain(&parsed) == expected_token_chain, 0);
assert!(
transfer::token_address(&parsed) == expected_token_address,
0
);
let coin_meta = test_scenario::take_shared(scenario);
let decimals = coin::get_decimals<COIN_WRAPPED_7>(&coin_meta);
test_scenario::return_shared(coin_meta);
assert!(
transfer::raw_amount(&parsed, decimals) == expected_amount,
0
);
assert!(
transfer::raw_relayer_fee(&parsed, decimals) == expected_relayer_fee,
0
);
assert!(
transfer::recipient_as_address(&parsed) == expected_recipient,
0
);
assert!(transfer::recipient_chain(&parsed) == chain_id(), 0);
// Clean up.
transfer::destroy(parsed);
};
let verified_vaa = parse_and_verify_vaa(scenario, transfer_vaa);
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
// Ignore effects.
test_scenario::next_tx(scenario, tx_relayer);
let receipt =
authorize_transfer<COIN_WRAPPED_7>(
&mut token_bridge_state,
msg,
test_scenario::ctx(scenario)
);
let payout = redeem_relayer_payout(receipt);
assert!(coin::value(&payout) == expected_relayer_fee, 0);
// TODO: Check for one event? `TransferRedeemed`.
let _effects = test_scenario::next_tx(scenario, tx_relayer);
// Check recipient's `Coin`.
let received =
test_scenario::take_from_address<Coin<COIN_WRAPPED_7>>(
scenario,
expected_recipient
);
assert!(coin::value(&received) == expected_recipient_amount, 0);
// And check that the amount is the total wrapped supply.
let registry = state::borrow_token_registry(&token_bridge_state);
{
let asset =
token_registry::borrow_wrapped<COIN_WRAPPED_7>(registry);
assert!(wrapped_asset::total_supply(asset) == expected_amount, 0);
};
// Clean up.
coin::burn_for_testing(payout);
coin::burn_for_testing(received);
return_state(token_bridge_state);
// Done.
test_scenario::end(my_scenario);
}
#[test]
/// An end-to-end test for complete transfer wrapped with VAA.
fun test_complete_transfer_wrapped_12_relayer_fee() {
use token_bridge::complete_transfer::{
authorize_transfer,
redeem_relayer_payout
};
let transfer_vaa =
dummy_message::encoded_transfer_vaa_wrapped_12_with_fee();
let (expected_recipient, tx_relayer, coin_deployer) = three_people();
let my_scenario = test_scenario::begin(tx_relayer);
let scenario = &mut my_scenario;
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Register foreign emitter on chain ID == 2.
let expected_source_chain = 2;
register_dummy_emitter(scenario, expected_source_chain);
coin_wrapped_12::init_and_register(scenario, coin_deployer);
// Ignore effects.
//
// NOTE: `tx_relayer` != `expected_recipient`.
assert!(expected_recipient != tx_relayer, 0);
test_scenario::next_tx(scenario, tx_relayer);
let token_bridge_state = take_state(scenario);
// These will be checked later.
let expected_relayer_fee = 1000;
let expected_recipient_amount = 2000;
let expected_amount = expected_relayer_fee + expected_recipient_amount;
// Scope to allow immutable reference to `TokenRegistry`.
{
let registry = state::borrow_token_registry(&token_bridge_state);
let asset =
token_registry::borrow_wrapped<COIN_WRAPPED_12>(registry);
assert!(wrapped_asset::total_supply(asset) == 0, 0);
// Verify transfer parameters.
let parsed =
transfer::deserialize_test_only(
wormhole::vaa::take_payload(
parse_and_verify_vaa(scenario, transfer_vaa)
)
);
let asset_info =
token_registry::verified_asset<COIN_WRAPPED_12>(registry);
let expected_token_chain = token_registry::token_chain(&asset_info);
let expected_token_address =
token_registry::token_address(&asset_info);
assert!(transfer::token_chain(&parsed) == expected_token_chain, 0);
assert!(transfer::token_address(&parsed) == expected_token_address, 0);
let coin_meta = test_scenario::take_shared(scenario);
let decimals = coin::get_decimals<COIN_WRAPPED_12>(&coin_meta);
test_scenario::return_shared(coin_meta);
assert!(transfer::raw_amount(&parsed, decimals) == expected_amount, 0);
assert!(
transfer::raw_relayer_fee(&parsed, decimals) == expected_relayer_fee,
0
);
assert!(
transfer::recipient_as_address(&parsed) == expected_recipient,
0
);
assert!(transfer::recipient_chain(&parsed) == chain_id(), 0);
// Clean up.
transfer::destroy(parsed);
};
let verified_vaa = parse_and_verify_vaa(scenario, transfer_vaa);
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
// Ignore effects.
test_scenario::next_tx(scenario, tx_relayer);
let receipt =
authorize_transfer<COIN_WRAPPED_12>(
&mut token_bridge_state,
msg,
test_scenario::ctx(scenario)
);
let payout = redeem_relayer_payout(receipt);
assert!(coin::value(&payout) == expected_relayer_fee, 0);
// TODO: Check for one event? `TransferRedeemed`.
let _effects = test_scenario::next_tx(scenario, tx_relayer);
// Check recipient's `Coin`.
let received =
test_scenario::take_from_address<Coin<COIN_WRAPPED_12>>(
scenario,
expected_recipient
);
assert!(coin::value(&received) == expected_recipient_amount, 0);
// And check that the amount is the total wrapped supply.
let registry = state::borrow_token_registry(&token_bridge_state);
{
let asset = token_registry::borrow_wrapped<COIN_WRAPPED_12>(registry);
assert!(wrapped_asset::total_supply(asset) == expected_amount, 0);
};
// Clean up.
coin::burn_for_testing(payout);
coin::burn_for_testing(received);
return_state(token_bridge_state);
// Done.
test_scenario::end(my_scenario);
}
#[test]
/// An end-to-end test for complete transfer native with VAA. The encoded VAA
/// specifies a nonzero fee, however the `recipient` should receive the full
/// amount for self redeeming the transfer.
fun test_complete_transfer_native_10_relayer_fee_self_redemption() {
use token_bridge::complete_transfer::{
authorize_transfer,
redeem_relayer_payout
};
let transfer_vaa =
dummy_message::encoded_transfer_vaa_native_with_fee();
let (expected_recipient, _, coin_deployer) = three_people();
let my_scenario = test_scenario::begin(expected_recipient);
let scenario = &mut my_scenario;
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Register foreign emitter on chain ID == 2.
let expected_source_chain = 2;
register_dummy_emitter(scenario, expected_source_chain);
let custody_amount = 500000;
coin_native_10::init_register_and_deposit(
scenario,
coin_deployer,
custody_amount
);
// Ignore effects.
test_scenario::next_tx(scenario, expected_recipient);
let token_bridge_state = take_state(scenario);
// NOTE: Although there is a fee encoded in the VAA, the relayer
// shouldn't receive this fee. The `expected_relayer_fee` should
// go to the recipient.
//
// These values will be used later.
let expected_relayer_fee = 0;
let encoded_relayer_fee = 100000;
let expected_recipient_amount = 300000;
let expected_amount = expected_relayer_fee + expected_recipient_amount;
// Scope to allow immutable reference to `TokenRegistry`.
{
let registry = state::borrow_token_registry(&token_bridge_state);
let asset = token_registry::borrow_native<COIN_NATIVE_10>(registry);
assert!(native_asset::custody(asset) == custody_amount, 0);
// Verify transfer parameters.
let parsed =
transfer::deserialize_test_only(
wormhole::vaa::take_payload(
parse_and_verify_vaa(scenario, transfer_vaa)
)
);
let asset_info =
token_registry::verified_asset<COIN_NATIVE_10>(registry);
let expected_token_chain = token_registry::token_chain(&asset_info);
let expected_token_address =
token_registry::token_address(&asset_info);
assert!(transfer::token_chain(&parsed) == expected_token_chain, 0);
assert!(transfer::token_address(&parsed) == expected_token_address, 0);
let coin_meta = test_scenario::take_shared(scenario);
let decimals = coin::get_decimals<COIN_NATIVE_10>(&coin_meta);
test_scenario::return_shared(coin_meta);
assert!(transfer::raw_amount(&parsed, decimals) == expected_amount, 0);
assert!(
transfer::raw_relayer_fee(&parsed, decimals) == encoded_relayer_fee,
0
);
assert!(
transfer::recipient_as_address(&parsed) == expected_recipient,
0
);
assert!(transfer::recipient_chain(&parsed) == chain_id(), 0);
// Clean up.
transfer::destroy(parsed);
};
let verified_vaa = parse_and_verify_vaa(scenario, transfer_vaa);
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
// Ignore effects.
test_scenario::next_tx(scenario, expected_recipient);
let receipt =
authorize_transfer<COIN_NATIVE_10>(
&mut token_bridge_state,
msg,
test_scenario::ctx(scenario)
);
let payout = redeem_relayer_payout(receipt);
assert!(coin::value(&payout) == expected_relayer_fee, 0);
// TODO: Check for one event? `TransferRedeemed`.
let _effects = test_scenario::next_tx(scenario, expected_recipient);
// Check recipient's `Coin`.
let received =
test_scenario::take_from_address<Coin<COIN_NATIVE_10>>(
scenario,
expected_recipient
);
assert!(coin::value(&received) == expected_recipient_amount, 0);
// And check remaining amount in custody.
let registry = state::borrow_token_registry(&token_bridge_state);
let remaining = custody_amount - expected_amount;
{
let asset = token_registry::borrow_native<COIN_NATIVE_10>(registry);
assert!(native_asset::custody(asset) == remaining, 0);
};
// Clean up.
coin::burn_for_testing(payout);
coin::burn_for_testing(received);
return_state(token_bridge_state);
// Done.
test_scenario::end(my_scenario);
}
#[test]
#[expected_failure(
abort_code = complete_transfer::E_CANONICAL_TOKEN_INFO_MISMATCH
)]
/// This test verifies that `authorize_transfer` reverts when called with
/// a native COIN_TYPE that's not encoded in the VAA.
fun test_cannot_authorize_transfer_native_invalid_coin_type() {
use token_bridge::complete_transfer::{authorize_transfer};
let transfer_vaa =
dummy_message::encoded_transfer_vaa_native_with_fee();
let (_, tx_relayer, coin_deployer) = three_people();
let my_scenario = test_scenario::begin(tx_relayer);
let scenario = &mut my_scenario;
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Register foreign emitter on chain ID == 2.
let expected_source_chain = 2;
register_dummy_emitter(scenario, expected_source_chain);
let custody_amount_coin_10 = 500000;
coin_native_10::init_register_and_deposit(
scenario,
coin_deployer,
custody_amount_coin_10
);
// Register a second native asset.
let custody_amount_coin_4 = 69420;
coin_native_4::init_register_and_deposit(
scenario,
coin_deployer,
custody_amount_coin_4
);
// Ignore effects.
test_scenario::next_tx(scenario, tx_relayer);
let token_bridge_state = take_state(scenario);
// Scope to allow immutable reference to `TokenRegistry`. This verifies
// that both coin types have been registered.
{
let registry = state::borrow_token_registry(&token_bridge_state);
// COIN_10.
let coin_10 =
token_registry::borrow_native<COIN_NATIVE_10>(registry);
assert!(
native_asset::custody(coin_10) == custody_amount_coin_10,
0
);
// COIN_4.
let coin_4 = token_registry::borrow_native<COIN_NATIVE_4>(registry);
assert!(native_asset::custody(coin_4) == custody_amount_coin_4, 0);
};
let verified_vaa = parse_and_verify_vaa(scenario, transfer_vaa);
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
// Ignore effects.
test_scenario::next_tx(scenario, tx_relayer);
// NOTE: this call should revert since the transfer VAA is for
// a coin of type COIN_NATIVE_10. However, the `complete_transfer`
// method is called using the COIN_NATIVE_4 type.
let receipt =
authorize_transfer<COIN_NATIVE_4>(
&mut token_bridge_state,
msg,
test_scenario::ctx(scenario)
);
// Clean up.
complete_transfer::burn(receipt);
abort 42
}
#[test]
#[expected_failure(
abort_code = complete_transfer::E_CANONICAL_TOKEN_INFO_MISMATCH
)]
/// This test verifies that `authorize_transfer` reverts when called with
/// a wrapped COIN_TYPE that's not encoded in the VAA.
fun test_cannot_authorize_transfer_wrapped_invalid_coin_type() {
use token_bridge::complete_transfer::{authorize_transfer};
let transfer_vaa = dummy_message::encoded_transfer_vaa_wrapped_12_with_fee();
let (expected_recipient, tx_relayer, coin_deployer) = three_people();
let my_scenario = test_scenario::begin(tx_relayer);
let scenario = &mut my_scenario;
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Register foreign emitter on chain ID == 2.
let expected_source_chain = 2;
register_dummy_emitter(scenario, expected_source_chain);
// Register both wrapped coin types (12 and 7).
coin_wrapped_12::init_and_register(scenario, coin_deployer);
coin_wrapped_7::init_and_register(scenario, coin_deployer);
// Ignore effects.
test_scenario::next_tx(scenario, tx_relayer);
// NOTE: `tx_relayer` != `expected_recipient`.
assert!(expected_recipient != tx_relayer, 0);
let token_bridge_state = take_state(scenario);
// Scope to allow immutable reference to `TokenRegistry`. This verifies
// that both coin types have been registered.
{
let registry = state::borrow_token_registry(&token_bridge_state);
let coin_12 =
token_registry::borrow_wrapped<COIN_WRAPPED_12>(registry);
assert!(wrapped_asset::total_supply(coin_12) == 0, 0);
let coin_7 =
token_registry::borrow_wrapped<COIN_WRAPPED_7>(registry);
assert!(wrapped_asset::total_supply(coin_7) == 0, 0);
};
let verified_vaa = parse_and_verify_vaa(scenario, transfer_vaa);
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
// Ignore effects.
test_scenario::next_tx(scenario, tx_relayer);
// NOTE: this call should revert since the transfer VAA is for
// a coin of type COIN_WRAPPED_12. However, the `authorize_transfer`
// method is called using the COIN_WRAPPED_7 type.
let receipt =
authorize_transfer<COIN_WRAPPED_7>(
&mut token_bridge_state,
msg,
test_scenario::ctx(scenario)
);
// Clean up.
complete_transfer::burn(receipt);
abort 42
}
#[test]
#[expected_failure(abort_code = complete_transfer::E_TARGET_NOT_SUI)]
/// This test verifies that `authorize_transfer` reverts when a transfer is
/// sent to the wrong target blockchain (chain ID != 21).
fun test_cannot_authorize_transfer_wrapped_12_invalid_target_chain() {
use token_bridge::complete_transfer::{authorize_transfer};
let transfer_vaa =
dummy_message::encoded_transfer_vaa_wrapped_12_invalid_target_chain();
let (expected_recipient, tx_relayer, coin_deployer) = three_people();
let my_scenario = test_scenario::begin(tx_relayer);
let scenario = &mut my_scenario;
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Register foreign emitter on chain ID == 2.
let expected_source_chain = 2;
register_dummy_emitter(scenario, expected_source_chain);
coin_wrapped_12::init_and_register(scenario, coin_deployer);
// Ignore effects.
//
// NOTE: `tx_relayer` != `expected_recipient`.
assert!(expected_recipient != tx_relayer, 0);
// Ignore effects.
test_scenario::next_tx(scenario, tx_relayer);
let token_bridge_state = take_state(scenario);
let verified_vaa = parse_and_verify_vaa(scenario, transfer_vaa);
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
// Ignore effects.
test_scenario::next_tx(scenario, tx_relayer);
// NOTE: this call should revert since the target chain encoded is
// chain 69 instead of chain 21 (Sui).
let receipt =
authorize_transfer<COIN_WRAPPED_12>(
&mut token_bridge_state,
msg,
test_scenario::ctx(scenario)
);
// Clean up.
complete_transfer::burn(receipt);
abort 42
}
#[test]
#[expected_failure(abort_code = wormhole::package_utils::E_NOT_CURRENT_VERSION)]
fun test_cannot_complete_transfer_outdated_version() {
use token_bridge::complete_transfer::{authorize_transfer};
let transfer_vaa =
dummy_message::encoded_transfer_vaa_native_with_fee();
let (tx_relayer, coin_deployer) = two_people();
let my_scenario = test_scenario::begin(tx_relayer);
let scenario = &mut my_scenario;
// Set up contracts.
let wormhole_fee = 350;
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
// Register foreign emitter on chain ID == 2.
let expected_source_chain = 2;
register_dummy_emitter(scenario, expected_source_chain);
let custody_amount = 500000;
coin_native_10::init_register_and_deposit(
scenario,
coin_deployer,
custody_amount
);
// Ignore effects.
test_scenario::next_tx(scenario, tx_relayer);
let token_bridge_state = take_state(scenario);
let verified_vaa = parse_and_verify_vaa(scenario, transfer_vaa);
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
// Ignore effects.
test_scenario::next_tx(scenario, tx_relayer);
// Conveniently roll version back.
state::reverse_migrate_version(&mut token_bridge_state);
// Simulate executing with an outdated build by upticking the minimum
// required version for `publish_message` to something greater than
// this build.
state::migrate_version_test_only(
&mut token_bridge_state,
token_bridge::version_control::previous_version_test_only(),
token_bridge::version_control::next_version()
);
// You shall not pass!
let receipt =
authorize_transfer<COIN_NATIVE_10>(
&mut token_bridge_state,
msg,
test_scenario::ctx(scenario)
);
// Clean up.
complete_transfer::burn(receipt);
abort 42
}
}