terra/nft_bridge: Implement nft-bridge for terra (#698)

commit-id:0b547fa5
This commit is contained in:
Csongor Kiss 2022-01-07 16:47:33 +01:00 committed by GitHub
parent 089d7cde97
commit 7e212fa739
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1456 additions and 289 deletions

View File

@ -99,7 +99,7 @@ yargs(hideBin(process.argv))
1,
1,
"0x0000000000000000000000000000000000000000000000000000000000000004",
0,
Math.floor(Math.random() * 100000000),
data,
[
"cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"

View File

@ -19,10 +19,12 @@
| Migration Contract | SOL | Ex9bCdVMSfx7EzB3pgSi2R4UHwJAXvTw18rBQm5YQ8gK | |
| P2W Emitter | SOL | 8fuAZUxHecYLMC76ZNjYzwRybUiDv9LhkRQsAccEykLr | |
| Test Wallet | Terra | terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v | Mnemonic: `notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius` |
| Example Token | Terra | terra13nkgqrfymug724h8pprpexqj9h629sa3ncw7sh | Tokens minted to Test Wallet |
| Test CW20 | Terra | terra13nkgqrfymug724h8pprpexqj9h629sa3ncw7sh | Tokens minted to Test Wallet |
| Test CW721 | Terra | terra1l425neayde0fzfcv3apkyk4zqagvflm6cmha9v | NFTs minted to Test Wallet |
| Bridge Core | Terra | terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5 | |
| Token Bridge | Terra | terra10pyejy66429refv3g35g2t7am0was7ya7kz2a4 | |
| Pyth Bridge | Terra | terra1wgh6adn8geywx0v78zs9azrqtqdegufuegnwep | |
| NFT Bridge | Terra | terra19zpyd046u4swqpksr3n44cej4j8pg6ah2y6dcg | |
| Pyth Bridge | Terra | terra1plju286nnfj3z54wgcggd4enwaa9fgf5kgrgzl | |
| Governance Emitter | Universal | 0x0000000000000000000000000000000000000000000000000000000000000004 / 11111111111111111111111111111115 | Emitter Chain: 0x01 |
### Terra

View File

@ -61,6 +61,12 @@ contract NFTBridge is NFTBridgeGovernance {
} else {
assembly {
// first 32 bytes hold string length
// mload then loads the next word, i.e. the first 32 bytes of the strings
// NOTE: this means that we might end up with an
// invalid utf8 string (e.g. if we slice an emoji in half). The VAA
// payload specification doesn't require that these are valid utf8
// strings, and it's cheaper to do any validation off-chain for
// presentation purposes
symbol := mload(add(symbolString, 32))
name := mload(add(nameString, 32))
}

View File

@ -10,9 +10,9 @@ contract NFTBridgeStructs {
bytes32 tokenAddress;
// Chain ID of the token
uint16 tokenChain;
// Symbol of the token (UTF-8)
// Symbol of the token
bytes32 symbol;
// Name of the token (UTF-8)
// Name of the token
bytes32 name;
// TokenID of the token
uint256 tokenID;

View File

@ -19,6 +19,7 @@ module.exports = async function (callback) {
gasLimit: 2000000
});
// Register the Solana NFT bridge endpoint
await nftBridge.methods.registerChain("0x010000000001007985ba742002ae745c19722fea4d82102e68526c7c9d94d0e5d0a809071c98451c9693b230b3390f4ca9555a3ba9a9abbe87cf6f9e400682213e4fbbe1dabb9e0100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000004e4654427269646765010000000196ee982293251b48729804c8e8b24b553eb6b887867024948d2236fd37a577ab").send({
value: 0,
from: accounts[0],
@ -30,4 +31,4 @@ module.exports = async function (callback) {
catch (e) {
callback(e);
}
}
}

View File

@ -2,16 +2,25 @@
const jsonfile = require("jsonfile");
const TokenBridge = artifacts.require("TokenBridge");
const NFTBridge = artifacts.require("NFTBridgeEntrypoint");
const TokenImplementation = artifacts.require("TokenImplementation");
const BridgeImplementationFullABI = jsonfile.readFileSync("../build/contracts/BridgeImplementation.json").abi
module.exports = async function (callback) {
try {
const accounts = await web3.eth.getAccounts();
const initialized = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address);
const tokenBridge = new web3.eth.Contract(BridgeImplementationFullABI, TokenBridge.address);
const nftBridge = new web3.eth.Contract(BridgeImplementationFullABI, NFTBridge.address);
// Register the Solana endpoint
await initialized.methods.registerChain("0x010000000001009a895e8b42444fdf60a71666121d7e84b3a49579ba3b84fff1dbdf9ec93360390c05a88f66c90df2034cb38427ba9b01632e780ce7b84df559a1bf44c316447d01000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000546f6b656e4272696467650100000003000000000000000000000000784999135aaa8a3ca5914468852fdddbddd8789d").send({
// Register the terra token bridge endpoint
await tokenBridge.methods.registerChain("0x010000000001009a895e8b42444fdf60a71666121d7e84b3a49579ba3b84fff1dbdf9ec93360390c05a88f66c90df2034cb38427ba9b01632e780ce7b84df559a1bf44c316447d01000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000546f6b656e4272696467650100000003000000000000000000000000784999135aaa8a3ca5914468852fdddbddd8789d").send({
value: 0,
from: accounts[0],
gasLimit: 2000000
});
// Register the terra NFT bridge endpoint
await nftBridge.methods.registerChain("0x010000000001008ebe6a1971ae336bb7817aa7e8ffc13e1582ebbc00dc85e33b592dfea998787a1a9ccece2efce7fcdf228153baafb6f1232b320805c82fac90b5e49c3b5ad4fd0100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000004e46544272696467650100000003000000000000000000000000288246bebae560e006d01c675ae332ac8e146bb7").send({
value: 0,
from: accounts[0],
gasLimit: 2000000

View File

@ -30,6 +30,7 @@ var NFTEmitters = map[string]string{
// devnet
"96ee982293251b48729804c8e8b24b553eb6b887867024948d2236fd37a577ab": "NFTWqJR8YnRVqPDvTJrYuLrQDitTG5AScqbeghi4zSA", // solana
"00000000000000000000000026b4afb60d6c903165150c6f0aa14f8016be4aec": "0x26b4afb60d6c903165150c6f0aa14f8016be4aec", // ethereum
"000000000000000000000000288246bebae560e006d01c675ae332ac8e146bb7": "terra19zpyd046u4swqpksr3n44cej4j8pg6ah2y6dcg",// terra
}
var TokenTransferEmitters = map[string]string{
// mainnet

View File

@ -100,6 +100,7 @@ popd
pushd /usr/src/clients/nft_bridge
# Register the NFT Bridge Endpoint on ETH
node main.js solana execute_governance_vaa $(node main.js generate_register_chain_vaa 2 0x00000000000000000000000026b4afb60d6c903165150c6f0aa14f8016be4aec)
node main.js solana execute_governance_vaa $(node main.js generate_register_chain_vaa 3 0x000000000000000000000000288246bebae560e006d01c675ae332ac8e146bb7)
popd
# Let k8s startup probe succeed

19
terra/Cargo.lock generated
View File

@ -1143,6 +1143,25 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389"
[[package]]
name = "nft-bridge"
version = "0.1.0"
dependencies = [
"bigint",
"cosmwasm-std",
"cosmwasm-storage",
"cosmwasm-vm",
"cw721",
"cw721-base",
"cw721-wrapped",
"hex",
"schemars",
"serde",
"serde_json",
"sha3",
"wormhole",
]
[[package]]
name = "num-derive"
version = "0.3.3"

View File

@ -1,5 +1,5 @@
[workspace]
members = ["contracts/cw20-wrapped", "contracts/wormhole", "contracts/token-bridge", "contracts/pyth-bridge", "contracts/cw721-wrapped", "packages/cw721", "contracts/cw721-base"]
members = ["contracts/cw20-wrapped", "contracts/wormhole", "contracts/token-bridge", "contracts/pyth-bridge", "contracts/nft-bridge", "contracts/cw721-wrapped", "packages/cw721", "contracts/cw721-base"]
[profile.release]
opt-level = 3

View File

@ -11,7 +11,6 @@ pub use crate::msg::{ExecuteMsg, InstantiateMsg, MintMsg, MinterResponse, QueryM
pub use crate::state::Cw721Contract;
use cosmwasm_std::Empty;
// This is a simple type to let us handle empty extensions
pub type Extension = Option<Empty>;
#[cfg(not(feature = "library"))]

View File

@ -22,7 +22,7 @@ pub type Extension = Option<Empty>;
pub type Cw721MetadataContract<'a> = cw721_base::Cw721Contract<'a, Extension, Empty>;
pub type ExecuteMsg = cw721_base::ExecuteMsg<Extension>;
// #[cfg(not(feature = "library"))]
#[cfg(not(feature = "library"))]
pub mod entry {
use std::convert::TryInto;

View File

@ -0,0 +1,5 @@
[alias]
wasm = "build --release --target wasm32-unknown-unknown"
wasm-debug = "build --target wasm32-unknown-unknown"
unit-test = "test --lib --features backtraces"
integration-test = "test --test integration"

View File

@ -0,0 +1,30 @@
[package]
name = "nft-bridge"
version = "0.1.0"
edition = "2018"
description = "Wormhole NFT bridge"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
backtraces = ["cosmwasm-std/backtraces"]
# use library feature to disable all init/handle/query exports
library = []
[dependencies]
cosmwasm-std = { version = "0.16.0" }
cosmwasm-storage = { version = "0.16.0" }
schemars = "0.8.1"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
cw721-wrapped = { path = "../cw721-wrapped", features = ["library"] }
cw721-base = { path = "../../contracts/cw721-base", version = "0.10.0", features = ["library"] }
cw721 = { path = "../../packages/cw721" }
wormhole = { path = "../wormhole", features = ["library"] }
sha3 = { version = "0.9.1", default-features = false }
hex = "0.4.2"
bigint = "4"
[dev-dependencies]
cosmwasm-vm = { version = "0.16.0", default-features = false }
serde_json = "1.0"

View File

@ -0,0 +1,588 @@
use crate::{
msg::WrappedRegistryResponse,
state::{
spl_cache,
spl_cache_read,
wrapped_asset,
BoundedVec,
SplCacheItem,
},
token_id::{
from_external_token_id,
to_external_token_id,
},
CHAIN_ID,
};
use cosmwasm_std::{
entry_point,
to_binary,
Binary,
CanonicalAddr,
CosmosMsg,
Deps,
DepsMut,
Empty,
Env,
MessageInfo,
QueryRequest,
Response,
StdError,
StdResult,
WasmMsg,
WasmQuery, Order,
};
use crate::{
msg::{
ExecuteMsg,
InstantiateMsg,
MigrateMsg,
QueryMsg,
},
state::{
bridge_contracts,
bridge_contracts_read,
config,
config_read,
wrapped_asset_address,
wrapped_asset_address_read,
wrapped_asset_read,
Action,
ConfigInfo,
RegisterChain,
TokenBridgeMessage,
TransferInfo,
UpgradeContract,
},
};
use wormhole::{
byte_utils::{
extend_address_to_32,
extend_address_to_32_array,
get_string_from_32,
string_to_array,
ByteUtils,
},
error::ContractError,
};
use wormhole::msg::{
ExecuteMsg as WormholeExecuteMsg,
QueryMsg as WormholeQueryMsg,
};
use wormhole::state::{
vaa_archive_add,
vaa_archive_check,
GovernancePacket,
ParsedVAA,
};
use sha3::{
Digest,
Keccak256,
};
type HumanAddr = String;
const WRAPPED_ASSET_UPDATING: &str = "updating";
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
Ok(Response::new())
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
// Save general wormhole info
let state = ConfigInfo {
gov_chain: msg.gov_chain,
gov_address: msg.gov_address.as_slice().to_vec(),
wormhole_contract: msg.wormhole_contract,
wrapped_asset_code_id: msg.wrapped_asset_code_id,
};
config(deps.storage).save(&state)?;
Ok(Response::default())
}
pub fn parse_vaa(deps: DepsMut, block_time: u64, data: &Binary) -> StdResult<ParsedVAA> {
let cfg = config_read(deps.storage).load()?;
let vaa: ParsedVAA = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: cfg.wormhole_contract.clone(),
msg: to_binary(&WormholeQueryMsg::VerifyVAA {
vaa: data.clone(),
block_time,
})?,
}))?;
Ok(vaa)
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> {
match msg {
ExecuteMsg::InitiateTransfer {
contract_addr,
token_id,
recipient_chain,
recipient,
nonce,
} => handle_initiate_transfer(
deps,
env,
info,
contract_addr,
token_id,
recipient_chain,
recipient.to_array()?,
nonce,
),
ExecuteMsg::SubmitVaa { data } => submit_vaa(deps, env, info, &data),
ExecuteMsg::RegisterAssetHook { asset_id } => {
handle_register_asset(deps, env, info, &asset_id.as_slice())
}
}
}
fn submit_vaa(
mut deps: DepsMut,
env: Env,
info: MessageInfo,
data: &Binary,
) -> StdResult<Response> {
let state = config_read(deps.storage).load()?;
let vaa = parse_vaa(deps.branch(), env.block.time.seconds(), data)?;
let data = vaa.payload;
if vaa_archive_check(deps.storage, vaa.hash.as_slice()) {
return ContractError::VaaAlreadyExecuted.std_err();
}
vaa_archive_add(deps.storage, vaa.hash.as_slice())?;
// check if vaa is from governance
if state.gov_chain == vaa.emitter_chain && state.gov_address == vaa.emitter_address {
return handle_governance_payload(deps, env, &data);
}
let message = TokenBridgeMessage::deserialize(&data)?;
match message.action {
Action::TRANSFER => handle_complete_transfer(
deps,
env,
info,
vaa.emitter_chain,
vaa.emitter_address,
TransferInfo::deserialize(&message.payload)?,
),
_ => ContractError::InvalidVAAAction.std_err(),
}
}
fn handle_governance_payload(deps: DepsMut, env: Env, data: &Vec<u8>) -> StdResult<Response> {
let gov_packet = GovernancePacket::deserialize(&data)?;
let module = get_string_from_32(&gov_packet.module);
if module != "NFTBridge" {
return Err(StdError::generic_err("this is not a valid module"));
}
if gov_packet.chain != 0 && gov_packet.chain != CHAIN_ID {
return Err(StdError::generic_err(
"the governance VAA is for another chain",
));
}
match gov_packet.action {
1u8 => handle_register_chain(deps, env, RegisterChain::deserialize(&gov_packet.payload)?),
2u8 => handle_upgrade_contract(
deps,
env,
UpgradeContract::deserialize(&gov_packet.payload)?,
),
_ => ContractError::InvalidVAAAction.std_err(),
}
}
fn handle_upgrade_contract(
_deps: DepsMut,
env: Env,
upgrade_contract: UpgradeContract,
) -> StdResult<Response> {
Ok(Response::new()
.add_message(CosmosMsg::Wasm(WasmMsg::Migrate {
contract_addr: env.contract.address.to_string(),
new_code_id: upgrade_contract.new_contract,
msg: to_binary(&MigrateMsg {})?,
}))
.add_attribute("action", "contract_upgrade"))
}
fn handle_register_chain(
deps: DepsMut,
_env: Env,
register_chain: RegisterChain,
) -> StdResult<Response> {
let RegisterChain {
chain_id,
chain_address,
} = register_chain;
let existing = bridge_contracts_read(deps.storage).load(&chain_id.to_be_bytes());
if existing.is_ok() {
return Err(StdError::generic_err(
"bridge contract already exists for this chain",
));
}
let mut bucket = bridge_contracts(deps.storage);
bucket.save(&chain_id.to_be_bytes(), &chain_address)?;
Ok(Response::new()
.add_attribute("chain_id", chain_id.to_string())
.add_attribute("chain_address", hex::encode(chain_address)))
}
fn handle_complete_transfer(
deps: DepsMut,
env: Env,
_info: MessageInfo,
emitter_chain: u16,
emitter_address: Vec<u8>,
transfer_info: TransferInfo,
) -> StdResult<Response> {
let cfg = config_read(deps.storage).load()?;
let expected_contract =
bridge_contracts_read(deps.storage).load(&emitter_chain.to_be_bytes())?;
// must be sent by a registered token bridge contract
if expected_contract != emitter_address {
return Err(StdError::generic_err("invalid emitter"));
}
if transfer_info.recipient_chain != CHAIN_ID {
return Err(StdError::generic_err(
"this transfer is not directed at this chain",
));
}
let token_chain = transfer_info.nft_chain;
let target_address = &(&transfer_info.recipient[..]).get_address(0);
let mut messages = vec![];
let recipient = deps
.api
.addr_humanize(&target_address)
.or_else(|_| ContractError::WrongTargetAddressFormat.std_err())?;
let contract_addr;
let token_id = from_external_token_id(
deps.storage,
token_chain,
&transfer_info.nft_address,
&transfer_info.token_id,
)?;
if token_chain != CHAIN_ID {
// NFT is not native to this chain, so we need a wrapper
let asset_address = transfer_info.nft_address;
let asset_id = build_asset_id(token_chain, &asset_address);
let token_uri = String::from_utf8(transfer_info.uri.to_vec())
.or_else(|_| Err(StdError::generic_err("could not parse uri string")))?;
let mint_msg = cw721_base::msg::MintMsg {
token_id,
owner: recipient.to_string(),
token_uri: Some(token_uri),
extension: None,
};
// Check if this asset is already deployed
if let Some(wrapped_addr) = wrapped_asset_read(deps.storage).load(&asset_id).ok() {
contract_addr = wrapped_addr;
// Asset already deployed, just mint
messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: contract_addr.clone(),
msg: to_binary(&cw721_base::msg::ExecuteMsg::Mint(mint_msg))?,
funds: vec![],
}));
} else {
contract_addr = env.contract.address.clone().into_string();
wrapped_asset(deps.storage)
.save(&asset_id, &HumanAddr::from(WRAPPED_ASSET_UPDATING))?;
let (name, symbol) = if token_chain == 1 {
let spl_cache_item = SplCacheItem {
name: transfer_info.name,
symbol: transfer_info.symbol,
};
spl_cache(deps.storage).save(&transfer_info.token_id, &spl_cache_item)?;
// Solana NFTs all use the same NFT contract, so unify the name
(
"Wormhole Bridged Solana-NFT".to_string(),
"WORMSPLNFT".to_string(),
)
} else {
(
get_string_from_32(&transfer_info.name.to_vec()),
get_string_from_32(&transfer_info.symbol.to_vec()),
)
};
messages.push(CosmosMsg::Wasm(WasmMsg::Instantiate {
admin: Some(contract_addr.clone()),
code_id: cfg.wrapped_asset_code_id,
msg: to_binary(&cw721_wrapped::msg::InstantiateMsg {
name,
symbol,
asset_chain: token_chain,
asset_address: (&transfer_info.nft_address[..]).into(),
minter: env.contract.address.into_string(),
mint: Some(mint_msg),
init_hook: Some(cw721_wrapped::msg::InitHook {
msg: cw721_wrapped::to_binary(&ExecuteMsg::RegisterAssetHook {
asset_id: asset_id.to_vec().into(),
})
.map_err(|_| StdError::generic_err("couldn't convert to binary"))?,
contract_addr: contract_addr.clone(),
}),
})?,
funds: vec![],
label: String::new(),
}));
}
} else {
// Native NFT, transfer from custody
let token_address = (&transfer_info.nft_address[..]).get_address(0);
contract_addr = deps.api.addr_humanize(&token_address)?.to_string();
messages.push(CosmosMsg::<Empty>::Wasm(WasmMsg::Execute {
contract_addr: contract_addr.clone(),
msg: to_binary(&cw721_base::msg::ExecuteMsg::<Option<Empty>>::TransferNft {
recipient: recipient.to_string(),
token_id,
})?,
funds: vec![],
}));
}
Ok(Response::new()
.add_messages(messages)
.add_attribute("action", "complete_transfer")
.add_attribute("recipient", recipient)
.add_attribute("contract", contract_addr))
}
fn handle_initiate_transfer(
deps: DepsMut,
env: Env,
info: MessageInfo,
asset: HumanAddr,
token_id: String,
recipient_chain: u16,
recipient: [u8; 32],
nonce: u32,
) -> StdResult<Response> {
if recipient_chain == CHAIN_ID {
return ContractError::SameSourceAndTarget.std_err();
}
let asset_chain: u16;
let asset_address: [u8; 32];
let cfg: ConfigInfo = config_read(deps.storage).load()?;
let asset_canonical: CanonicalAddr = deps.api.addr_canonicalize(&asset)?;
let mut messages: Vec<CosmosMsg> = vec![];
if let Ok(_) = wrapped_asset_address_read(deps.storage).load(asset_canonical.as_slice()) {
// This is a deployed wrapped asset, burn it
messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: asset.clone(),
msg: to_binary(&cw721_wrapped::msg::ExecuteMsg::Burn::<Option<Empty>> {
token_id: token_id.clone(),
})?,
funds: vec![],
}));
let wrapped_token_info: cw721_wrapped::msg::WrappedAssetInfoResponse = deps
.querier
.custom_query(&QueryRequest::<Empty>::Wasm(WasmQuery::Smart {
contract_addr: asset.clone(),
msg: to_binary(&cw721_wrapped::msg::QueryMsg::WrappedAssetInfo {})?,
}))?;
asset_address = wrapped_token_info.asset_address.to_array()?;
asset_chain = wrapped_token_info.asset_chain;
} else {
// Native NFT, lock it up
messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: asset.clone(),
msg: to_binary(&cw721_base::msg::ExecuteMsg::<Option<Empty>>::TransferNft {
recipient: env.contract.address.to_string(),
token_id: token_id.clone(),
})?,
funds: vec![],
}));
asset_chain = CHAIN_ID;
asset_address = extend_address_to_32_array(&asset_canonical);
};
let external_token_id =
to_external_token_id(deps.storage, asset_chain, &asset_address, token_id.clone())?;
let symbol: [u8; 32];
let name: [u8; 32];
if asset_chain == 1 {
let SplCacheItem {
name: cached_name,
symbol: cached_symbol,
} = spl_cache_read(deps.storage).load(&external_token_id)?;
symbol = cached_symbol;
name = cached_name;
} else {
let response: cw721::ContractInfoResponse =
deps.querier
.custom_query(&QueryRequest::<Empty>::Wasm(WasmQuery::Smart {
contract_addr: asset.clone(),
msg: to_binary(&cw721_base::msg::QueryMsg::ContractInfo {})?,
}))?;
name = string_to_array(&response.name);
symbol = string_to_array(&response.symbol);
}
let cw721::NftInfoResponse::<Option<Empty>> { token_uri, .. } =
deps.querier
.custom_query(&QueryRequest::<Empty>::Wasm(WasmQuery::Smart {
contract_addr: asset.clone(),
msg: to_binary(&cw721_base::msg::QueryMsg::NftInfo {
token_id: token_id.clone(),
})?,
}))?;
let transfer_info = TransferInfo {
nft_address: asset_address,
nft_chain: asset_chain,
symbol,
name,
token_id: external_token_id,
uri: BoundedVec::new(token_uri.unwrap().into())?,
recipient,
recipient_chain,
};
let token_bridge_message = TokenBridgeMessage {
action: Action::TRANSFER,
payload: transfer_info.serialize(),
};
messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: cfg.wormhole_contract,
msg: to_binary(&WormholeExecuteMsg::PostMessage {
message: Binary::from(token_bridge_message.serialize()),
nonce,
})?,
funds: vec![],
}));
Ok(Response::new()
.add_messages(messages)
.add_attribute("transfer.token_chain", asset_chain.to_string())
.add_attribute("transfer.token", hex::encode(asset_address))
.add_attribute("transfer.token_id", token_id)
.add_attribute("transfer.external_token_id", hex::encode(external_token_id))
.add_attribute(
"transfer.sender",
hex::encode(extend_address_to_32(
&deps.api.addr_canonicalize(&info.sender.as_str())?,
)),
)
.add_attribute("transfer.recipient_chain", recipient_chain.to_string())
.add_attribute("transfer.recipient", hex::encode(recipient))
.add_attribute("transfer.nonce", nonce.to_string())
.add_attribute("transfer.block_time", env.block.time.seconds().to_string()))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::WrappedRegistry { chain, address } => {
to_binary(&query_wrapped_registry(deps, chain, address.as_slice())?)
}
QueryMsg::AllWrappedAssets { } => {
to_binary(&query_all_wrapped_assets(deps)?)
}
}
}
/// Handle wrapped asset registration messages
fn handle_register_asset(
deps: DepsMut,
_env: Env,
info: MessageInfo,
asset_id: &[u8],
) -> StdResult<Response> {
let mut bucket = wrapped_asset(deps.storage);
let result = bucket
.load(asset_id)
.map_err(|_| ContractError::RegistrationForbidden.std())?;
if result != HumanAddr::from(WRAPPED_ASSET_UPDATING) {
return ContractError::AssetAlreadyRegistered.std_err();
}
bucket.save(asset_id, &info.sender.to_string())?;
let contract_address: CanonicalAddr = deps.api.addr_canonicalize(&info.sender.as_str())?;
wrapped_asset_address(deps.storage).save(contract_address.as_slice(), &asset_id.to_vec())?;
Ok(Response::new()
.add_attribute("action", "register_asset")
.add_attribute("asset_id", format!("{:?}", asset_id))
.add_attribute("contract_addr", info.sender))
}
pub fn query_wrapped_registry(
deps: Deps,
chain: u16,
address: &[u8],
) -> StdResult<WrappedRegistryResponse> {
let asset_id = build_asset_id(chain, address);
// Check if this asset is already deployed
match wrapped_asset_read(deps.storage).load(&asset_id) {
Ok(address) => Ok(WrappedRegistryResponse { address }),
Err(_) => ContractError::AssetNotFound.std_err(),
}
}
fn query_all_wrapped_assets(deps: Deps) -> StdResult<Vec<String>> {
let bucket = wrapped_asset_address_read(deps.storage);
let mut result = vec![];
for item in bucket.range(None, None, Order::Ascending) {
let contract_address = item?.0.into();
result.push(deps.api.addr_humanize(&contract_address)?.to_string())
}
Ok(result)
}
fn build_asset_id(chain: u16, address: &[u8]) -> Vec<u8> {
let mut asset_id: Vec<u8> = vec![];
asset_id.extend_from_slice(&chain.to_be_bytes());
asset_id.extend_from_slice(address);
let mut hasher = Keccak256::new();
hasher.update(asset_id);
hasher.finalize().to_vec()
}

View File

@ -0,0 +1,10 @@
#[cfg(test)]
extern crate lazy_static;
pub mod contract;
pub mod msg;
pub mod state;
pub mod token_id;
// Chain ID of Terra
const CHAIN_ID: u16 = 3;

View File

@ -0,0 +1,61 @@
use cosmwasm_std::Binary;
use schemars::JsonSchema;
use serde::{
Deserialize,
Serialize,
};
type HumanAddr = String;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
// governance contract details
pub gov_chain: u16,
pub gov_address: Binary,
pub wormhole_contract: HumanAddr,
pub wrapped_asset_code_id: u64,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
RegisterAssetHook {
asset_id: Binary,
},
InitiateTransfer {
contract_addr: String,
token_id: String,
recipient_chain: u16,
recipient: Binary,
nonce: u32,
},
SubmitVaa {
data: Binary,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct MigrateMsg {}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
WrappedRegistry { chain: u16, address: Binary },
AllWrappedAssets { },
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct WrappedRegistryResponse {
pub address: HumanAddr,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WormholeQueryMsg {
VerifyVAA { vaa: Binary, block_time: u64 },
}

View File

@ -0,0 +1,271 @@
use std::convert::TryInto;
use schemars::JsonSchema;
use serde::{
Deserialize,
Serialize,
};
use cosmwasm_std::{
StdError,
StdResult,
Storage,
};
use cosmwasm_storage::{
bucket,
bucket_read,
singleton,
singleton_read,
Bucket,
ReadonlyBucket,
ReadonlySingleton,
Singleton,
};
use wormhole::byte_utils::ByteUtils;
type HumanAddr = String;
pub static CONFIG_KEY: &[u8] = b"config";
pub static WRAPPED_ASSET_KEY: &[u8] = b"wrapped_asset";
pub static WRAPPED_ASSET_ADDRESS_KEY: &[u8] = b"wrapped_asset_address";
pub static BRIDGE_CONTRACTS_KEY: &[u8] = b"bridge_contracts";
pub static TOKEN_ID_HASHES_KEY: &[u8] = b"token_id_hashes";
pub static SPL_CACHE_KEY: &[u8] = b"spl_cache";
// Guardian set information
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ConfigInfo {
// governance contract details
pub gov_chain: u16,
pub gov_address: Vec<u8>,
pub wormhole_contract: HumanAddr,
pub wrapped_asset_code_id: u64,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct SplCacheItem {
pub name: [u8; 32],
pub symbol: [u8; 32],
}
pub fn config(storage: &mut dyn Storage) -> Singleton<ConfigInfo> {
singleton(storage, CONFIG_KEY)
}
pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<ConfigInfo> {
singleton_read(storage, CONFIG_KEY)
}
pub fn bridge_contracts(storage: &mut dyn Storage) -> Bucket<Vec<u8>> {
bucket(storage, BRIDGE_CONTRACTS_KEY)
}
pub fn bridge_contracts_read(storage: &dyn Storage) -> ReadonlyBucket<Vec<u8>> {
bucket_read(storage, BRIDGE_CONTRACTS_KEY)
}
pub fn wrapped_asset(storage: &mut dyn Storage) -> Bucket<HumanAddr> {
bucket(storage, WRAPPED_ASSET_KEY)
}
pub fn wrapped_asset_read(storage: &dyn Storage) -> ReadonlyBucket<HumanAddr> {
bucket_read(storage, WRAPPED_ASSET_KEY)
}
pub fn wrapped_asset_address(storage: &mut dyn Storage) -> Bucket<Vec<u8>> {
bucket(storage, WRAPPED_ASSET_ADDRESS_KEY)
}
pub fn wrapped_asset_address_read(storage: &dyn Storage) -> ReadonlyBucket<Vec<u8>> {
bucket_read(storage, WRAPPED_ASSET_ADDRESS_KEY)
}
pub fn spl_cache(storage: &mut dyn Storage) -> Bucket<SplCacheItem> {
bucket(storage, SPL_CACHE_KEY)
}
pub fn spl_cache_read(storage: &dyn Storage) -> ReadonlyBucket<SplCacheItem> {
bucket_read(storage, SPL_CACHE_KEY)
}
pub fn token_id_hashes(storage: &mut dyn Storage, chain: u16, address: [u8; 32]) -> Bucket<String> {
Bucket::multilevel(
storage,
&[TOKEN_ID_HASHES_KEY, &chain.to_be_bytes(), &address],
)
}
pub fn token_id_hashes_read(
storage: &mut dyn Storage,
chain: u16,
address: [u8; 32],
) -> ReadonlyBucket<String> {
ReadonlyBucket::multilevel(
storage,
&[TOKEN_ID_HASHES_KEY, &chain.to_be_bytes(), &address],
)
}
pub struct Action;
impl Action {
pub const TRANSFER: u8 = 1;
}
// 0 u8 action
// 1 [u8] payload
pub struct TokenBridgeMessage {
pub action: u8,
pub payload: Vec<u8>,
}
impl TokenBridgeMessage {
pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
let data = data.as_slice();
let action = data.get_u8(0);
let payload = &data[1..];
Ok(TokenBridgeMessage {
action,
payload: payload.to_vec(),
})
}
pub fn serialize(&self) -> Vec<u8> {
[self.action.to_be_bytes().to_vec(), self.payload.clone()].concat()
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[repr(transparent)]
pub struct BoundedVec<T, const N: usize> {
vec: Vec<T>,
}
impl<T, const N: usize> BoundedVec<T, N> {
pub fn new(vec: Vec<T>) -> StdResult<Self> {
if vec.len() > N {
return Result::Err(StdError::GenericErr {
msg: format!("vector length exceeds {}", N),
});
};
Ok(Self { vec })
}
#[inline]
pub fn to_vec(&self) -> Vec<T>
where
T: Clone,
{
self.vec.clone()
}
#[inline]
pub fn len(&self) -> usize {
self.vec.len()
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TransferInfo {
pub nft_address: [u8; 32],
pub nft_chain: u16,
pub symbol: [u8; 32],
pub name: [u8; 32],
pub token_id: [u8; 32],
pub uri: BoundedVec<u8, 200>, // max 200 bytes due to Solana
pub recipient: [u8; 32],
pub recipient_chain: u16,
}
impl TransferInfo {
pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
let data = data.as_slice();
let mut offset: usize = 0; // offset into data in bytes
let nft_address = data.get_const_bytes::<32>(offset);
offset += 32;
let nft_chain = data.get_u16(offset);
offset += 2;
let symbol = data.get_const_bytes::<32>(offset);
offset += 32;
let name = data.get_const_bytes::<32>(offset);
offset += 32;
let token_id = data.get_const_bytes::<32>(offset);
offset += 32;
let uri_length: usize = data.get_u8(offset).into();
offset += 1;
let uri = data.get_bytes(offset, uri_length).to_vec();
offset += uri_length;
let recipient = data.get_const_bytes::<32>(offset);
offset += 32;
let recipient_chain = data.get_u16(offset);
offset += 2;
if data.len() != offset {
return Result::Err(StdError::GenericErr {
msg: format!(
"Invalid transfer length, expected {}, but got {}",
offset,
data.len()
),
});
}
Ok(TransferInfo {
nft_address,
nft_chain,
symbol,
name,
token_id,
uri: BoundedVec::new(uri.to_vec())?,
recipient,
recipient_chain,
})
}
pub fn serialize(&self) -> Vec<u8> {
[
self.nft_address.to_vec(),
self.nft_chain.to_be_bytes().to_vec(),
self.symbol.to_vec(),
self.name.to_vec(),
self.token_id.to_vec(),
vec![self.uri.len().try_into().unwrap()], // won't panic, because uri.len() is less than 200
self.uri.to_vec(),
self.recipient.to_vec(),
self.recipient_chain.to_be_bytes().to_vec(),
]
.concat()
}
}
pub struct UpgradeContract {
pub new_contract: u64,
}
pub struct RegisterChain {
pub chain_id: u16,
pub chain_address: Vec<u8>,
}
impl UpgradeContract {
pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
let data = data.as_slice();
let new_contract = data.get_u64(24);
Ok(UpgradeContract { new_contract })
}
}
impl RegisterChain {
pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
let data = data.as_slice();
let chain_id = data.get_u16(0);
let chain_address = data[2..].to_vec();
Ok(RegisterChain {
chain_id,
chain_address,
})
}
}

View File

@ -0,0 +1,97 @@
use bigint::U256;
use cosmwasm_std::{
StdError,
StdResult,
Storage,
};
use sha3::{
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: &String) -> Vec<u8> {
let mut hasher = Keccak256::new();
hasher.update(token_id);
hasher.finalize().to_vec()
}
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)
}
}

View File

@ -143,7 +143,7 @@ fn submit_vaa(
fn handle_governance_payload(deps: DepsMut, env: Env, data: &Vec<u8>) -> StdResult<Response> {
let gov_packet = GovernancePacket::deserialize(&data)?;
let module = get_string_from_32(&gov_packet.module)?;
let module = get_string_from_32(&gov_packet.module);
if module != "PythBridge" {
return Err(StdError::generic_err("this is not a valid module"));

View File

@ -381,8 +381,8 @@ fn handle_attest_meta(
CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: contract,
msg: to_binary(&WrappedMsg::UpdateMetadata {
name: get_string_from_32(&meta.name)?,
symbol: get_string_from_32(&meta.symbol)?,
name: get_string_from_32(&meta.name),
symbol: get_string_from_32(&meta.symbol),
})?,
funds: vec![],
})
@ -392,8 +392,8 @@ fn handle_attest_meta(
admin: Some(env.contract.address.clone().into_string()),
code_id: cfg.wrapped_asset_code_id,
msg: to_binary(&WrappedInit {
name: get_string_from_32(&meta.name)?,
symbol: get_string_from_32(&meta.symbol)?,
name: get_string_from_32(&meta.name),
symbol: get_string_from_32(&meta.symbol),
asset_chain: meta.token_chain,
asset_address: meta.token_address.to_vec().into(),
decimals: min(meta.decimals, 8u8),
@ -561,7 +561,7 @@ fn submit_vaa(
fn handle_governance_payload(deps: DepsMut, env: Env, data: &Vec<u8>) -> StdResult<Response> {
let gov_packet = GovernancePacket::deserialize(&data)?;
let module = get_string_from_32(&gov_packet.module)?;
let module = get_string_from_32(&gov_packet.module);
if module != "TokenBridge" {
return Err(StdError::generic_err("this is not a valid module"));

View File

@ -1,8 +1,4 @@
use cosmwasm_std::{
CanonicalAddr,
StdError,
StdResult,
};
use cosmwasm_std::CanonicalAddr;
pub trait ByteUtils {
fn get_u8(&self, index: usize) -> u8;
@ -15,6 +11,8 @@ pub trait ByteUtils {
fn get_u256(&self, index: usize) -> (u128, u128);
fn get_address(&self, index: usize) -> CanonicalAddr;
fn get_bytes32(&self, index: usize) -> &[u8];
fn get_bytes(&self, index: usize, bytes: usize) -> &[u8];
fn get_const_bytes<const N: usize>(&self, index: usize) -> [u8; N];
}
impl ByteUtils for &[u8] {
@ -51,23 +49,49 @@ impl ByteUtils for &[u8] {
fn get_bytes32(&self, index: usize) -> &[u8] {
&self[index..index + 32]
}
fn get_bytes(&self, index: usize, bytes: usize) -> &[u8] {
&self[index..index + bytes]
}
fn get_const_bytes<const N: usize>(&self, index: usize) -> [u8; N] {
let mut bytes: [u8; N] = [0; N];
bytes.copy_from_slice(&self[index..index + N]);
bytes
}
}
/// Left-pad a 20 byte address with 0s
pub fn extend_address_to_32(addr: &CanonicalAddr) -> Vec<u8> {
let mut result: Vec<u8> = vec![0; 12];
result.extend(addr.as_slice());
extend_address_to_32_array(addr).to_vec()
}
pub fn extend_address_to_32_array(addr: &CanonicalAddr) -> [u8; 32] {
let mut v: Vec<u8> = vec![0; 12];
v.extend(addr.as_slice());
let mut result: [u8; 32] = [0; 32];
result.copy_from_slice(&v);
result
}
/// Turn a string into a fixed length array. If the string is shorter than the
/// resulting array, it gets padded with \0s on the right. If longer, it gets
/// truncated.
pub fn string_to_array<const N: usize>(s: &str) -> [u8; N] {
let bytes = s.as_bytes();
let len = usize::min(N, bytes.len());
let zeros = vec![0; N - len];
let padded = [bytes[..len].to_vec(), zeros].concat();
let mut result: [u8; N] = [0; N];
result.copy_from_slice(&padded);
result
}
pub fn extend_string_to_32(s: &str) -> Vec<u8> {
let bytes = s.as_bytes();
let len = usize::min(32, bytes.len());
let result = vec![0; 32 - len];
[bytes[..len].to_vec(), result].concat()
string_to_array::<32>(s).to_vec()
}
pub fn get_string_from_32(v: &Vec<u8>) -> StdResult<String> {
let s = String::from_utf8(v.clone())
.or_else(|_| Err(StdError::generic_err("could not parse string")))?;
Ok(s.chars().filter(|c| c != &'\0').collect())
pub fn get_string_from_32(v: &Vec<u8>) -> String {
let s = String::from_utf8_lossy(v);
s.chars().filter(|c| c != &'\0').collect()
}

View File

@ -1,4 +1,4 @@
import { Wallet, LCDClient, MnemonicKey } from "@terra-money/terra.js";
import { LCDClient, MnemonicKey } from "@terra-money/terra.js";
import {
StdFee,
MsgInstantiateContract,
@ -6,262 +6,288 @@ import {
MsgStoreCode,
} from "@terra-money/terra.js";
import { readFileSync, readdirSync } from "fs";
import { Bech32, toHex } from "@cosmjs/encoding";
import { zeroPad } from "ethers/lib/utils.js";
// TODO: Workaround /tx/estimate_fee errors.
/*
NOTE: Only append to this array: keeping the ordering is crucial, as the
contracts must be imported in a deterministic order so their addresses remain
deterministic.
*/
const artifacts = [
"wormhole.wasm",
"token_bridge.wasm",
"cw20_wrapped.wasm",
"cw20_base.wasm",
"pyth_bridge.wasm",
"nft_bridge.wasm",
"cw721_wrapped.wasm",
"cw721_base.wasm",
];
const gas_prices = {
uluna: "0.15",
usdr: "0.1018",
uusd: "0.15",
ukrw: "178.05",
umnt: "431.6259",
ueur: "0.125",
ucny: "0.97",
ujpy: "16",
ugbp: "0.11",
uinr: "11",
ucad: "0.19",
uchf: "0.13",
uaud: "0.19",
usgd: "0.2",
};
/* Check that the artifact folder contains all the wasm files we expect and nothing else */
async function main() {
const terra = new LCDClient({
URL: "http://localhost:1317",
chainID: "localterra",
});
const actual_artifacts = readdirSync("../artifacts/").filter((a) =>
a.endsWith(".wasm")
);
const wallet = terra.wallet(
new MnemonicKey({
mnemonic:
"notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius",
})
const missing_artifacts = artifacts.filter(
(a) => !actual_artifacts.includes(a)
);
if (missing_artifacts.length) {
console.log(
"Error during terra deployment. The following files are expected to be in the artifacts folder:"
);
missing_artifacts.forEach((file) => console.log(` - ${file}`));
console.log(
"Hint: the deploy script needs to run after the contracts have been built."
);
console.log(
"External binary blobs need to be manually added in tools/Dockerfile."
);
process.exit(1);
}
const unexpected_artifacts = actual_artifacts.filter(
(a) => !artifacts.includes(a)
);
if (unexpected_artifacts.length) {
console.log(
"Error during terra deployment. The following files are not expected to be in the artifacts folder:"
);
unexpected_artifacts.forEach((file) => console.log(` - ${file}`));
console.log("Hint: you might need to modify tools/deploy.js");
process.exit(1);
}
/* Set up terra client & wallet */
const terra = new LCDClient({
URL: "http://localhost:1317",
chainID: "localterra",
});
const wallet = terra.wallet(
new MnemonicKey({
mnemonic:
"notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius",
})
);
await wallet.sequence();
/* Deploy artifacts */
const codeIds = {};
for (const file of artifacts) {
const contract_bytes = readFileSync(`../artifacts/${file}`);
console.log(`Storing WASM: ${file} (${contract_bytes.length} bytes)`);
const store_code = new MsgStoreCode(
wallet.key.accAddress,
contract_bytes.toString("base64")
);
await wallet.sequence();
// Deploy WASM blobs.
// Read a list of files from directory containing compiled contracts.
const artifacts = readdirSync("../artifacts/");
// Sort them to get a determinstic list for consecutive code ids.
artifacts.sort();
artifacts.reverse();
const hardcodedGas = {
"cw20_base.wasm": 4000000,
"cw20_wrapped.wasm": 4000000,
"wormhole.wasm": 5000000,
"token_bridge.wasm": 6000000,
"pyth_bridge.wasm": 5000000,
};
// Deploy all found WASM files and assign Code IDs.
const codeIds = {};
for (const artifact in artifacts) {
if (
artifacts.hasOwnProperty(artifact) &&
artifacts[artifact].includes(".wasm")
) {
const file = artifacts[artifact];
const contract_bytes = readFileSync(`../artifacts/${file}`);
console.log(`Storing WASM: ${file} (${contract_bytes.length} bytes)`);
const store_code = new MsgStoreCode(
wallet.key.accAddress,
contract_bytes.toString("base64")
);
try {
const tx = await wallet.createAndSignTx({
msgs: [store_code],
memo: "",
fee: new StdFee(hardcodedGas[artifacts[artifact]], {
uluna: "100000",
}),
});
const rs = await terra.tx.broadcast(tx);
const ci = /"code_id","value":"([^"]+)/gm.exec(rs.raw_log)[1];
codeIds[file] = parseInt(ci);
} catch (e) {
console.log("Failed to Execute");
}
}
}
console.log(codeIds);
// Governance constants defined by the Wormhole spec.
const govChain = 1;
const govAddress =
"0000000000000000000000000000000000000000000000000000000000000004";
const addresses = {};
// Instantiate Wormhole
console.log("Instantiating Wormhole");
await wallet
.createAndSignTx({
msgs: [
new MsgInstantiateContract(
wallet.key.accAddress,
wallet.key.accAddress,
codeIds["wormhole.wasm"],
{
gov_chain: govChain,
gov_address: Buffer.from(govAddress, "hex").toString("base64"),
guardian_set_expirity: 86400,
initial_guardian_set: {
addresses: [
{
bytes: Buffer.from(
"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe",
"hex"
).toString("base64"),
},
],
expiration_time: 0,
},
}
),
],
try {
const tx = await wallet.createAndSignTx({
msgs: [store_code],
memo: "",
})
.then((tx) => terra.tx.broadcast(tx))
.then((rs) => {
const address = /"contract_address","value":"([^"]+)/gm.exec(
rs.raw_log
)[1];
addresses["wormhole.wasm"] = address;
});
console.log("Instantiating Token Bridge");
await wallet
.createAndSignTx({
msgs: [
new MsgInstantiateContract(
wallet.key.accAddress,
wallet.key.accAddress,
codeIds["token_bridge.wasm"],
{
owner: wallet.key.accAddress,
gov_chain: govChain,
gov_address: Buffer.from(govAddress, "hex").toString("base64"),
wormhole_contract: addresses["wormhole.wasm"],
wrapped_asset_code_id: codeIds["cw20_wrapped.wasm"],
}
),
],
memo: "",
})
.then((tx) => terra.tx.broadcast(tx))
.then((rs) => {
const address = /"contract_address","value":"([^"]+)/gm.exec(
rs.raw_log
)[1];
addresses["token_bridge.wasm"] = address;
});
await wallet
.createAndSignTx({
msgs: [
new MsgInstantiateContract(
wallet.key.accAddress,
undefined,
codeIds["cw20_base.wasm"],
{
name: "MOCK",
symbol: "MCK",
decimals: 6,
initial_balances: [
{
address: wallet.key.accAddress,
amount: "100000000",
},
],
mint: null,
}
),
],
memo: "",
})
.then((tx) => terra.tx.broadcast(tx))
.then((rs) => {
const address = /"contract_address","value":"([^"]+)/gm.exec(
rs.raw_log
)[1];
addresses["mock.wasm"] = address;
});
const pythEmitterAddress =
"71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b";
const pythChain = 1;
// Instantiate Pyth over Wormhole
console.log("Instantiating Pyth over Wormhole");
await wallet
.createAndSignTx({
msgs: [
new MsgInstantiateContract(
wallet.key.accAddress,
wallet.key.accAddress,
codeIds["pyth_bridge.wasm"],
{
gov_chain: govChain,
gov_address: Buffer.from(govAddress, "hex").toString("base64"),
wormhole_contract: addresses["wormhole.wasm"],
pyth_emitter: Buffer.from(pythEmitterAddress, "hex").toString(
"base64"
),
pyth_emitter_chain: pythChain,
}
),
],
memo: "",
})
.then((tx) => terra.tx.broadcast(tx))
.then((rs) => {
const address = /"contract_address","value":"([^"]+)/gm.exec(
rs.raw_log
)[1];
addresses["pyth_bridge.wasm"] = address;
});
console.log(addresses);
const registrations = [
"01000000000100c9f4230109e378f7efc0605fb40f0e1869f2d82fda5b1dfad8a5a2dafee85e033d155c18641165a77a2db6a7afbf2745b458616cb59347e89ae0c7aa3e7cc2d400000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000546f6b656e4272696467650100000001c69a1b1a65dd336bf1df6a77afb501fc25db7fc0938cb08595a9ef473265cb4f",
"01000000000100e2e1975d14734206e7a23d90db48a6b5b6696df72675443293c6057dcb936bf224b5df67d32967adeb220d4fe3cb28be515be5608c74aab6adb31099a478db5c01000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000546f6b656e42726964676501000000020000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16",
];
for (const registration in registrations) {
if (registrations.hasOwnProperty(registration)) {
console.log('Registering');
await wallet
.createAndSignTx({
msgs: [
new MsgExecuteContract(
wallet.key.accAddress,
addresses["token_bridge.wasm"],
{
submit_vaa: {
data: Buffer.from(registrations[registration], "hex").toString('base64'),
},
},
{ uluna: 1000 }
),
],
memo: "",
fee: new StdFee(2000000, {
uluna: "100000",
}),
})
.then((tx) => terra.tx.broadcast(tx))
.then((rs) => console.log(rs));
}
const rs = await terra.tx.broadcast(tx);
const ci = /"code_id","value":"([^"]+)/gm.exec(rs.raw_log)[1];
codeIds[file] = parseInt(ci);
} catch (e) {
console.log(`${e}`);
}
}
main();
console.log(codeIds);
/* Instantiate contracts.
*
* We instantiate the core contracts here (i.e. wormhole itself and the bridge contracts).
* The wrapped asset contracts don't need to be instantiated here, because those
* will be instantiated by the on-chain bridge contracts on demand.
* */
// Governance constants defined by the Wormhole spec.
const govChain = 1;
const govAddress =
"0000000000000000000000000000000000000000000000000000000000000004";
async function instantiate(contract, inst_msg) {
var address;
await wallet
.createAndSignTx({
msgs: [
new MsgInstantiateContract(
wallet.key.accAddress,
wallet.key.accAddress,
codeIds[contract],
inst_msg
),
],
memo: "",
})
.then((tx) => terra.tx.broadcast(tx))
.then((rs) => {
address = /"contract_address","value":"([^"]+)/gm.exec(rs.raw_log)[1];
});
console.log(`Instantiated ${contract} at ${address} (${convert_terra_address_to_hex(address)})`);
return address;
}
// Instantiate contracts. NOTE: Only append at the end, the ordering must be
// deterministic for the addresses to work
const addresses = {};
addresses["wormhole.wasm"] = await instantiate("wormhole.wasm", {
gov_chain: govChain,
gov_address: Buffer.from(govAddress, "hex").toString("base64"),
guardian_set_expirity: 86400,
initial_guardian_set: {
addresses: [
{
bytes: Buffer.from(
"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe",
"hex"
).toString("base64"),
},
],
expiration_time: 0,
},
});
addresses["token_bridge.wasm"] = await instantiate("token_bridge.wasm", {
gov_chain: govChain,
gov_address: Buffer.from(govAddress, "hex").toString("base64"),
wormhole_contract: addresses["wormhole.wasm"],
wrapped_asset_code_id: codeIds["cw20_wrapped.wasm"],
});
addresses["mock.wasm"] = await instantiate("cw20_base.wasm", {
name: "MOCK",
symbol: "MCK",
decimals: 6,
initial_balances: [
{
address: wallet.key.accAddress,
amount: "100000000",
},
],
mint: null,
});
const pythEmitterAddress =
"71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b";
const pythChain = 1;
addresses["pyth_bridge.wasm"] = await instantiate("pyth_bridge.wasm", {
gov_chain: govChain,
gov_address: Buffer.from(govAddress, "hex").toString("base64"),
wormhole_contract: addresses["wormhole.wasm"],
pyth_emitter: Buffer.from(pythEmitterAddress, "hex").toString(
"base64"
),
pyth_emitter_chain: pythChain,
});
addresses["nft_bridge.wasm"] = await instantiate("nft_bridge.wasm", {
gov_chain: govChain,
gov_address: Buffer.from(govAddress, "hex").toString("base64"),
wormhole_contract: addresses["wormhole.wasm"],
wrapped_asset_code_id: codeIds["cw721_wrapped.wasm"],
});
addresses["cw721_base.wasm"] = await instantiate("cw721_base.wasm", {
name: "MOCK",
symbol: "MCK",
minter: wallet.key.accAddress,
});
async function mint_cw721(token_id, token_uri) {
await wallet
.createAndSignTx({
msgs: [
new MsgExecuteContract(
wallet.key.accAddress,
addresses["cw721_base.wasm"],
{
mint: {
token_id: token_id.toString(),
owner: wallet.key.accAddress,
token_uri: token_uri,
},
},
{ uluna: 1000 }
),
],
memo: "",
fee: new StdFee(2000000, {
uluna: "100000",
}),
})
.then((tx) => terra.tx.broadcast(tx));
console.log(`Minted NFT with token_id ${token_id} at ${addresses["cw721_base.wasm"]}`);
}
await mint_cw721(0, 'https://ixmfkhnh2o4keek2457f2v2iw47cugsx23eynlcfpstxihsay7nq.arweave.net/RdhVHafTuKIRWud-XVdItz4qGlfWyYasRXyndB5Ax9s/');
await mint_cw721(1, 'https://portal.neondistrict.io/api/getNft/158456327500392944014123206890');
/* Registrations: tell the bridge contracts to know about each other */
const contract_registrations = {
"token_bridge.wasm": [
// Solana
"01000000000100c9f4230109e378f7efc0605fb40f0e1869f2d82fda5b1dfad8a5a2dafee85e033d155c18641165a77a2db6a7afbf2745b458616cb59347e89ae0c7aa3e7cc2d400000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000546f6b656e4272696467650100000001c69a1b1a65dd336bf1df6a77afb501fc25db7fc0938cb08595a9ef473265cb4f",
// Ethereum
"01000000000100e2e1975d14734206e7a23d90db48a6b5b6696df72675443293c6057dcb936bf224b5df67d32967adeb220d4fe3cb28be515be5608c74aab6adb31099a478db5c01000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000546f6b656e42726964676501000000020000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16",
// BSC
"01000000000100719b4ada436f614489dbf87593c38ba9aea35aa7b997387f8ae09f819806f5654c8d45b6b751faa0e809ccbc294794885efa205bd8a046669464c7cbfb03d183010000000100000001000100000000000000000000000000000000000000000000000000000000000000040000000002c8bb0600000000000000000000000000000000000000000000546f6b656e42726964676501000000040000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16",
],
"nft_bridge.wasm": [
// Solana
"010000000001007985ba742002ae745c19722fea4d82102e68526c7c9d94d0e5d0a809071c98451c9693b230b3390f4ca9555a3ba9a9abbe87cf6f9e400682213e4fbbe1dabb9e0100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000004e4654427269646765010000000196ee982293251b48729804c8e8b24b553eb6b887867024948d2236fd37a577ab",
// Ethereum
"01000000000100d073f81a4ecf2469b0674b1902dcbcad2da7f70ecdd7e1aec65414380ca2c05426380c33bb083ab41167c522231c1485b9c8ffce04eaf2e8a6f50edaa72074c50000000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000004e4654427269646765010000000200000000000000000000000026b4afb60d6c903165150c6f0aa14f8016be4aec",
],
};
for (const [contract, registrations] of Object.entries(
contract_registrations
)) {
console.log(`Registering chains for ${contract}:`);
for (const registration of registrations) {
await wallet
.createAndSignTx({
msgs: [
new MsgExecuteContract(
wallet.key.accAddress,
addresses[contract],
{
submit_vaa: {
data: Buffer.from(registration, "hex").toString("base64"),
},
},
{ uluna: 1000 }
),
],
memo: "",
fee: new StdFee(2000000, {
uluna: "100000",
}),
})
.then((tx) => terra.tx.broadcast(tx))
.then((rs) => console.log(rs));
}
}
// Terra addresses are "human-readable", but for cross-chain registrations, we
// want the "canonical" version
function convert_terra_address_to_hex(human_addr) {
return "0x" + toHex(zeroPad(Bech32.decode(human_addr).data, 32));
}

View File

@ -207,7 +207,7 @@ async def main():
registrations = [
'01000000000100c9f4230109e378f7efc0605fb40f0e1869f2d82fda5b1dfad8a5a2dafee85e033d155c18641165a77a2db6a7afbf2745b458616cb59347e89ae0c7aa3e7cc2d400000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000546f6b656e4272696467650100000001c69a1b1a65dd336bf1df6a77afb501fc25db7fc0938cb08595a9ef473265cb4f',
'01000000000100e2e1975d14734206e7a23d90db48a6b5b6696df72675443293c6057dcb936bf224b5df67d32967adeb220d4fe3cb28be515be5608c74aab6adb31099a478db5c01000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000546f6b656e42726964676501000000020000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16'
'01000000000100e2e1975d14734206e7a23d90db48a6b5b6696df72675443293c6057dcb936bf224b5df67d32967adeb220d4fe3cb28be515be5608c74aab6adb31099a478db5c01000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000546f6b656e42726964676501000000020000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16',
'01000000000100719b4ada436f614489dbf87593c38ba9aea35aa7b997387f8ae09f819806f5654c8d45b6b751faa0e809ccbc294794885efa205bd8a046669464c7cbfb03d183010000000100000001000100000000000000000000000000000000000000000000000000000000000000040000000002c8bb0600000000000000000000000000000000000000000000546f6b656e42726964676501000000040000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16'
]

View File

@ -10,6 +10,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"@terra-money/terra.js": "^2.0.11"
"@terra-money/terra.js": "^2.0.11",
"@cosmjs/encoding": "^0.26.2",
"ethers": "^5.4.4"
}
}

View File

@ -36,8 +36,9 @@ NFTs ("Wrapped NFTs") and custody locked NFTs.
We aim to support:
- EIP721: Ethereum, BSC
- EIP721 with token_uri extension: Ethereum, BSC
- Metaplex SPL Meta: Solana
- CW721 with token_uri extension: Terra
## Detailed Design
@ -69,7 +70,7 @@ message.
Since every NFT has unique metadata the Transfer messages contain all metadata, a transfer (even the first on per NFT)
only requires a single Wormhole message to be passed compared to the Token Bridge. On the first transfer action of an
NFT (address / symbol / name) a wrapped asset (i.e. master edition or new contract) is created. When the wrapped asset (
contract) is already initialized or was just initialized, the (new) token_id and metadata URL are registered.
contract) is already initialized or was just initialized, the (new) token_id and metadata URI are registered.
### API / database schema
@ -100,9 +101,9 @@ Symbol [32]uint8
Name [32]uint8
// ID of the token (big-endian uint256)
TokenID [32]uint8
// URL of the NFT
URLLength u8
URL [n]uint8
// URI of the NFT. Valid utf8 string, maximum 200 bytes.
URILength u8
URI [n]uint8
// Address of the recipient. Left-zero-padded if shorter than 32 bytes
To [32]uint8
// Chain ID of the recipient
@ -138,7 +139,21 @@ native asset on its chain, there may be transfers initiated for assets that don'
target chain. However, the transfer will become executable once the wrapped asset is set up (which can be done any time)
.
The name and symbol fields of the Transfer payload are not guaranteed to be
valid UTF8 strings. Implementations might truncate longer strings at the 32 byte
mark, which may result in invalid UTF8 bytes at the end. Thus, any client
whishing to present these as strings must validate them first, potentially
dropping the garbage at the end.
Currently Solana only supports u64 token ids which is incompatible with Ethereum which specifically mentions the use of
UUIDs as token ids (utilizing all bytes of the uint256). There will either need to be a mechanism to translate ids i.e.
a map of `[32]u8 -> incrementing_u64` (in the expectation there will never be more than MaxU64 editions) or Solana needs
to change their NFT contract.
to change their NFT contract.
Terra CW721 contracts support arbitrary strings as token IDs. In order to fit
them into 32 bytes, we store their keccak256 hash instead. This means that when
transferring a terra-native NFT through the wormhole, the ID of the output token
will be the original token's hash. However, wrapped assets on terra will retain
their original token ids, simply stringified into a decimal number. Then,
when transferring them back through the wormhole, we can guarantee that these
ids will parse as a uint256.