1228 lines
44 KiB
Plaintext
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
|
|
}
|
|
}
|