terra/nft_bridge: Implement nft-bridge for terra (#698)
commit-id:0b547fa5
This commit is contained in:
parent
089d7cde97
commit
7e212fa739
|
@ -99,7 +99,7 @@ yargs(hideBin(process.argv))
|
|||
1,
|
||||
1,
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000004",
|
||||
0,
|
||||
Math.floor(Math.random() * 100000000),
|
||||
data,
|
||||
[
|
||||
"cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"))]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
|
@ -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"
|
|
@ -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()
|
||||
}
|
|
@ -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;
|
|
@ -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 },
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,27 +6,62 @@ 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 */
|
||||
|
||||
const actual_artifacts = readdirSync("../artifacts/").filter((a) =>
|
||||
a.endsWith(".wasm")
|
||||
);
|
||||
|
||||
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 */
|
||||
|
||||
async function main() {
|
||||
const terra = new LCDClient({
|
||||
URL: "http://localhost:1317",
|
||||
chainID: "localterra",
|
||||
|
@ -41,32 +76,11 @@ async function main() {
|
|||
|
||||
await wallet.sequence();
|
||||
|
||||
// Deploy WASM blobs.
|
||||
// Read a list of files from directory containing compiled contracts.
|
||||
const artifacts = readdirSync("../artifacts/");
|
||||
/* Deploy 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];
|
||||
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(
|
||||
|
@ -78,38 +92,58 @@ async function main() {
|
|||
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(`${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
const addresses = {};
|
||||
|
||||
// Instantiate Wormhole
|
||||
console.log("Instantiating Wormhole");
|
||||
async function instantiate(contract, inst_msg) {
|
||||
var address;
|
||||
await wallet
|
||||
.createAndSignTx({
|
||||
msgs: [
|
||||
new MsgInstantiateContract(
|
||||
wallet.key.accAddress,
|
||||
wallet.key.accAddress,
|
||||
codeIds["wormhole.wasm"],
|
||||
{
|
||||
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,
|
||||
|
@ -124,54 +158,16 @@ async function main() {
|
|||
],
|
||||
expiration_time: 0,
|
||||
},
|
||||
}
|
||||
),
|
||||
],
|
||||
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,
|
||||
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"],
|
||||
}
|
||||
),
|
||||
],
|
||||
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"],
|
||||
{
|
||||
addresses["mock.wasm"] = await instantiate("cw20_base.wasm", {
|
||||
name: "MOCK",
|
||||
symbol: "MCK",
|
||||
decimals: 6,
|
||||
|
@ -182,33 +178,13 @@ async function main() {
|
|||
},
|
||||
],
|
||||
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"],
|
||||
{
|
||||
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"],
|
||||
|
@ -216,38 +192,84 @@ async function main() {
|
|||
"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);
|
||||
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"],
|
||||
});
|
||||
|
||||
const registrations = [
|
||||
"01000000000100c9f4230109e378f7efc0605fb40f0e1869f2d82fda5b1dfad8a5a2dafee85e033d155c18641165a77a2db6a7afbf2745b458616cb59347e89ae0c7aa3e7cc2d400000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000546f6b656e4272696467650100000001c69a1b1a65dd336bf1df6a77afb501fc25db7fc0938cb08595a9ef473265cb4f",
|
||||
"01000000000100e2e1975d14734206e7a23d90db48a6b5b6696df72675443293c6057dcb936bf224b5df67d32967adeb220d4fe3cb28be515be5608c74aab6adb31099a478db5c01000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000546f6b656e42726964676501000000020000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16",
|
||||
];
|
||||
addresses["cw721_base.wasm"] = await instantiate("cw721_base.wasm", {
|
||||
name: "MOCK",
|
||||
symbol: "MCK",
|
||||
minter: wallet.key.accAddress,
|
||||
});
|
||||
|
||||
for (const registration in registrations) {
|
||||
if (registrations.hasOwnProperty(registration)) {
|
||||
console.log('Registering');
|
||||
async function mint_cw721(token_id, token_uri) {
|
||||
await wallet
|
||||
.createAndSignTx({
|
||||
msgs: [
|
||||
new MsgExecuteContract(
|
||||
wallet.key.accAddress,
|
||||
addresses["token_bridge.wasm"],
|
||||
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(registrations[registration], "hex").toString('base64'),
|
||||
data: Buffer.from(registration, "hex").toString("base64"),
|
||||
},
|
||||
},
|
||||
{ uluna: 1000 }
|
||||
|
@ -262,6 +284,10 @@ async function main() {
|
|||
.then((rs) => console.log(rs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
|
|
@ -207,7 +207,7 @@ async def main():
|
|||
|
||||
registrations = [
|
||||
'01000000000100c9f4230109e378f7efc0605fb40f0e1869f2d82fda5b1dfad8a5a2dafee85e033d155c18641165a77a2db6a7afbf2745b458616cb59347e89ae0c7aa3e7cc2d400000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000546f6b656e4272696467650100000001c69a1b1a65dd336bf1df6a77afb501fc25db7fc0938cb08595a9ef473265cb4f',
|
||||
'01000000000100e2e1975d14734206e7a23d90db48a6b5b6696df72675443293c6057dcb936bf224b5df67d32967adeb220d4fe3cb28be515be5608c74aab6adb31099a478db5c01000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000546f6b656e42726964676501000000020000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16'
|
||||
'01000000000100e2e1975d14734206e7a23d90db48a6b5b6696df72675443293c6057dcb936bf224b5df67d32967adeb220d4fe3cb28be515be5608c74aab6adb31099a478db5c01000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000546f6b656e42726964676501000000020000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16',
|
||||
'01000000000100719b4ada436f614489dbf87593c38ba9aea35aa7b997387f8ae09f819806f5654c8d45b6b751faa0e809ccbc294794885efa205bd8a046669464c7cbfb03d183010000000100000001000100000000000000000000000000000000000000000000000000000000000000040000000002c8bb0600000000000000000000000000000000000000000000546f6b656e42726964676501000000040000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16'
|
||||
]
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
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.
|
||||
|
|
Loading…
Reference in New Issue