aptos/nft_bridge: implement contract

This commit is contained in:
Csongor Kiss 2022-12-13 00:35:12 -06:00 committed by Csongor Kiss
parent 6da8e4ae7d
commit 9824d71fe1
21 changed files with 2629 additions and 5 deletions

View File

@ -8,6 +8,7 @@ WORKDIR /tmp
COPY wormhole/ wormhole
COPY token_bridge/ token_bridge
COPY nft_bridge/ nft_bridge
COPY deployer/ deployer
COPY coin/ coin
COPY examples/ examples

View File

@ -1,4 +1,4 @@
CONTRACT_DIRS := deployer wormhole token_bridge examples coin
CONTRACT_DIRS := deployer wormhole token_bridge nft_bridge examples coin
TARGETS := build test

View File

@ -11,6 +11,7 @@ The project is laid out as follows:
- [wormhole](./wormhole) the core messaging layer
- [token_bridge](./token_bridge) the asset transfer layer
- [nft_bridge](./nft_bridge) NFT transfer layer
- [examples](./examples) various example contracts
To see a minimal example of how to integrate with wormhole, check out

14
aptos/nft_bridge/Makefile Normal file
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,28 @@
[package]
name = "NFTBridge"
version = "0.0.1"
[dependencies]
AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-framework/", rev = "mainnet" }
MoveStdlib = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/move-stdlib/", rev = "mainnet" }
AptosStdlib = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-stdlib/", rev = "mainnet" }
AptosToken = { git = "https://github.com/aptos-labs/aptos-core.git", subdir = "aptos-move/framework/aptos-token/", rev = "mainnet" }
Wormhole = { local = "../wormhole/" }
TokenBridge = { local = "../token_bridge/" }
Deployer = { local = "../deployer/" }
# only included in testing
[dev-dependencies]
WrappedCoin = { local = "../coin/" }
[dev-addresses]
# derived address from sha3_256(deployer | "nft_bridge" | 0xFF) by running
# worm aptos derive-resource-account 0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b nft_bridge
nft_bridge = "46da3d4c569388af61f951bdd1153f4c875f90c2991f6b2d0a38e2161a40852c"
token_bridge = "84a5f374d29fc77e370014dce4fd6a55b58ad608de8074b0be5571701724da31"
deployer = "0x277fa055b6a73c42c0662d5236c65c864ccbf2d4abd21f174a30c8b786eab84b"
wrapped_coin = "0xf4f53cc591e5190eddbc43940746e2b5deea6e0e1562b2bba765d488504842c7"
wormhole = "0xde0036a9600559e295d5f6802ef6f3f802f510366e0c23912b0655d972166017"
[addresses]
nft_bridge = "_"

156
aptos/nft_bridge/README.md Normal file
View File

@ -0,0 +1,156 @@
# Aptos NFT Bridge
This contract is a reference implementation of the [Wormhole NFT bridge
specification](../../whitepapers/0006_nft_bridge.md) on Aptos, written in the
Move programming language.
This document provides an overview of the design and structure of the program.
## NFTs on Aptos
The [Aptos Token
specification](https://aptos.dev/concepts/coin-and-token/aptos-token/) provides
a good overview of how Tokens are specified on Aptos, but we review the relevant
parts here.
First, it's important to mention that the Token specification is more general
than NFTs, as Tokens can be used to describe both fungible and non-fungible
tokens.
Tokens belong to collections, which in turn belong to their creators. Given a
creator address, the collection's name (string) uniquely identifies the
collection. Within a collection, a token's name uniquely identifies the token.
These tokens may be fungible however: it's possible to have multiple copies of
them which are fully interchangeable. Each type of token has a set of properties
of key-value pairs, that allow the creator to attach custom information to the
tokens, such as hair colour. For an example, see
https://www.topaz.so/assets/Aptos-Undead-5a4505c2e9/Aptos%20Undead%20%233085/0.
The base token (which is identified by `(creator, collection_name, token_name)`)
has a set of "default" properties. It is possible to create "editions" of these
base tokens, which turn them into unique variations with additional properties
relative to the base token. The modified editions are unique and non-fungible,
so they are NFTs. When such an edition is created, it gets assigned a version,
called the `property_version`, within the base token. Such editions are thus
identified by `(creator, collection_name, token_name, property_version)`. When
the `property_version` is 0, the token may be fungible, but when it is non-0,
only a single copy may exist.
From the documentation, it appears that this property versioning is more of an
optimisation to allow bulk-minting NFTs cheaply and later add properties in a
copy-on-write fashion. It's unclear if the fungibility is meaningful outside of
this optimisation, as fungible tokens are already better supported by the first
class `Coin` type.
## Wormhole NFT Bridge
The Wormhole NFT bridge specification (which is based on ERC721) uses 32 bytes
to identify collections (only 20 bytes of which are used on EVM chains, for the
contract address) and another 32 bytes to identify the token within the
collection. Neither of these fields are sufficient to pack the necessary
information on Aptos, since the creator address itself is already 32 bytes, and
the collection name can be an arbitary string up to 128 bytes. Token names can
also be arbitary 128 byte strings.
Thus, we store 32 byte hashes of these two fields respectively. The exact
details of how the hashes are computed are defined in
[token_hash.move](./sources/token_hash.move). The collection's hash is computed
from the creator and the collection name. The hash of the individual NFTs is
computed from the creator, the collection's name, the token name, and the
property version. Note that it would be sufficient to just take the token name
and the property version, but this way the token's hash can be used as a
globally unique identifier, which simplifies the implementation.
When transferring a native token out for the first time, we
(`state::set_native_asset_info`) store a mapping from its hash to the token's
`TokenId`, so it can be retrieved when transferring the token back
(`state::get_native_asset_info`).
### Wrapped asset creation
When transferring an NFT from a collection on a foreign chain to Aptos, a
corresponding "wrapped" collection is created. The module responsible for this
is [wrapped.move](sources/wrapped.move). The collection name is the the NFT name
field from the transfer VAA. To avoid collisions here, each NFT is minted into a
freshly created creator account, implemented as a resource account.
### Handling "fungible" tokens
As discussed above, tokens whose property version is 0 are technically fungible.
We could disallow tokens whose property version is 0, and only allow
transferring ones that are non-0. However, many real-world NFT projects (such as
[Aptos Undead](https://www.topaz.so/collection/Aptos-Undead-5a4505c2e9)) simply
mint all tokens as separate tokens with property version 0 (and don't
necessarily use editions). We could instead check that the supply of the token
is 1, but new tokens can always be minted after the check is performed anyway.
Also, the supply is only tracked if the token has a specified maximum supply,
which, again, real-world NFT projects may not specify.
Instead, we don't check the supply, and simply allow transferring a single copy
of `property_version = 0` tokens at a given time. What this means is that when a
token is transferred out, we check that only a single copy is sent at a given
time, and also that there is at most 1 token held by the NFT bridge contract.
This is the most general setup that supports existing NFT projects, but it does
mean there is an edge case where tokens that are legitimately fungible (but
decided to not use the `Coin` type for some reason) are transferrable through
the NFT bridge, although at most 1 can be locked at any given time, so this edge
case is not observable outside of Aptos.
An additional caveat: it is possible for the creator to mutate the properties of
an NFT by calling `token::mutate_one_token` (in fact this is the mechanism by
which property versions other than 0 are assigned). If the token already had a
non-0 property version, then this operation will simply mutate it in-place,
keeping the identity of the token. However, if the property version was 0, then
the token is burned and a new token with a non-0 property version is created in
its place. If this happens to a token held in custody by the NFT bridge, then
that token will be irredeemable. It does require the creator of the NFT to
explicitly mutate a token held by the NFT bridge.
### Handling Solana NFTs
Solana NFTs require special handling currently. This is because at the time the
Solana NFT bridge was first implemented, there was no notion of NFT collections,
and each NFT would simply be its own individual token. Due to the gas costs of
creating collections on Ethereum, the Solana NFT bridge simply puts all NFTs
into a single dummy collection, so when transferred to other chains, they end up
under the same collection. This means that storing the collection metadata in
the wrapped collection does not work due to the many-to-one mapping. Instead,
like on Ethereum, we implement a cache (the "SPL cache") to store the name and
symbol of these tokens separately in a mapping keyed by the solana token's
address. When transferring out, this cache is consulted to recover the metadata
needed in the outgoing VAA.
The `state::is_unified_solana_collection` implements the check to determine
whether this caching behaviour is needed. It not only checks for the source
chain (Solana) but also the dummy collection address. This allows smoothly
upgrading the Solana NFT bridge to use the real collection address, in which
case the collections will be preserved moving forward and the cache ignored.
The cache is set in `wrapped::create_or_find_wrapped_nft_collection` when
transferring in, and read by the `state::get_wrapped_asset_name_and_symbol`
function (used by `transfer_nft::lock_or_burn` on the way out).
## Governance
Outside of handling NFT transfers, the NFT bridge can perform two additional
operations, both of which require a VAA signed by the Wormhole guardians.
These are governance operations, as they alter the behaviour of the bridge.
Both of these governance operations are identical to the token bridge implementation.
### Registrations
Since sending messages through Wormhole is permissionless and message payloads
are arbitrary, any program could send messages that look like NFT transfers.
To ensure that such messages are accepted from a trusted set of contracts, the
NFT bridge maintains a set of known "emitters". These are stored in a table
`registered_emitters` in `state::State`, keyed by the chains' ids (i.e. at most
one emitter per chain). This mapping can be updated by submitting registration
VAAs (which are special VAAs that are signed manually by the guardians through a
governance ceremony), and handled in the `register_chain.move` module.
### Contract upgrades
Contract upgrades also require governance VAAs. In the case of Aptos, the VAA
will contain the hash of the bytecode we're upgrading to. This logic is
implemented in `contract_upgrade.move`.

View File

@ -0,0 +1,460 @@
module nft_bridge::complete_transfer {
use aptos_std::from_bcs;
use aptos_token::token;
use nft_bridge::vaa;
use nft_bridge::transfer::{Self, Transfer};
use nft_bridge::state;
use nft_bridge::wrapped;
use nft_bridge::token_hash;
use token_bridge::string32;
use wormhole::state as wormhole_state;
use wormhole::external_address;
const E_INVALID_TARGET: u64 = 0;
const E_INVALID_TOKEN_ADDRESS: u64 = 1;
public fun submit_vaa(vaa: vector<u8>): Transfer {
let vaa = vaa::parse_verify_and_replay_protect(vaa);
let transfer = transfer::parse(wormhole::vaa::destroy(vaa));
complete_transfer(&transfer);
transfer
}
public entry fun submit_vaa_entry(vaa: vector<u8>) {
submit_vaa(vaa);
}
/// Submits the complete transfer VAA and registers the NFT for the
/// recipient if not already registered.
public entry fun submit_vaa_and_register_entry(recipient: &signer, vaa: vector<u8>) {
token::opt_in_direct_transfer(recipient, true);
submit_vaa(vaa);
}
#[test_only]
public fun test(transfer: &Transfer) {
complete_transfer(transfer)
}
fun complete_transfer(transfer: &Transfer) {
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_id = transfer::get_token_id(transfer);
let uri = transfer::get_uri(transfer);
let recipient = from_bcs::to_address(external_address::get_bytes(&transfer::get_to(transfer)));
let is_wrapped_asset: bool = token_chain != wormhole_state::get_chain_id();
if (is_wrapped_asset) {
let (creator, collection) = wrapped::create_or_find_wrapped_nft_collection(transfer);
wrapped::mint_to(&creator, recipient, string32::to_string(&collection), &token_id, uri);
} else {
// native, we must have seen this token before (on the way out)
let nft_bridge = state::nft_bridge_signer();
// Since the way we derive external ids for tokens (see
// token_hash.move) guarantees that the ids are globally unique, we
// only need the token_id field of the transfer VAA to identify the
// token in question, the token_address is not necessary...
let token_hash = token_hash::from_external_address(token_id);
let token_id = state::get_native_asset_info(token_hash);
// ...nevertheless, as a sanity check, we derive the collection hash
// and ensure that it comes from collection that token_address claims.
let (collection_hash, _) = token_hash::derive(&token_id);
let collection_hash = token_hash::get_collection_external_address(&collection_hash);
assert!(collection_hash == transfer::get_token_address(transfer), E_INVALID_TOKEN_ADDRESS);
token::transfer(&nft_bridge, token_id, recipient, 1);
};
}
}
#[test_only]
module nft_bridge::complete_transfer_test {
use std::signer;
use std::string::{Self, String};
use std::bcs;
use aptos_framework::account;
use aptos_token::token;
use wormhole::external_address::{Self, ExternalAddress};
use wormhole::u16;
use token_bridge::string32;
use nft_bridge::transfer;
use nft_bridge::uri;
use nft_bridge::complete_transfer;
use nft_bridge::state;
use nft_bridge::transfer_nft;
use nft_bridge::token_hash;
use nft_bridge::transfer_nft_test;
use nft_bridge::wrapped_token_name;
// ------ Wrapped asset tests
// Test that complete_transfer for wrapped token works
#[test(deployer = @deployer, recipient = @0x1234)]
public fun test_complete_transfer_wrapped_asset(deployer: &signer, recipient: address) {
wormhole::wormhole_test::setup(0);
nft_bridge::nft_bridge::init_test(deployer);
let recipient_signer = aptos_framework::account::create_account_for_test(recipient);
complete_transfer_wrapped_helper(&recipient_signer, external_address::from_bytes(x"01"));
}
// Test that the same token can't be transferred in twice without being
// transferred out first
#[test(deployer = @deployer, recipient1 = @0x1234, recipient2 = @0x5678)]
#[expected_failure(abort_code = 524297, location = aptos_token::token)]
public fun test_complete_transfer_wrapped_asset_twice(
deployer: &signer,
recipient1: address,
recipient2: address
) {
wormhole::wormhole_test::setup(0);
nft_bridge::nft_bridge::init_test(deployer);
let recipient_signer1 = aptos_framework::account::create_account_for_test(recipient1);
let recipient_signer2 = aptos_framework::account::create_account_for_test(recipient2);
complete_transfer_wrapped_helper(&recipient_signer1, external_address::from_bytes(x"01"));
complete_transfer_wrapped_helper(&recipient_signer2, external_address::from_bytes(x"01"));
}
// Test that transferring a token in, then out, then back in again works
#[test(deployer = @deployer, recipient = @0x1234)]
public fun test_complete_transfer_wrapped_there_and_back(
deployer: &signer,
recipient: address,
) {
wormhole::wormhole_test::setup(0);
nft_bridge::nft_bridge::init_test(deployer);
let recipient_signer = aptos_framework::account::create_account_for_test(recipient);
let token_id = external_address::from_bytes(x"01");
// step 1) transfer in
complete_transfer_wrapped_helper(&recipient_signer, token_id);
let token_address = external_address::from_bytes(x"09");
let token_chain = u16::from_u64(14);
let origin_info = state::create_origin_info(token_chain, token_address);
let creator = state::get_wrapped_asset_signer(origin_info);
let expected_token_name = string::utf8(b"0000000000000000000000000000000000000000000000000000000000000001");
// step 2) transfer out
transfer_nft::transfer_nft_entry(
&recipient_signer,
signer::address_of(&creator),
string::utf8(b"my name"),
expected_token_name,
0, // property_version
1, // recipient chain (doesn't matter)
x"0000000000000000000000000000000000000000000000000000000000012345", // recipient (doesn't matter)
0 // nonce (doesn't matter)
);
// step 3) transfer in again
complete_transfer_wrapped_helper(&recipient_signer, token_id);
}
// Test that transferring a token in, then out, preserves the metadata
#[test(deployer = @deployer, recipient = @0x1234)]
public fun test_complete_transfer_wrapped_preserves_metadata(
deployer: &signer,
recipient: address,
) {
wormhole::wormhole_test::setup(0);
nft_bridge::nft_bridge::init_test(deployer);
let recipient_signer = aptos_framework::account::create_account_for_test(recipient);
let token_id = external_address::from_bytes(x"01");
// step 1) transfer in
complete_transfer_wrapped_helper(&recipient_signer, token_id);
let token_address = external_address::from_bytes(x"09");
let token_chain = u16::from_u64(14);
let origin_info = state::create_origin_info(token_chain, token_address);
let creator = state::get_wrapped_asset_signer(origin_info);
let expected_token_name = string::utf8(b"0000000000000000000000000000000000000000000000000000000000000001");
let token_id = token::create_token_id_raw(
signer::address_of(&creator),
string::utf8(b"my name"),
expected_token_name,
0, // property_version
);
let token = token::withdraw_token(&recipient_signer, token_id, 1);
let (token_address,
token_chain,
symbol,
name,
token_id,
uri,
) = transfer_nft::transfer_nft_test(token);
assert!(token_address == external_address::from_bytes(x"09"), 0);
assert!(token_chain == u16::from_u64(14), 0);
assert!(symbol == string32::from_bytes(b"my symbol"), 0);
assert!(name == string32::from_bytes(b"my name"), 0);
assert!(token_id == external_address::from_bytes(x"01"), 0);
assert!(uri == uri::from_bytes(b"http://google.com"), 0);
}
// Test that multiple tokens can be minted from the same collection
#[test(deployer=@deployer, recipient=@0x1234)]
public fun test_complete_transfer_wrapped_asset_multiple_tokens(deployer: &signer, recipient: address) {
wormhole::wormhole_test::setup(0);
nft_bridge::nft_bridge::init_test(deployer);
let recipient = aptos_framework::account::create_account_for_test(recipient);
token::opt_in_direct_transfer(&recipient, true);
let token_id1 = external_address::from_bytes(x"01");
let token_id2 = external_address::from_bytes(x"02");
complete_transfer_wrapped_helper(&recipient, token_id1);
complete_transfer_wrapped_helper(&recipient, token_id2);
}
/// Helper function that transfer a token into the recipient address
fun complete_transfer_wrapped_helper(
recipient_signer: &signer,
token_id: ExternalAddress
) {
let recipient = signer::address_of(recipient_signer);
let recipient_external = external_address::left_pad(&bcs::to_bytes(&recipient));
token::opt_in_direct_transfer(recipient_signer, true);
let token_address = external_address::from_bytes(x"09");
let token_chain = u16::from_u64(14);
let token_symbol = string32::from_bytes(b"my symbol");
let token_name = string32::from_bytes(b"my name");
let token_uri = uri::from_bytes(b"http://google.com");
let t = transfer::create(
token_address,
token_chain,
token_symbol,
token_name,
token_id,
token_uri,
recipient_external,
u16::from_u64(22) // to chain
);
// complete transfer using transfer object above
complete_transfer::test(&t);
let origin_info = state::create_origin_info(token_chain, token_address);
let creator = state::get_wrapped_asset_signer(origin_info);
let expected_collection_name = string::utf8(b"my name");
let expected_token_name = wrapped_token_name::render_hex(external_address::get_bytes(&token_id));
let token_id = token::create_token_id_raw(
signer::address_of(&creator),
expected_collection_name,
expected_token_name,
0
);
assert!(token::balance_of(recipient, token_id) == 1, 0);
}
// ------ Native asset tests
#[test(
deployer = @deployer,
creator = @0x654321,
first_user = @0x123456,
second_user = @0x121212
)]
fun complete_transfer_native_test(
deployer: &signer,
creator: address,
first_user: &signer,
second_user: &signer
) {
let collection_name = string::utf8(b"my test collection");
let token_name = string::utf8(b"my test token");
complete_transfer_native_helper(
deployer,
creator,
first_user,
second_user,
collection_name,
token_name,
true,
22
);
}
#[test(
deployer = @deployer,
creator = @0x654321,
first_user = @0x123456,
second_user = @0x121212
)]
#[expected_failure(abort_code = 1, location = nft_bridge::complete_transfer)]
fun complete_transfer_native_test_incorrect_token_address(
deployer: &signer,
creator: address,
first_user: &signer,
second_user: &signer
) {
let collection_name = string::utf8(b"my test collection");
let token_name = string::utf8(b"my test token");
complete_transfer_native_helper(
deployer,
creator,
first_user,
second_user,
collection_name,
token_name,
false,
22
);
}
#[test(
deployer = @deployer,
creator = @0x654321,
first_user = @0x123456,
second_user = @0x121212
)]
#[expected_failure(abort_code = 0, location = nft_bridge::complete_transfer)]
fun complete_transfer_native_test_incorrect_target_chain(
deployer: &signer,
creator: address,
first_user: &signer,
second_user: &signer
) {
let collection_name = string::utf8(b"my test collection");
let token_name = string::utf8(b"my test token");
complete_transfer_native_helper(
deployer,
creator,
first_user,
second_user,
collection_name,
token_name,
true,
21
);
}
/// Helper function for performing a variety of tests that all follow the
/// same flow.
///
/// This function performs the setup, then creates a collection under
/// `creator`, then mints a token to `first_user`, then transfers out that
/// token.
/// Finally, it transfers the token back in to `second_user`.
fun complete_transfer_native_helper(
deployer: &signer,
creator: address,
first_user: &signer,
second_user: &signer,
collection_name: String,
token_name: String,
// if this flag is `true`, the 'token_address' field in the incoming
// transfer will be one matching the collection hash of the token,
// otherwise an arbitary address
use_correct_token_address: bool,
// this flag determines the target chain of the incoming transfer
// (only 22 should be accepted)
target_chain: u64
) {
// ------ Setup
wormhole::wormhole_test::setup(0);
nft_bridge::nft_bridge::init_test(deployer);
aptos_framework::aptos_account::create_account(signer::address_of(first_user));
token::opt_in_direct_transfer(first_user, true);
aptos_framework::aptos_account::create_account(signer::address_of(second_user));
token::opt_in_direct_transfer(second_user, true);
let creator = account::create_account_for_test(creator);
// ------ Create a collection under `creator`
transfer_nft_test::create_collection(&creator, collection_name);
// ------ Mint a token to `first_user` from the collection we just created
transfer_nft_test::mint_token_to(
&creator,
signer::address_of(first_user),
collection_name,
token_name,
1
);
// ------ Construct token_id for follow-up queries
let token_id = token::create_token_id_raw(
signer::address_of(&creator),
collection_name,
token_name,
0 // property_version
);
let (collection_hash, token_hash) = token_hash::derive(&token_id);
// ------ Transfer the tokens
transfer_nft::transfer_nft_entry(
first_user,
signer::address_of(&creator),
collection_name,
token_name,
0, // property_version
3, // recipient chain
x"0000000000000000000000000000000000000000000000000000000000FAFAFA",
0
);
// ------ Check that `nft_bridge` now holds the token
assert!(token::balance_of(@nft_bridge, token_id) == 1, 0);
let expected_token_address: ExternalAddress;
if (use_correct_token_address) {
expected_token_address = token_hash::get_collection_external_address(&collection_hash);
} else {
expected_token_address = external_address::from_bytes(x"0123");
};
let t = transfer::create(
expected_token_address,
u16::from_u64(22),
string32::from_bytes(b"symbol (ignored)"),
string32::from_bytes(b"name (ignored)"),
token_hash::get_token_external_address(&token_hash),
uri::from_bytes(b"uri (ignored)"),
external_address::from_bytes(std::bcs::to_bytes(&signer::address_of(second_user))),
u16::from_u64(target_chain)
);
// complete transfer using transfer object above
complete_transfer::test(&t);
// ------ Check that `second_user` now holds the token
assert!(token::balance_of(signer::address_of(second_user), token_id) == 1, 0);
}
}

View File

@ -0,0 +1,221 @@
/// 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 nft_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 nft_bridge::vaa as nft_bridge_vaa;
use nft_bridge::state;
/// "NFTBridge" (left padded)
const NFT_BRIDGE: vector<u8> = x"00000000000000000000000000000000000000000000004e4654427269646765";
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 has drop {
hash: vector<u8>
}
public fun get_hash(hash: &Hash): vector<u8> {
hash.hash
}
fun parse_payload(payload: vector<u8>): Hash {
let cur = cursor::init(payload);
let target_module = deserialize::deserialize_vector(&mut cur, 32);
assert!(target_module == NFT_BRIDGE, E_INVALID_MODULE);
let action = deserialize::deserialize_u8(&mut cur);
assert!(action == 0x2, 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 fun submit_vaa(vaa: vector<u8>): Hash acquires UpgradeAuthorized {
let vaa = vaa::parse_and_verify(vaa);
vaa::assert_governance(&vaa);
nft_bridge_vaa::replay_protect(&vaa);
let hash = parse_payload(vaa::destroy(vaa));
authorize_upgrade(&hash);
hash
}
public entry fun submit_vaa_entry(vaa: vector<u8>) acquires UpgradeAuthorized {
submit_vaa(vaa);
}
fun authorize_upgrade(hash: &Hash) acquires UpgradeAuthorized {
let nft_bridge = state::nft_bridge_signer();
if (exists<UpgradeAuthorized>(@nft_bridge)) {
// NOTE: here we're dropping the upgrade hash, allowing to override
// a previous upgrade that hasn't been executed. It's possible that
// an upgrade hash corresponds to bytecode that can't be upgraded
// to, because it fails bytecode compatibility verification. While
// that should never happen^TM, we don't want to deadlock the
// contract if it does.
let UpgradeAuthorized { hash: _ } = move_from<UpgradeAuthorized>(@nft_bridge);
};
move_to(&nft_bridge, UpgradeAuthorized { hash: hash.hash });
}
#[test_only]
public fun authorized_hash(): vector<u8> acquires UpgradeAuthorized {
let u = borrow_global<UpgradeAuthorized>(@nft_bridge);
u.hash
}
// -----------------------------------------------------------------------------
// Reveal
public entry fun upgrade(
metadata_serialized: vector<u8>,
code: vector<vector<u8>>
) acquires UpgradeAuthorized {
assert!(exists<UpgradeAuthorized>(@nft_bridge), E_UPGRADE_UNAUTHORIZED);
let UpgradeAuthorized { hash } = move_from<UpgradeAuthorized>(@nft_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 nft_bridge = state::nft_bridge_signer();
code::publish_package_txn(&nft_bridge, metadata_serialized, code);
// allow migration to be run.
if (!exists<Migrating>(@nft_bridge)) {
move_to(&nft_bridge, Migrating {});
}
}
// -----------------------------------------------------------------------------
// Migration
struct Migrating has key {}
public fun is_migrating(): bool {
exists<Migrating>(@nft_bridge)
}
public entry fun migrate() acquires Migrating {
assert!(exists<Migrating>(@nft_bridge), E_NOT_MIGRATING);
let Migrating { } = move_from<Migrating>(@nft_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 nft_bridge::contract_upgrade_test {
use wormhole::wormhole;
use nft_bridge::contract_upgrade;
use nft_bridge::nft_bridge;
/// A nft bridge upgrade VAA that upgrades to 0x10263f154c466b139fda0bf2caa08fd9819d8ded3810446274a99399f886fc76
const UPGRADE_VAA: vector<u8> = x"0100000000010017876a4ed8cbe1bb0485b836414a271fbfc8ed9e61368645111ccd3dce1020a03417e943829e5e4a67d91a55913b2bcacd3bf066239b07ccf2261ef1b9b22eca000000000100000001000100000000000000000000000000000000000000000000000000000000000000040000000002e0d5010000000000000000000000000000000000000000000000004e465442726964676502001610263f154c466b139fda0bf2caa08fd9819d8ded3810446274a99399f886fc76";
/// A nft bridge upgrade VAA that targets ethereum
const ETH_UPGRADE: vector<u8> = x"010000000001004898e22dbdfd1d3d3b671414d06d8e0656cf20316f636f710bc54d80e34a0b3b781a034976e20982a19d06864c8d939651f1b4fd2d109fe469bd49f8bd2125b301000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000110ba0c0000000000000000000000000000000000000000000000004e465442726964676502000210263f154c466b139fda0bf2caa08fd9819d8ded3810446274a99399f886fc76";
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
);
nft_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, location = 0x1::table)]
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, location = nft_bridge::contract_upgrade)]
public fun test_contract_upgrade_wrong_chain(deployer: &signer) {
setup(deployer);
contract_upgrade::submit_vaa(ETH_UPGRADE);
}
}

View File

@ -0,0 +1,102 @@
/// The `uri` module defines the `URI` type which represents UTF8 encoded
/// strings that are at most 200 characters long, used for representing the URI
/// field of an NFT. Since these strings are not fixed length, their binary
/// encoding includes a length byte prefix.
module nft_bridge::uri {
use std::string::{Self, String};
use std::vector;
use wormhole::cursor::Cursor;
use wormhole::deserialize;
use wormhole::serialize;
const MAX_LENGTH: u64 = 200;
const E_URI_TOO_LONG: u64 = 0;
/// A `URI` holds a ut8 string which is guaranteed to be at most 200 characters long
struct URI has copy, drop, store {
string: String
}
spec URI {
invariant string::length(string) <= MAX_LENGTH;
}
/// Truncates a string to a URI.
/// Does not abort.
public fun from_string(s: &String): URI {
from_bytes(*string::bytes(s))
}
/// Truncates a byte vector to a URI.
/// Does not abort.
public fun from_bytes(b: vector<u8>): URI {
assert!(vector::length(&b) <= MAX_LENGTH, E_URI_TOO_LONG);
URI { string: string::utf8(b) }
}
/// Converts `URI` to `String`
public fun to_string(u: &URI): String {
u.string
}
/// Converts `String32` to a byte vector of length 32.
public fun to_bytes(u: &URI): vector<u8> {
*string::bytes(&u.string)
}
public fun deserialize(cur: &mut Cursor<u8>): URI {
let len = (deserialize::deserialize_u8(cur) as u64);
assert!(len <= MAX_LENGTH, E_URI_TOO_LONG);
let bytes = deserialize::deserialize_vector(cur, len);
from_bytes(bytes)
}
public fun serialize(buf: &mut vector<u8>, e: URI) {
let bytes = to_bytes(&e);
serialize::serialize_u8(buf, (vector::length(&bytes) as u8));
serialize::serialize_vector(buf, to_bytes(&e))
}
}
#[test_only]
module nft_bridge::uri_test {
use nft_bridge::uri;
#[test]
public fun test_uri_from_bytes_valid() {
let utf8 = b"hello world";
uri::from_bytes(utf8);
}
// The input string here is not a valid utf8 bytestring
#[test]
#[expected_failure(abort_code = 1, location = std::string)]
public fun test_uri_from_bytes_invalid() {
let not_utf8 = x"afafafaf";
uri::from_bytes(not_utf8);
}
// The string is longer than 200 characters, in which case we abort.
#[test]
#[expected_failure(abort_code = 0, location = nft_bridge::uri)]
public fun test_uri_too_long() {
let too_long = std::string::utf8(b"this string is very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long");
uri::from_string(&too_long);
}
#[test]
public fun test_serialize_roundtrip() {
let uri = uri::from_bytes(b"hello world");
let vec = std::vector::empty();
uri::serialize(&mut vec, uri);
let c = wormhole::cursor::init(vec);
let uri2 = uri::deserialize(&mut c);
wormhole::cursor::destroy_empty(c);
assert!(uri == uri2, 0);
}
}

View File

@ -0,0 +1,27 @@
module nft_bridge::nft_bridge {
#[test_only]
use aptos_framework::account::{Self};
use aptos_framework::account::{SignerCapability};
use deployer::deployer::{claim_signer_capability};
use nft_bridge::state::{init_nft_bridge_state};
use wormhole::wormhole;
/// Initializes the contract.
entry fun init_module(deployer: &signer) {
let signer_cap = claim_signer_capability(deployer, @nft_bridge);
init_internal(signer_cap);
}
fun init_internal(signer_cap: SignerCapability) {
let emitter_cap = wormhole::register_emitter();
init_nft_bridge_state(signer_cap, emitter_cap);
}
#[test_only]
/// Initialise contracts for testing
/// Returns the nft_bridge signer and wormhole signer
public fun init_test(deployer: &signer) {
let (_nft_bridge, signer_cap) = account::create_resource_account(deployer, b"nft_bridge");
init_internal(signer_cap);
}
}

View File

@ -0,0 +1,165 @@
module nft_bridge::register_chain {
use wormhole::u16::{Self, U16};
use wormhole::cursor;
use wormhole::deserialize;
use wormhole::vaa;
use wormhole::external_address::{Self, ExternalAddress};
use nft_bridge::vaa as nft_bridge_vaa;
use nft_bridge::state;
/// "NFTBridge" (left padded)
const NFT_BRIDGE: vector<u8> = x"00000000000000000000000000000000000000000000004e4654427269646765";
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,
}
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]
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 == NFT_BRIDGE, E_INVALID_MODULE);
let action = deserialize::deserialize_u8(&mut cur);
assert!(action == 0x01, E_INVALID_ACTION);
// NOTE: currently we only allow VAAs targeting the "0" chain (which is
// how registration VAAs are produced via governance.) Technically it
// would be possible to produce a VAA targeting only a single chain, but
// it's unclear if that would ever happen in practice.
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 fun submit_vaa(vaa: vector<u8>): RegisterChain {
let vaa = vaa::parse_and_verify(vaa);
vaa::assert_governance(&vaa); // not tested
nft_bridge_vaa::replay_protect(&vaa);
let register_chain = parse_payload(vaa::destroy(vaa));
state::set_registered_emitter(get_emitter_chain_id(&register_chain), get_emitter_address(&register_chain));
register_chain
}
public entry fun submit_vaa_entry(vaa: vector<u8>) {
submit_vaa(vaa);
}
}
#[test_only]
module nft_bridge::register_chain_test {
use std::option;
use wormhole::u16;
use nft_bridge::register_chain;
use wormhole::vaa;
use wormhole::wormhole;
use wormhole::external_address;
use nft_bridge::nft_bridge;
use nft_bridge::state;
/// Registration VAA for the etheruem NFT bridge 0xdeadbeef
const ETHEREUM_NFT_REG: vector<u8> = x"0100000000010062e307224ed7a222234012fe1cd38450076ef30eb488a1cfac75ec16476d750959c9851e7549aeb16a99c73d381a9c416657567ae6ae46613b9236d511a8a5b601000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000162f5340000000000000000000000000000000000000000000000004e4654427269646765010000000200000000000000000000000000000000000000000000000000000000deadbeef";
/// Registration VAA for the etheruem NFT bridge 0xbeefface
const ETHEREUM_NFT_REG_2: vector<u8> = x"01000000000100d7984b0abd82cbf9e39b74d16ada7cb43c9476fd4b9656a02f40eca2d1ffa560049dc5265946a88e7f643f8321cd5417e388b86580ed4c3a03f73d5599d4a9ed010000000100000001000100000000000000000000000000000000000000000000000000000000000000040000000000312bca0000000000000000000000000000000000000000000000004e4654427269646765010000000200000000000000000000000000000000000000000000000000000000beefface";
/// Registration VAA for the etheruem token bridge 0xdeadbeef
const ETHEREUM_TOKEN_REG: vector<u8> = x"0100000000010015d405c74be6d93c3c33ed6b48d8db70dfb31e0981f8098b2a6c7583083e0c3343d4a1abeb3fc1559674fa067b0c0e2e9de2fafeaecdfeae132de2c33c9d27cc0100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000016911ae00000000000000000000000000000000000000000000546f6b656e427269646765010000000200000000000000000000000000000000000000000000000000000000deadbeef";
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
);
nft_bridge::init_test(deployer);
}
#[test]
public fun test_parse() {
let vaa = vaa::parse_test(ETHEREUM_NFT_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, location = nft_bridge::register_chain)]
public fun test_parse_fail() {
let vaa = vaa::parse_test(ETHEREUM_TOKEN_REG);
// this should fail because it's an token bridge 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_NFT_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, location = 0x1::table)]
public fun test_replay_protect(deployer: &signer) {
setup(deployer);
register_chain::submit_vaa(ETHEREUM_NFT_REG);
register_chain::submit_vaa(ETHEREUM_NFT_REG);
}
#[test(deployer = @deployer)]
public fun test_re_registration(deployer: &signer) {
test_registration(deployer);
// we register aptos again, which overrides the
// previous one. This deviates from other chains (where this is
// rejected), but I think this is the right behaviour.
register_chain::submit_vaa(ETHEREUM_NFT_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,299 @@
module nft_bridge::state {
use std::table::{Self, Table};
use std::option::{Self, Option};
use std::string::String;
use aptos_framework::account::{Self, SignerCapability};
use aptos_framework::aptos_coin::AptosCoin;
use aptos_framework::coin::Coin;
use aptos_token::token::{Self, TokenId};
use wormhole::u16::{Self, U16};
use wormhole::emitter::EmitterCapability;
use wormhole::state;
use wormhole::wormhole;
use wormhole::set::{Self, Set};
use wormhole::external_address::{Self, ExternalAddress};
use token_bridge::string32::{Self, String32};
use nft_bridge::token_hash::{Self, TokenHash};
use nft_bridge::wrapped_token_name;
friend nft_bridge::contract_upgrade;
friend nft_bridge::register_chain;
friend nft_bridge::nft_bridge;
friend nft_bridge::vaa;
friend nft_bridge::wrapped;
friend nft_bridge::complete_transfer;
friend nft_bridge::transfer_nft;
#[test_only]
friend nft_bridge::wrapped_test;
#[test_only]
friend nft_bridge::vaa_test;
#[test_only]
friend nft_bridge::transfer_nft_test;
#[test_only]
friend nft_bridge::complete_transfer_test;
const E_ORIGIN_CHAIN_MISMATCH: u64 = 0;
const E_ORIGIN_ADDRESS_MISMATCH: u64 = 1;
const W_WRAPPING_NATIVE_NFT: u64 = 2;
const E_WRAPPED_ASSET_NOT_INITIALIZED: u64 = 3;
/// The origin chain and address of a token (represents the origin of a collection)
struct OriginInfo has key, store, copy, drop {
/// Chain from which the token originates
token_chain: U16,
/// Address of the collection (unique per chain)
/// For native tokens, it's derived as the hash of (creator || hash(collection))
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_chain, token_address }
}
struct WrappedInfo has store {
signer_cap: SignerCapability,
/// The token's symbol in the NFT bridge standard does not map to any of
/// the fields in the Aptos NFT standard, so there's no natural way to
/// store that information when creating a wrapped NFT collection.
/// However, when transferring out these assets, we want to preserve the
/// original symbol, so we do that here.
symbol: String32
}
/// See `is_unified_solana_collection` for the purpose of this type
/// It has the `drop` ability so old cache entries can be overridden
struct SPLCacheEntry has store, drop {
name: String32,
symbol: String32
}
struct State has key, store {
/// 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(TokenId) 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, TokenId>,
signer_cap: SignerCapability,
emitter_cap: EmitterCapability,
/// Mapping of bridge contracts on other chains
registered_emitters: Table<U16, ExternalAddress>,
/// See `is_unified_solana_collection` for the purpose
/// of this field.
/// Mapping of token_id => spl cache entry
spl_cache: Table<ExternalAddress, SPLCacheEntry>,
}
// getters
public fun vaa_is_consumed(hash: vector<u8>): bool acquires State {
let state = borrow_global<State>(@nft_bridge);
set::contains(&state.consumed_vaas, hash)
}
/// Returns the origin information for a token
public fun get_origin_info(token_id: &TokenId): (OriginInfo, ExternalAddress) acquires OriginInfo {
if (is_wrapped_asset(token_id)) {
let (creator, _, token_name, _) = token::get_token_id_fields(token_id);
let external_address = wormhole::external_address::from_bytes(wrapped_token_name::parse_hex(token_name));
(*borrow_global<OriginInfo>(creator), external_address)
} else {
let token_chain = state::get_chain_id();
let (collection_hash, token_hash) = token_hash::derive(token_id);
let token_address = token_hash::get_collection_external_address(&collection_hash);
let token_id = token_hash::get_token_external_address(&token_hash);
(OriginInfo { token_chain, token_address }, token_id)
}
}
public fun get_registered_emitter(chain_id: U16): Option<ExternalAddress> acquires State {
let state = borrow_global<State>(@nft_bridge);
if (table::contains(&state.registered_emitters, chain_id)) {
option::some(*table::borrow(&state.registered_emitters, chain_id))
} else {
option::none()
}
}
public fun is_wrapped_asset(token_id: &TokenId): bool {
let (creator, _, _, _) = token::get_token_id_fields(token_id);
exists<OriginInfo>(creator)
}
public fun get_spl_cache(token_id: ExternalAddress): (String32, String32) acquires State {
let state = borrow_global<State>(@nft_bridge);
let SPLCacheEntry { name, symbol } = table::borrow(&state.spl_cache, token_id);
(*name, *symbol)
}
public(friend) fun set_spl_cache(token_id: ExternalAddress, name: String32, symbol: String32) acquires State {
let state = borrow_global_mut<State>(@nft_bridge);
table::upsert(&mut state.spl_cache, token_id, SPLCacheEntry { name, symbol });
}
public(friend) fun setup_wrapped(
origin_info: OriginInfo
) acquires State {
assert!(origin_info.token_chain != state::get_chain_id(), W_WRAPPING_NATIVE_NFT);
let wrapped_infos = &mut borrow_global_mut<State>(@nft_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);
}
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>(@nft_bridge).emitter_cap;
wormhole::publish_message(
emitter_cap,
nonce,
payload,
message_fee
)
}
public(friend) fun nft_bridge_signer(): signer acquires State {
account::create_signer_with_capability(&borrow_global<State>(@nft_bridge).signer_cap)
}
// setters
public(friend) fun set_vaa_consumed(hash: vector<u8>) acquires State {
let state = borrow_global_mut<State>(@nft_bridge);
set::add(&mut state.consumed_vaas, hash);
}
public(friend) fun set_registered_emitter(chain_id: U16, bridge_contract: ExternalAddress) acquires State {
let state = borrow_global_mut<State>(@nft_bridge);
table::upsert(&mut state.registered_emitters, chain_id, bridge_contract);
}
// 32-byte native asset address => token info
public(friend) fun set_native_asset_info(token_id: TokenId) acquires State {
let (_, token_hash) = token_hash::derive(&token_id);
let state = borrow_global_mut<State>(@nft_bridge);
let native_infos = &mut state.native_infos;
if (!table::contains(native_infos, token_hash)) {
table::add(native_infos, token_hash, token_id);
}
}
public(friend) fun get_native_asset_info(token_hash: TokenHash): TokenId acquires State {
*table::borrow(&borrow_global<State>(@nft_bridge).native_infos, token_hash)
}
public(friend) fun set_wrapped_asset_info(
token: OriginInfo,
signer_cap: SignerCapability,
symbol: String32
) acquires State {
let state = borrow_global_mut<State>(@nft_bridge);
let wrapped_info = WrappedInfo {
signer_cap,
symbol
};
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_infos
= &borrow_global<State>(@nft_bridge).wrapped_infos;
let wrapped_info = table::borrow(wrapped_coin_infos, origin_info);
account::create_signer_with_capability(&wrapped_info.signer_cap)
}
public fun get_wrapped_asset_name_and_symbol(
origin_info: OriginInfo,
collection_name: String,
token_id: ExternalAddress
): (String32, String32) acquires State {
if (is_unified_solana_collection(origin_info)) {
get_spl_cache(token_id)
} else {
let wrapped_coin_infos
= &borrow_global<State>(@nft_bridge).wrapped_infos;
let wrapped_info = table::borrow(wrapped_coin_infos, origin_info);
(string32::from_string(&collection_name), wrapped_info.symbol)
}
}
public(friend) fun wrapped_asset_signer_exists(origin_info: OriginInfo): bool acquires State {
let wrapped_coin_signer_caps
= &borrow_global<State>(@nft_bridge).wrapped_infos;
table::contains(wrapped_coin_signer_caps, origin_info)
}
/// Tokens from Solana currently all have a token_address of [1u8; 32], i.e.
/// 32 1-bytes. This was originally devised back when Solana NFTs didn't
/// have collection information, and minting each NFT into a different
/// contract would have been too expensive on Eth, so instead all Solana
/// NFTs appear to originate from a single collection.
///
/// This requires some additional bookkeping however, in particular the name
/// and symbol of the original collection are no longer retrievable from
/// just the wrapped collection, so we need to store those separately.
///
/// This function determines whether a Solana NFT is to be minted into the
/// unified collection. In addition to checking the source chain, we also
/// check the token address. Doing so is future proof: when the Solana
/// implementation is ugpraded to use the collection key as opposed to the
/// dummy address as the token_address, newly transferred NFTs will simply
/// be minted into their respective collections without needing to upgrade
/// the aptos contract.
public fun is_unified_solana_collection(origin_info: OriginInfo): bool {
let token_chain = get_origin_info_token_chain(&origin_info);
let token_address = get_origin_info_token_address(&origin_info);
let dummy_address = x"0101010101010101010101010101010101010101010101010101010101010101";
token_chain == u16::from_u64(1) && token_address == external_address::from_bytes(dummy_address)
}
public(friend) fun init_nft_bridge_state(
signer_cap: SignerCapability,
emitter_cap: EmitterCapability
) {
let nft_bridge = account::create_signer_with_capability(&signer_cap);
move_to(&nft_bridge, State {
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(),
spl_cache: table::new(),
}
);
}
}

View File

@ -0,0 +1,206 @@
module nft_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 nft_bridge::uri::{Self, URI};
use token_bridge::string32::{Self, String32};
friend nft_bridge::transfer_nft;
#[test_only]
friend nft_bridge::complete_transfer_test;
#[test_only]
friend nft_bridge::transfer_test;
#[test_only]
friend nft_bridge::wrapped_test;
const E_INVALID_ACTION: u64 = 0;
struct Transfer has drop {
/// Address of the token. Left-zero-padded if shorter than 32 bytes
token_address: ExternalAddress,
/// Chain ID of the token
token_chain: U16,
/// Symbol of the token
symbol: String32,
/// Name of the token
name: String32,
/// Token ID
token_id: ExternalAddress,
/// URI of the token metadata
uri: URI,
/// Address of the recipient. Left-zero-padded if shorter than 32 bytes
to: ExternalAddress,
/// Chain ID of the recipient
to_chain: U16,
}
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_symbol(a: &Transfer): String32 {
a.symbol
}
public fun get_name(a: &Transfer): String32 {
a.name
}
public fun get_token_id(a: &Transfer): ExternalAddress {
a.token_id
}
public fun get_uri(a: &Transfer): URI {
a.uri
}
public fun get_to(a: &Transfer): ExternalAddress {
a.to
}
public fun get_to_chain(a: &Transfer): U16 {
a.to_chain
}
public(friend) fun create(
token_address: ExternalAddress,
token_chain: U16,
symbol: String32,
name: String32,
token_id: ExternalAddress,
uri: URI,
to: ExternalAddress,
to_chain: U16,
): Transfer {
Transfer {
token_address,
token_chain,
symbol,
name,
token_id,
uri,
to,
to_chain,
}
}
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 token_address = external_address::deserialize(&mut cur);
let token_chain = deserialize_u16(&mut cur);
let symbol = string32::deserialize(&mut cur);
let name = string32::deserialize(&mut cur);
let token_id = external_address::deserialize(&mut cur);
let uri = uri::deserialize(&mut cur);
let to = external_address::deserialize(&mut cur);
let to_chain = deserialize_u16(&mut cur);
cursor::destroy_empty(cur);
Transfer {
token_address,
token_chain,
symbol,
name,
token_id,
uri,
to,
to_chain,
}
}
public fun encode(transfer: &Transfer): vector<u8> {
let encoded = vector::empty<u8>();
serialize_u8(&mut encoded, 1);
external_address::serialize(&mut encoded, transfer.token_address);
serialize_u16(&mut encoded, transfer.token_chain);
string32::serialize(&mut encoded, transfer.symbol);
string32::serialize(&mut encoded, transfer.name);
external_address::serialize(&mut encoded, transfer.token_id);
uri::serialize(&mut encoded, transfer.uri);
external_address::serialize(&mut encoded, transfer.to);
serialize_u16(&mut encoded, transfer.to_chain);
encoded
}
}
#[test_only]
module nft_bridge::transfer_test {
use nft_bridge::transfer;
use nft_bridge::uri;
use token_bridge::string32;
use wormhole::external_address;
use wormhole::u16;
// VAA from https://etherscan.io/tx/0x8250625a8dfb66ecc9d5b8fd188057ef332c0a1d09a1510131f7b104e8dbf79b
const SOLANA_NFT: vector<u8> = x"01000000010d004ee8f9a0a898aedc2340289880ef662b55333cb4a9a374282f494706a780815e5462aff9e2f59da0a7402486ad5a5ea4e5604c4c3270c269dde700ff63011a7600022f1cd8622698fdbe116477e164175243271dc5a47a649fbbfc5ab44d7c0c8efc1ec9df05f7543dea0f67faa84ac53953bf57dd7a674dfe124fbc941e5471d5100103b63085d5a9fdc531d6e04f907fd1bd80217016878bf44fea1f41f5408a1e62426bf64e115b732aa53154f199f4299a5b1a4110ec3dbc1d69f4f4e386489da98e0104e46ac1fc4c35b8835191c9ec8f7146a212b33ce8592f8311addbcbed9fdd3caa257cb43cd07de39cc76f17edee89fb424f5797b08a71ab8a913b1c899d0182f3000561d3ba8f0ea84da5ccdf916ab22ed2c55b5ab2a2d0bfb8d6e4487285bb1817fb272b74aba27822045ad1b5dfd4378e8aadfef290c1c13007f7bd2f4f611cf32800070b83d82dd6dc975c1bcaefb8acb0e8127015805940d89366bf1d86988650e809522a29bf1c30393cf1bf46d5f0beefbe01dde1f20807116148e8dd20d339c24b00090f44858974ad211f9d21d7d59eb1b10207f7c1ae4591d3eaaef86afcc7afbe926584e38b58fc5d441890b905897346f1a8f9fca48217509a0415f06f80586804010a1de1bf4416235e41d56d5634161eb8544f3d1c82a5ee666c13402ad8652e706112af89e902974b61c4be8d253c24eb858872cf80d556bc8288487fd18c9673f8010c104c3c79c2224fa5bd04ab2737487cc2b49f05096237d3678ccbe57b92791471473e735c09659083758702ba241f99d6550a02b11cc43ba407c2eb1a294eb491010d436bc9540cef59afa60764c5d43640d854ff847f3afce0844c7de085e0e45df34693871393e701d15a7a5385c4c394c394b2d6427b7f16b57c89bd8d1e83a721000e56baf9369bcef85e1b705a7e5904f576101ff28dddce6078cfb1ee9ce95170c70ff05e6a8ff500b3a44877894fec08fd16244764675127c2497200301e76bb13010f1e8b4335050134d419ff985ba3d26f734eb19e78a0a3b39739a2eb9993eeb62c19569ac040f535e6f9093366fce418f127fe548dff25cba8d3b9b65f24306cc80010ae2a582ba0435d155115b6bfe9c92b5f7c0147670bd4b6d815f9507f689f80f7495114c0e742c108dc982038be28e89bde5124306b6cc39cbb3fd4ed32c74783006149baf70000cb2800010def15a24423e1edd1a5ab16f557b9060303ddbab8c803d2ee48f4b78a1cfd6b000000000000000001010101010101010101010101010101010101010101010101010101010101010101000154535400000000000000000000000000000000000000000000000000000000005465737400000000000000000000000000000000000000000000000000000000c4a79ff9105f87c3ed880ed4252b61759251acc6050c163c0a5d828d92dc0cb8c868747470733a2f2f6172742d73616e64626f782e73756e666c6f7765722e696e64757374726965732f746f6b656e2f3078643538343334663333613230363631663138366666363736323665613662646634316238306263612f393630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000096d13cbeffe7bae169b9032fe69ed56eb07b300f0002";
#[test]
public fun parse_sol_transfer() {
let vaa = wormhole::vaa::parse_test(SOLANA_NFT);
let parsed_transfer = transfer::parse(wormhole::vaa::destroy(vaa));
let token_address = external_address::from_bytes(x"0101010101010101010101010101010101010101010101010101010101010101");
let token_chain = u16::from_u64(1);
let symbol = string32::from_bytes(b"TST");
let name = string32::from_bytes(b"Test");
let to = external_address::from_bytes(x"00000000000000000000000096d13cbeffe7bae169b9032fe69ed56eb07b300f");
let token_id = external_address::from_bytes(x"c4a79ff9105f87c3ed880ed4252b61759251acc6050c163c0a5d828d92dc0cb8");
// The VAA uses all 200 characters and pads with a bunch of 0 bytes at the end
let uri = uri::from_bytes(b"https://art-sandbox.sunflower.industries/token/0xd58434f33a20661f186ff67626ea6bdf41b80bca/960\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
let to_chain = u16::from_u64(2);
let transfer = transfer::create(
token_address,
token_chain,
symbol,
name,
token_id,
uri,
to,
to_chain,
);
assert!(parsed_transfer == transfer, 0);
}
#[test]
public fun parse_roundtrip() {
let token_address = external_address::from_bytes(x"beef");
let token_chain = u16::from_u64(1);
let symbol = string32::from_bytes(b"HELLO");
let name = string32::from_bytes(b"hello token");
let to = external_address::from_bytes(x"cafe");
let token_id = external_address::from_bytes(x"beefcafe");
let uri = uri::from_bytes(b"http://google.com");
let to_chain = u16::from_u64(7);
let transfer = transfer::create(
token_address,
token_chain,
symbol,
name,
token_id,
uri,
to,
to_chain,
);
let parsed_transfer = transfer::parse(transfer::encode(&transfer));
assert!(parsed_transfer == transfer, 0);
}
}

View File

@ -0,0 +1,117 @@
/// A pair of 32 byte hashes representing an arbitrary Aptos NFT, to be used in
/// VAAs to refer to NFTs.
module nft_bridge::token_hash {
use aptos_token::token::{Self, TokenId};
use std::bcs;
use std::hash;
use std::string;
use std::vector;
use wormhole::serialize;
use wormhole::external_address::{Self, ExternalAddress};
/// Hash of (creator || hash(collection name)), which uniquely identifies a
/// collection on Aptos
struct CollectionHash has drop, copy, store {
// 32 bytes
hash: vector<u8>,
}
/// Hash of (creator || hash(collection name) || hash(token name) || property version), which
/// uniquely identifies a token on Aptos
struct TokenHash has drop, copy, store {
// 32 bytes
hash: vector<u8>,
}
#[test_only]
public fun get_collection_hash_bytes(x: &CollectionHash): vector<u8>{
return x.hash
}
#[test_only]
public fun get_token_hash_bytes(x: &TokenHash): vector<u8>{
return x.hash
}
public fun get_collection_external_address(a: &CollectionHash): ExternalAddress {
external_address::from_bytes(a.hash)
}
public fun get_token_external_address(a: &TokenHash): ExternalAddress {
external_address::from_bytes(a.hash)
}
public fun from_external_address(a: ExternalAddress): TokenHash {
TokenHash { hash: external_address::get_bytes(&a) }
}
public fun derive(token_id: &TokenId): (CollectionHash, TokenHash) {
let ser = vector::empty<u8>();
// we hash all variable length fields (that is, collection and name)
let (creator, collection, name, property_version) = token::get_token_id_fields(token_id);
let creator_bytes = bcs::to_bytes(&creator);
serialize::serialize_vector(&mut ser, creator_bytes);
serialize::serialize_vector(&mut ser, hash::sha3_256(*string::bytes(&collection)));
let collection_hash = hash::sha3_256(ser);
serialize::serialize_vector(&mut ser, hash::sha3_256(*string::bytes(&name)));
serialize::serialize_u64(&mut ser, property_version);
let token_hash = hash::sha3_256(ser);
(CollectionHash { hash: collection_hash }, TokenHash { hash: token_hash })
}
}
#[test_only]
module nft_bridge::token_hash_test {
use std::string;
use aptos_token::token;
use nft_bridge::token_hash;
#[test(creator = @0x1234)]
public fun test_derive(creator: address) {
let token_id = token::create_token_id_raw(
creator,
string::utf8(b"my collection"),
string::utf8(b"my token"),
0
);
let (collection_hash, token_hash) = token_hash::derive(&token_id);
let collection_hash = token_hash::get_collection_hash_bytes(&collection_hash);
let token_hash = token_hash::get_token_hash_bytes(&token_hash);
assert!(collection_hash == x"18905beccb7e5a0f17d22e6773bd94886535fa39f4c28841a752c97a52c5eb46", 0);
assert!(token_hash == x"54ea5951232ad17f3dbb133964eb0463605e0e35dacd856a1881090d7f0218fe", 0);
}
// this test ensures that variable length fields can't be reshuffled to
// cause a collision
#[test(creator = @0x1234)]
public fun test_derive_no_rearrange(creator: address) {
let token_id_1 = token::create_token_id_raw(
creator,
string::utf8(b"my collection"),
string::utf8(b"my token"),
0
);
let token_id_2 = token::create_token_id_raw(
creator,
string::utf8(b"my collectionmy"),
string::utf8(b" token"),
0
);
let (collection_hash_1, token_hash_1) = token_hash::derive(&token_id_1);
let (collection_hash_2, token_hash_2) = token_hash::derive(&token_id_2);
let collection_hash_1 = token_hash::get_collection_hash_bytes(&collection_hash_1);
let token_hash_1 = token_hash::get_token_hash_bytes(&token_hash_1);
let collection_hash_2 = token_hash::get_collection_hash_bytes(&collection_hash_2);
let token_hash_2 = token_hash::get_token_hash_bytes(&token_hash_2);
assert!(collection_hash_1 != collection_hash_2, 0);
assert!(token_hash_1 != token_hash_2, 0);
}
}

View File

@ -0,0 +1,402 @@
module nft_bridge::transfer_nft {
use std::string::{String};
use aptos_framework::aptos_coin::{AptosCoin};
use aptos_framework::coin::{Self, Coin};
use aptos_token::token::{Self, Token};
use wormhole::u16::{Self, U16};
use wormhole::external_address::{Self, ExternalAddress};
use nft_bridge::state;
use nft_bridge::transfer;
use nft_bridge::uri::{Self, URI};
use token_bridge::string32::{Self, String32};
const E_AMOUNT_SHOULD_BE_ONE: u64 = 0;
const E_FUNGIBLE_TOKEN: u64 = 1;
public entry fun transfer_nft_entry(
sender: &signer,
creators_address: address,
collection: String,
name: String,
property_version: u64,
recipient_chain: u64,
recipient: vector<u8>,
nonce: u64
) {
let token_id = token::create_token_id_raw(creators_address, collection, name, property_version);
let token = token::withdraw_token(sender, token_id, 1);
let wormhole_fee = wormhole::state::get_message_fee();
let wormhole_fee_coins: Coin<AptosCoin>;
if (wormhole_fee > 0) {
wormhole_fee_coins = coin::withdraw<AptosCoin>(sender, wormhole_fee);
} else {
wormhole_fee_coins = coin::zero<AptosCoin>();
};
transfer_nft(
token,
wormhole_fee_coins,
u16::from_u64(recipient_chain),
external_address::from_bytes(recipient),
nonce
);
}
public fun transfer_nft(
token: Token,
wormhole_fee_coins: Coin<AptosCoin>,
recipient_chain: U16,
recipient: ExternalAddress,
nonce: u64
): u64 {
let (token_address,
token_chain,
symbol,
name,
token_id,
uri,
) = lock_or_burn(token);
let transfer = transfer::create(
token_address,
token_chain,
symbol,
name,
token_id,
uri,
recipient,
recipient_chain,
);
state::publish_message(
nonce,
transfer::encode(&transfer),
wormhole_fee_coins,
)
}
#[test_only]
public fun transfer_nft_test(
token: Token,
): (
ExternalAddress, // token_address
U16, // token_chain
String32, // symbol
String32, // name
ExternalAddress, // token_id
URI, // URI
) {
lock_or_burn(token)
}
/// Transfer a native (lock) or wrapped (burn) token from sender to nft_bridge.
/// Returns the token's address and native chain
fun lock_or_burn(token: Token): (
ExternalAddress, // token_address
U16, // token_chain
String32, // symbol
String32, // name
ExternalAddress, // token_id
URI, // URI
) {
// NOTE: the way aptos tokens are designed, it is possible to mint
// multiple copies of a token. See the README for an explanation
let amount = token::get_token_amount(&token);
assert!(amount == 1, E_AMOUNT_SHOULD_BE_ONE);
let token_id = token::get_token_id(&token);
let (creator, collection, name, property_version)
= token::get_token_id_fields(&token_id);
// We deposit the token into the nft bridge signer account.address,
// regardless of whether it is a wrapped or native token. In the native
// token case, this just means custodying the tokens as expected. In the
// wrapped case, we do this because there are only two ways to burn a
// token: either by the owner, or the creator: `burn` and
// `burn_by_creator` respectively. In both cases, the owner of the token
// must be known, so we deposit to the nft bridge first.
//
// tldr; first deposit token into nft bridge by convention, then if it is wrapped,
// then load the creator signer from nft bridge state and burn it using `burn_by_creator`.
// Disallow `burn` (by owner) to avoid edge cases
let nft_bridge: signer = state::nft_bridge_signer();
token::deposit_token(&nft_bridge, token);
let (origin_info, external_token_id) = state::get_origin_info(&token_id);
// We need to grab the URI *before* burning the NFT, otherwise its
// tokendata will no longer be available
let token_data_id = token::create_token_data_id(creator, collection, name);
let uri = uri::from_string(&token::get_tokendata_uri(creator, token_data_id));
// The symbol field will be set to empty for aptos native NFTs (as they
// do not have an equivalent field). For wrapped assets, it's simply
// preserved from the original metadata.
let symbol: String32;
let external_name: String32;
if (state::is_wrapped_asset(&token_id)) {
(external_name, symbol) = state::get_wrapped_asset_name_and_symbol(
origin_info,
collection,
external_token_id
);
// burn the wrapped token to remove it from circulation
let creator_signer = state::get_wrapped_asset_signer(origin_info);
token::burn_by_creator(
&creator_signer,
std::signer::address_of(&nft_bridge),
collection,
name,
property_version,
1
);
} else {
symbol = string32::from_bytes(b"");
external_name = string32::from_string(&collection);
// NOTE: The way Aptos Tokens are designed, it is possible to mint
// multiple copies of a token, if its property_version is 0, and the
// tokendata's `maximum` value is greater than 1. We could check
// that `maximum` is 1, but the `maximum` field can be mutated by
// the collection's creator by calling
// `token::mutate_tokendata_maximum`, so that check would not
// suffice. We could additionally check that the mutability config
// of the maximum field is set to immutable, which would ensure that
// if it's 1, then it stays 1. However, I expect most collections to
// just leave that field as `true` even if there's no intention of
// mutating that field.
//
// Instead, we just ensure that the NFT bridge can hold a maximum of
// 1 copy of this token. That is, even if the token has a supply
// larger than 1, only 1 of the tokens can be bridged out at any
// given time, so they effectively behave as NFTs outside of Aptos,
// even when they're tecnhically fungible on aptos.
assert!(token::balance_of(@nft_bridge, token_id) == 1, E_FUNGIBLE_TOKEN);
// if we're seeing this native token for the first time, store its token id
state::set_native_asset_info(token_id);
};
let token_chain = state::get_origin_info_token_chain(&origin_info);
let token_address = state::get_origin_info_token_address(&origin_info);
return (
token_address,
token_chain,
symbol,
external_name,
external_token_id,
uri,
)
}
}
#[test_only]
module nft_bridge::transfer_nft_test {
use std::signer;
use std::string::{Self, String};
use aptos_framework::account;
use aptos_token::token;
use nft_bridge::wrapped_test;
use nft_bridge::transfer_nft;
// test transfer wrapped NFT to another chain
#[test(deployer = @deployer, sender = @0x123456)]
public fun test_transfer_wrapped_nft(deployer: &signer, sender: &signer) {
wormhole::wormhole_test::setup(0);
nft_bridge::nft_bridge::init_test(deployer);
aptos_framework::aptos_account::create_account(signer::address_of(sender));
token::opt_in_direct_transfer(sender, true);
// ------ Create wrapped collection
let collection_name = string::utf8(b"collection name");
let creator = wrapped_test::create_wrapped_nft_collection(
signer::address_of(sender),
collection_name
);
// ------ Construct token_id for follow-up queries
// NOTE: the x"01" comes from the
// `wrapped_test::create_wrapped_nft_collection` function, and
// (currently) we render it as hex
let token_name = string::utf8(b"0000000000000000000000000000000000000000000000000000000000000001");
let token_id = token::create_token_id_raw(
signer::address_of(&creator),
collection_name,
token_name,
0 // property_version
);
// ------ Check that `sender` now owns the token
assert!(token::balance_of(signer::address_of(sender), token_id) == 1, 0);
assert!(token::check_tokendata_exists(signer::address_of(&creator), collection_name, token_name), 0);
// ------ Transfer the tokens
transfer_nft::transfer_nft_entry(
sender,
signer::address_of(&creator),
string::utf8(b"collection name"), // collection
token_name,
0, // property_version
3, // recipient chain
x"0000000000000000000000000000000000000000000000000000000000FAFAFA",
0
);
// ------ Check that `sender` no longer owns the token, and that it no longer exists
assert!(token::balance_of(signer::address_of(sender), token_id) == 0, 0);
assert!(!token::check_tokendata_exists(signer::address_of(&creator), collection_name, token_name), 0);
}
// test transfer native NFT to another chain
// this function is called in complete_transfer::complete_transfer_test
#[test(deployer = @deployer, creator = @0x654321, sender = @0x123456)]
public fun test_transfer_native_nft(deployer: &signer, creator: address, sender: &signer) {
// ------ Setup
wormhole::wormhole_test::setup(0);
nft_bridge::nft_bridge::init_test(deployer);
aptos_framework::aptos_account::create_account(signer::address_of(sender));
token::opt_in_direct_transfer(sender, true);
let creator = account::create_account_for_test(creator);
// ------ Create a collection under `creator`
let collection_name = string::utf8(b"my test collection");
create_collection(&creator, collection_name);
// ------ Mint two token to `sender` from the collection we just created
let token_name = string::utf8(b"my test token");
mint_token_to(
&creator,
signer::address_of(sender),
collection_name,
token_name,
2
);
// ------ Construct token_id for follow-up queries
let token_id = token::create_token_id_raw(
signer::address_of(&creator),
collection_name,
token_name,
0 // property_version
);
// ------ Check that `sender` now owns the token
assert!(token::balance_of(signer::address_of(sender), token_id) == 2, 0);
// ------ Transfer the tokens
transfer_nft::transfer_nft_entry(
sender,
signer::address_of(&creator),
collection_name,
token_name,
0, // property_version
3, // recipient chain
x"0000000000000000000000000000000000000000000000000000000000FAFAFA",
0
);
// ------ Check that `sender` no longer owns the token, but `nft_bridge` does
assert!(token::balance_of(signer::address_of(sender), token_id) == 1, 0);
assert!(token::balance_of(@nft_bridge, token_id) == 1, 0);
}
// This test case checks that we handle 'fungible' NFTs correctly (see NOTE in `lock_or_burn`)
#[test(deployer=@deployer, creator = @0x654321, sender = @0x123456)]
#[expected_failure(abort_code = 1, location = nft_bridge::transfer_nft)]
public fun test_transfer_native_nft_fungible(deployer: &signer, creator: &signer, sender: &signer) {
// We first invoke `test_transfer_native_nft_fungible` which will
// deposit one of the tokens into the NFT bridge.
// Then we will attempt to transfer another one, which should fail
test_transfer_native_nft(deployer, signer::address_of(creator), sender);
let collection_name = string::utf8(b"my test collection");
let token_name = string::utf8(b"my test token");
let token_id = token::create_token_id_raw(
signer::address_of(creator),
collection_name,
token_name,
0 // property_version
);
// ------ Check that `sender` and `nft_bridge` both own one token
assert!(token::balance_of(signer::address_of(sender), token_id) == 1, 0);
assert!(token::balance_of(@nft_bridge, token_id) == 1, 0);
// ------ Attempt to transfer the token
transfer_nft::transfer_nft_entry(
sender,
signer::address_of(creator),
collection_name,
token_name,
0, // property_version
3, // recipient chain
x"0000000000000000000000000000000000000000000000000000000000FAFAFA",
0
);
}
// ------------ Helper functions --------------------
/// Create a collection with a given name
public fun create_collection(creator: &signer, collection_name: String) {
token::create_collection(
creator,
collection_name, // collection name
string::utf8(b"beeeeef"), //description
string::utf8(b"beef.com"), //uri
0,
vector[true, true, true]
);
}
public fun mint_token_to(
creator: &signer,
recipient: address,
collection_name: String,
token_name: String,
amount: u64
) {
let token_data_id = token::create_tokendata(
creator,
collection_name,
token_name,
string::utf8(b"some description"),
amount,
string::utf8(b"some uri"),
signer::address_of(creator), // royalty payee
0, // royalty_points_denominator
0, // royalty_points_numerator
token::create_token_mutability_config(&vector[true, true, true, true, true]), // allow all fields to be mutated
vector[],
vector[],
vector[],
);
token::mint_token_to(
creator,
recipient,
token_data_id,
amount
);
}
}

View File

@ -0,0 +1,140 @@
/// Token Bridge VAA utilities
module nft_bridge::vaa {
use std::option;
use wormhole::vaa::{Self, VAA};
use nft_bridge::state;
friend nft_bridge::complete_transfer;
friend nft_bridge::contract_upgrade;
friend nft_bridge::register_chain;
friend nft_bridge::wrapped;
#[test_only]
friend nft_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 nft_bridge::vaa_test {
use nft_bridge::vaa;
use nft_bridge::state;
use nft_bridge::nft_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
);
nft_bridge::init_test(deployer);
}
#[test(deployer = @deployer)]
#[expected_failure(abort_code = 0, location = nft_bridge::vaa)] // 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, location = nft_bridge::vaa)] // 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, location = 0x1::table)] // 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,204 @@
module nft_bridge::wrapped {
use std::signer;
use std::vector;
use std::bcs;
use std::string::{Self, String};
use aptos_framework::account;
use aptos_token::token;
use wormhole::serialize;
use wormhole::external_address::{Self, ExternalAddress};
use token_bridge::string32::{Self, String32};
use nft_bridge::state::{Self, OriginInfo};
use nft_bridge::transfer::{Self, Transfer};
use nft_bridge::uri::{Self, URI};
use nft_bridge::wrapped_token_name;
friend nft_bridge::complete_transfer;
friend nft_bridge::transfer_nft;
#[test_only]
friend nft_bridge::transfer_nft_test;
#[test_only]
friend nft_bridge::wrapped_test;
#[test_only]
friend nft_bridge::complete_transfer_test;
const E_IS_NOT_WRAPPED_ASSET: u64 = 0;
/// Create a new collection from the transfer data if it doesn't already exist.
/// The collection will be created into a resource account, whose signer is
/// returned.
public(friend) fun create_or_find_wrapped_nft_collection(transfer: &Transfer): (signer, String32) {
let token_address = transfer::get_token_address(transfer);
let token_chain = transfer::get_token_chain(transfer);
let origin_info = state::create_origin_info(token_chain, token_address);
let original_name = transfer::get_name(transfer);
let original_symbol = transfer::get_symbol(transfer);
let name: String32;
let symbol: String32;
if (state::is_unified_solana_collection(origin_info)) {
name = string32::from_bytes(b"Wormhole Bridged Solana-NFT");
symbol = string32::from_bytes(b"WORMSPLNFT");
state::set_spl_cache(transfer::get_token_id(transfer), original_name, original_symbol);
} else {
name = original_name;
symbol = original_symbol;
};
// if the resource account already exists, we don't need do anything
if (!state::wrapped_asset_signer_exists(origin_info)) {
let seed = create_seed(&origin_info);
//create resource account
let nft_bridge_signer = state::nft_bridge_signer();
let (new_signer, new_cap) = account::create_resource_account(&nft_bridge_signer, seed);
state::set_wrapped_asset_info(origin_info, new_cap, symbol);
init_wrapped_nft(&new_signer, name, origin_info);
(new_signer, name)
} else {
(state::get_wrapped_asset_signer(origin_info), name)
}
}
fun init_wrapped_nft(
creator_signer: &signer,
name: String32,
origin_info: OriginInfo,
) {
let description = string::utf8(b"NFT transferred through Wormhole");
let uri = string::utf8(b"http://portalbridge.com");
// unbounded
let maximum = 0;
// allow all fields to be mutated, in case needed in the future
let mutability_config = vector[true, true, true];
token::create_collection(
creator_signer,
string32::to_string(&name),
description,
uri,
maximum,
mutability_config,
);
state::setup_wrapped(origin_info);
}
public(friend) fun mint_to(
creator: &signer,
recipient: address,
collection: String,
token_external_id: &ExternalAddress,
uri: URI
) {
// for the token name, we put the hex of the token id.
// TODO: is there anything better we could do? maybe render as
// decimal, as most chains use decimal numbers for token ids.
let name = wrapped_token_name::render_hex(external_address::get_bytes(token_external_id));
// set token data, including property keys (set token burnability to true)
let token_mut_config = token::create_token_mutability_config(
&vector[
true, // TOKEN_MAX_MUTABLE
true, // TOKEN_URI_MUTABLE
true, // TOKEN_ROYALTY_MUTABLE_IND
true, // TOKEN_DESCRIPTION_MUTABLE_IND
true // TOKEN_PROPERTY_MUTABLE_IND
]
);
// NOTE: Whether a token can be burned at all, burned by owner, or
// burned by creator is set in the property keys field when calling
// token::create_tokendata. We only allow `burn_by_creator` to avoid an
// edge case whereby a user burns a wrapped token and can no longer
// bridge it back to the origin chain.
let token_data_id = token::create_tokendata(
creator,
collection, // token collection name
name, // token name
string::utf8(b""), //empty description
1, //supply cap 1
uri::to_string(&uri),
signer::address_of(creator),
0, // royalty_points_denominator
0, // royalty_points_numerator
token_mut_config, // see above
// the following three arguments declare that
// TOKEN_BURNABLE_BY_CREATOR (of type bool) should be set to true
// see NOTE above
vector<String>[string::utf8(b"TOKEN_BURNABLE_BY_CREATOR")],
vector<vector<u8>>[bcs::to_bytes<bool>(&true)],
vector<String>[string::utf8(b"bool")],
);
token::mint_token_to(
creator,
recipient,
token_data_id,
1
);
}
/// Derive the generation seed for the resource account from
/// (token chain (2 bytes) || token address (32 bytes)).
fun create_seed(origin_info: &OriginInfo): vector<u8> {
let token_chain = state::get_origin_info_token_chain(origin_info);
let token_address = state::get_origin_info_token_address(origin_info);
let seed = vector::empty<u8>();
serialize::serialize_u16(&mut seed, token_chain);
external_address::serialize(&mut seed, token_address);
seed
}
}
#[test_only]
module nft_bridge::wrapped_test {
use std::signer;
use std::string::String;
use aptos_token::token;
use wormhole::external_address;
use wormhole::u16;
use token_bridge::string32;
use nft_bridge::transfer;
use nft_bridge::uri;
use nft_bridge::wrapped;
/// Creates a test NFT collection
public fun create_wrapped_nft_collection(recipient: address, collection_name: String): signer {
let token_address = external_address::from_bytes(x"00");
let token_chain = u16::from_u64(14);
let token_id = external_address::from_bytes(x"01");
let token_symbol = string32::from_bytes(b"collection symbol");
let token_name = string32::from_string(&collection_name);
let uri = uri::from_bytes(b"http://netscape-navigator.it");
let t = transfer::create(
token_address,
token_chain,
token_symbol,
token_name,
token_id,
uri,
external_address::from_bytes(x"0000"),
u16::from_u64(1) // target chain
);
let (creator, _) = wrapped::create_or_find_wrapped_nft_collection(&t);
// assert that collection was indeed created
assert!(token::check_collection_exists(signer::address_of(&creator), string32::to_string(&token_name)), 0);
wrapped::mint_to(&creator, recipient, string32::to_string(&token_name), &token_id, uri);
creator
}
}

View File

@ -0,0 +1,77 @@
module nft_bridge::wrapped_token_name {
use std::vector;
use std::string::{Self, String};
const E_INVALID_HEX_DIGIT: u64 = 0;
const E_INVALID_HEX_CHAR: u64 = 1;
// TODO(csongor): rename this functions maybe
/// Render a vector as a hex string
public fun render_hex(bytes: vector<u8>): String {
let res = vector::empty<u8>();
vector::reverse(&mut bytes);
while (!vector::is_empty(&bytes)) {
let b = vector::pop_back(&mut bytes);
let l = b >> 4;
let h = b & 0xF;
vector::push_back(&mut res, hex_digit(l));
vector::push_back(&mut res, hex_digit(h));
};
string::utf8(res)
}
/// Returns the ASCII code for a hex digit (i.e. 0 -> '0', a -> 'a')
fun hex_digit(d: u8): u8 {
assert!(d < 16, E_INVALID_HEX_DIGIT);
if (d < 10) {
d + 48
} else {
d + 87
}
}
public fun parse_hex(s: String): vector<u8> {
let res = vector::empty<u8>();
let bytes = *string::bytes(&s);
while (!vector::is_empty(&bytes)) {
let h = hex_char(vector::pop_back(&mut bytes));
let l = hex_char(vector::pop_back(&mut bytes));
let b = (l << 4) + h;
vector::push_back(&mut res, b);
};
vector::reverse(&mut res);
res
}
// Inverse of hex_digit
fun hex_char(v: u8): u8 {
if (48 <= v && v <= 57) {
v - 48
} else if (97 <= v && v <= 102) {
v - 87
} else {
assert!(false, E_INVALID_HEX_CHAR);
0
}
}
}
#[test_only]
module nft_bridge::wrapped_token_name_test {
use std::string;
use nft_bridge::wrapped_token_name;
#[test]
fun render_hex_test() {
assert!(wrapped_token_name::render_hex(x"beefcafe") == string::utf8(b"beefcafe"), 0);
}
#[test]
fun parse_hex_test() {
assert!(wrapped_token_name::parse_hex(string::utf8(b"beefcafe")) == x"beefcafe", 0);
}
}

View File

@ -218,7 +218,7 @@ module token_bridge::complete_transfer_test {
deployer = @deployer,
token_bridge = @token_bridge,
)]
#[expected_failure(abort_code = 65542, location = 0000000000000000000000000000000000000000000000000000000000000001::coin)] // EINSUFFICIENT_BALANCE
#[expected_failure(abort_code = 65542, location = aptos_framework::coin)] // EINSUFFICIENT_BALANCE
public fun test_native_too_much_fee(
deployer: &signer,
token_bridge: &signer

View File

@ -93,8 +93,12 @@ module token_bridge::contract_upgrade {
fun authorize_upgrade(hash: &Hash) acquires UpgradeAuthorized {
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?
// NOTE: here we're dropping the upgrade hash, allowing to override
// a previous upgrade that hasn't been executed. It's possible that
// an upgrade hash corresponds to bytecode that can't be upgraded
// to, because it fails bytecode compatibility verification. While
// that should never happen^TM, we don't want to deadlock the
// contract if it does.
let UpgradeAuthorized { hash: _ } = move_from<UpgradeAuthorized>(@token_bridge);
};
move_to(&token_bridge, UpgradeAuthorized { hash: hash.hash });

View File

@ -42,7 +42,6 @@ const GOVERNANCE_CHAIN = 1;
const GOVERNANCE_EMITTER =
"0000000000000000000000000000000000000000000000000000000000000004";
// TODO: remove this once the aptos SDK changes are merged in
const OVERRIDES = {
MAINNET: {
aptos: {
@ -63,6 +62,7 @@ const OVERRIDES = {
token_bridge:
"0x84a5f374d29fc77e370014dce4fd6a55b58ad608de8074b0be5571701724da31",
core: "0xde0036a9600559e295d5f6802ef6f3f802f510366e0c23912b0655d972166017",
nft_bridge: "0x46da3d4c569388af61f951bdd1153f4c875f90c2991f6b2d0a38e2161a40852c"
},
},
};