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>, /// Mapping of wrapped assets ((chain_id, origin_address) => wrapped_asset info) wrapped_infos: Table, /// 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, signer_cap: SignerCapability, emitter_cap: EmitterCapability, /// Mapping of bridge contracts on other chains registered_emitters: Table, /// See `is_unified_solana_collection` for the purpose /// of this field. /// Mapping of token_id => spl cache entry spl_cache: Table, } // getters public fun vaa_is_consumed(hash: vector): bool acquires State { let state = borrow_global(@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(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 acquires State { let state = borrow_global(@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(creator) } public fun get_spl_cache(token_id: ExternalAddress): (String32, String32) acquires State { let state = borrow_global(@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(@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(@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, message_fee: Coin, ): u64 acquires State { let emitter_cap = &mut borrow_global_mut(@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(@nft_bridge).signer_cap) } // setters public(friend) fun set_vaa_consumed(hash: vector) acquires State { let state = borrow_global_mut(@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(@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(@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(@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(@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(@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(@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(@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>(), wrapped_infos: table::new(), native_infos: table::new(), signer_cap: signer_cap, emitter_cap: emitter_cap, registered_emitters: table::new(), spl_cache: table::new(), } ); } }