wormhole/terra/contracts/nft-bridge/src/token_id.rs

91 lines
3.3 KiB
Rust

use bigint::U256;
use cosmwasm_std::{StdError, StdResult, Storage};
use sha3::{
digest::{consts::U32, generic_array::GenericArray},
Digest, Keccak256,
};
use wormhole::byte_utils::ByteUtils;
use crate::{
state::{token_id_hashes, token_id_hashes_read},
CHAIN_ID,
};
// NOTE: [External and internal token id conversion]
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// The CW721 NFT standard allows token ids to be arbitrarily long (utf8)
// strings, while the token_ids in VAA payloads are always 32 bytes (and not
// necessarily valid utf8).
//
// We call a token id that's in string format an "internal id", and a token id
// that's in 32 byte format an "external id". Note that whether a token id is in
// internal format or external format doesn't imply which chain the token id
// originates from. We can have a terra (native) token id in both internal and
// external formats, and likewise we can have an ethereum token in in both
// internal and external formats.
//
// To support seamless transfers through the bridge, we need a way to have a
// 1-to-1 mapping from internal ids to external ids.
// When a foreign (such as ethereum or solana) token id first comes through, we
// simply render it into a string by formatting it as a decimal number. Then,
// when we want to transfer such a token back through the bridge, we simply
// parse the string back into a u256 (32 byte) number.
//
// When a native token id first leaves through the bridge, we turn its id into a
// 32 byte hash (keccak256). This hash is the external id. We store a mapping
//
// (chain_id, nft_address, keccak256(internal_id)) => internal_id
//
// so that we can turn it back into an internal id when it comes back through
// the bridge. When the token is sent back, we could choose to delete the hash
// from the store, but we do not. This way, external token verifiers will be
// able to verify NFT origins even for NFTs that have been transferred back.
//
// If two token ids within the same contract have the same keccak256 hash, then
// it's possible to lose tokens, but this is very unlikely.
pub fn from_external_token_id(
storage: &mut dyn Storage,
nft_chain: u16,
nft_address: &[u8; 32],
token_id_external: &[u8; 32],
) -> StdResult<String> {
if nft_chain == CHAIN_ID {
token_id_hashes_read(storage, nft_chain, *nft_address).load(token_id_external)
} else {
Ok(format!("{}", U256::from_big_endian(token_id_external)))
}
}
fn hash(token_id: &str) -> GenericArray<u8, U32> {
let mut hasher = Keccak256::new();
hasher.update(token_id);
hasher.finalize()
}
pub fn to_external_token_id(
storage: &mut dyn Storage,
nft_chain: u16,
nft_address: &[u8; 32],
token_id_internal: String,
) -> StdResult<[u8; 32]> {
if nft_chain == CHAIN_ID {
let hash = hash(&token_id_internal);
token_id_hashes(storage, nft_chain, *nft_address).save(&hash, &token_id_internal)?;
Ok(hash.as_slice().get_const_bytes(0))
} else {
let mut bytes = [0; 32];
U256::from_dec_str(&token_id_internal)
.map_err(|_| {
StdError::generic_err(format!(
"{} could not be parsed as a decimal number",
token_id_internal
))
})?
.to_big_endian(&mut bytes);
Ok(bytes)
}
}