From 16cc520187ebc74940de1bb8a9357ed60a1db1b5 Mon Sep 17 00:00:00 2001 From: Alwin Date: Mon, 31 May 2021 09:13:59 -0400 Subject: [PATCH] initial terra contracts for wormhole v2 Change-Id: Ie28f1e7ce381fcbf33de609bc6c8465679cd2a43 --- terra/Cargo.lock | 150 +++-- terra/Cargo.toml | 2 +- terra/build.sh | 6 + terra/contracts/cw20-wrapped/src/msg.rs | 4 +- terra/contracts/cw20-wrapped/src/state.rs | 2 +- terra/contracts/token-bridge/.cargo/config | 5 + terra/contracts/token-bridge/Cargo.toml | 36 ++ .../contracts/token-bridge/src/byte_utils.rs | 67 +++ terra/contracts/token-bridge/src/contract.rs | 548 ++++++++++++++++++ terra/contracts/token-bridge/src/error.rs | 114 ++++ terra/contracts/token-bridge/src/lib.rs | 14 + terra/contracts/token-bridge/src/msg.rs | 60 ++ terra/contracts/token-bridge/src/state.rs | 186 ++++++ .../token-bridge/tests/integration.rs | 90 +++ terra/contracts/wormhole/src/byte_utils.rs | 26 +- terra/contracts/wormhole/src/contract.rs | 401 +++---------- terra/contracts/wormhole/src/msg.rs | 23 +- terra/contracts/wormhole/src/state.rs | 80 ++- terra/deploy.py | 269 +++++++++ 19 files changed, 1672 insertions(+), 411 deletions(-) create mode 100755 terra/build.sh create mode 100644 terra/contracts/token-bridge/.cargo/config create mode 100644 terra/contracts/token-bridge/Cargo.toml create mode 100644 terra/contracts/token-bridge/src/byte_utils.rs create mode 100644 terra/contracts/token-bridge/src/contract.rs create mode 100644 terra/contracts/token-bridge/src/error.rs create mode 100644 terra/contracts/token-bridge/src/lib.rs create mode 100644 terra/contracts/token-bridge/src/msg.rs create mode 100644 terra/contracts/token-bridge/src/state.rs create mode 100644 terra/contracts/token-bridge/tests/integration.rs create mode 100644 terra/deploy.py diff --git a/terra/Cargo.lock b/terra/Cargo.lock index 472e9f07..6a183b60 100644 --- a/terra/Cargo.lock +++ b/terra/Cargo.lock @@ -2,11 +2,11 @@ # It is not intended for manual editing. [[package]] name = "addr2line" -version = "0.14.1" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" +checksum = "03345e98af8f3d786b6d9f656ccfa6ac316d954e92bc4841f0bba20789d5fb5a" dependencies = [ - "gimli 0.23.0", + "gimli 0.24.0", ] [[package]] @@ -35,11 +35,12 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "backtrace" -version = "0.3.56" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" +checksum = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744" dependencies = [ "addr2line", + "cc", "cfg-if 1.0.0", "libc", "miniz_oxide", @@ -54,12 +55,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" [[package]] -name = "bincode" -version = "1.3.1" +name = "bigint" +version = "4.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d" +checksum = "c0e8c8a600052b52482eff2cf4d810e462fdff1f656ac1ecb6232132a1ed7def" dependencies = [ "byteorder", + "crunchy", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ "serde", ] @@ -82,9 +92,9 @@ dependencies = [ [[package]] name = "blake3" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9ff35b701f3914bdb8fad3368d822c766ef2858b2583198e41639b936f09d3f" +checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" dependencies = [ "arrayref", "arrayvec", @@ -119,9 +129,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cc" -version = "1.0.67" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" [[package]] name = "cfg-if" @@ -194,10 +204,13 @@ dependencies = [ ] [[package]] -name = "cpuid-bool" -version = "0.1.2" +name = "cpufeatures" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" +checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8" +dependencies = [ + "libc", +] [[package]] name = "cranelift-bforest" @@ -261,9 +274,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -282,9 +295,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2584f639eb95fea8c798496315b297cf81b9b58b6d30ab066a75455333cf4b12" +checksum = "52fb27eab85b17fbb9f6fd667089e07d6a2eb8743d02639ee7f6a7a7729c9c94" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -295,15 +308,21 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" +checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278" dependencies = [ "autocfg", "cfg-if 1.0.0", "lazy_static", ] +[[package]] +name = "crunchy" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2f4a431c5c9f662e1200b7c7f02c34e91361150e382089a8f2dec3ba680cbda" + [[package]] name = "crypto-mac" version = "0.8.0" @@ -521,9 +540,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" +checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" [[package]] name = "group" @@ -609,9 +628,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.91" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7" +checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" [[package]] name = "lock_api" @@ -643,9 +662,9 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.6.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" dependencies = [ "autocfg", ] @@ -685,9 +704,9 @@ dependencies = [ [[package]] name = "object" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" +checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170" [[package]] name = "opaque-debug" @@ -737,9 +756,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" dependencies = [ "unicode-xid", ] @@ -778,9 +797,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" dependencies = [ "autocfg", "crossbeam-deque", @@ -790,9 +809,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -809,9 +828,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "rustc-demangle" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" +checksum = "410f7acf3cb3a44527c5d9546bad4bf4e6c460915d5f9f2fc524498bfe8f70ce" [[package]] name = "rustc_version" @@ -874,9 +893,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.125" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" dependencies = [ "serde_derive", ] @@ -911,9 +930,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.125" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" dependencies = [ "proc-macro2", "quote", @@ -944,13 +963,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de" +checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" dependencies = [ "block-buffer", "cfg-if 1.0.0", - "cpuid-bool", + "cpufeatures", "digest 0.9.0", "opaque-debug", ] @@ -1013,9 +1032,9 @@ checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" [[package]] name = "syn" -version = "1.0.64" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f" +checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" dependencies = [ "proc-macro2", "quote", @@ -1030,24 +1049,47 @@ checksum = "ab0e7238dcc7b40a7be719a25365910f6807bd864f4cce6b2e6b873658e2b19d" [[package]] name = "thiserror" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "token-bridge" +version = "0.1.0" +dependencies = [ + "bigint", + "cosmwasm-std", + "cosmwasm-storage", + "cosmwasm-vm", + "cw20", + "cw20-base", + "cw20-wrapped", + "generic-array 0.14.4", + "hex", + "k256", + "lazy_static", + "schemars", + "serde", + "serde_json", + "sha3", + "thiserror", + "wormhole", +] + [[package]] name = "typenum" version = "1.13.0" @@ -1056,9 +1098,9 @@ checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" [[package]] name = "unicode-xid" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "version_check" @@ -1231,6 +1273,6 @@ checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" [[package]] name = "zeroize" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81a974bcdd357f0dca4d41677db03436324d45a4c9ed2d0b873a5a360ce41c36" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" diff --git a/terra/Cargo.toml b/terra/Cargo.toml index 58642782..924e0591 100644 --- a/terra/Cargo.toml +++ b/terra/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["contracts/cw20-wrapped", "contracts/wormhole"] +members = ["contracts/cw20-wrapped", "contracts/wormhole", "contracts/token-bridge"] [profile.release] opt-level = 3 diff --git a/terra/build.sh b/terra/build.sh new file mode 100755 index 00000000..3db2ca02 --- /dev/null +++ b/terra/build.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/workspace-optimizer:0.10.7 diff --git a/terra/contracts/cw20-wrapped/src/msg.rs b/terra/contracts/cw20-wrapped/src/msg.rs index eb0919a1..3d55827e 100644 --- a/terra/contracts/cw20-wrapped/src/msg.rs +++ b/terra/contracts/cw20-wrapped/src/msg.rs @@ -7,7 +7,7 @@ use cw20::Expiration; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct InitMsg { - pub asset_chain: u8, + pub asset_chain: u16, pub asset_address: Binary, pub decimals: u8, pub mint: Option, @@ -105,7 +105,7 @@ pub enum QueryMsg { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct WrappedAssetInfoResponse { - pub asset_chain: u8, // Asset chain id + pub asset_chain: u16, // Asset chain id pub asset_address: Binary, // Asset smart contract address in the original chain pub bridge: HumanAddr, // Bridge address, authorized to mint and burn wrapped tokens } diff --git a/terra/contracts/cw20-wrapped/src/state.rs b/terra/contracts/cw20-wrapped/src/state.rs index 30c8f1e9..9997c38c 100644 --- a/terra/contracts/cw20-wrapped/src/state.rs +++ b/terra/contracts/cw20-wrapped/src/state.rs @@ -9,7 +9,7 @@ pub const KEY_WRAPPED_ASSET: &[u8] = b"wrappedAsset"; // Created at initialization and reference original asset and bridge address #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct WrappedAssetInfo { - pub asset_chain: u8, // Asset chain id + pub asset_chain: u16, // Asset chain id pub asset_address: Binary, // Asset smart contract address on the original chain pub bridge: CanonicalAddr, // Bridge address, authorized to mint and burn wrapped tokens } diff --git a/terra/contracts/token-bridge/.cargo/config b/terra/contracts/token-bridge/.cargo/config new file mode 100644 index 00000000..2d5cce4e --- /dev/null +++ b/terra/contracts/token-bridge/.cargo/config @@ -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" \ No newline at end of file diff --git a/terra/contracts/token-bridge/Cargo.toml b/terra/contracts/token-bridge/Cargo.toml new file mode 100644 index 00000000..b00d6af1 --- /dev/null +++ b/terra/contracts/token-bridge/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "token-bridge" +version = "0.1.0" +authors = ["Yuriy Savchenko "] +edition = "2018" +description = "Wormhole token 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.10.0" } +cosmwasm-storage = { version = "0.10.0" } +schemars = "0.7" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +cw20 = "0.2.2" +cw20-base = { version = "0.2.2", features = ["library"] } +cw20-wrapped = { path = "../cw20-wrapped", features = ["library"] } +wormhole = { path = "../wormhole", features = ["library"] } + +thiserror = { version = "1.0.20" } +k256 = { version = "0.5.9", default-features = false, features = ["ecdsa"] } +sha3 = { version = "0.9.1", default-features = false } +generic-array = { version = "0.14.4" } +hex = "0.4.2" +lazy_static = "1.4.0" +bigint = "4" + +[dev-dependencies] +cosmwasm-vm = { version = "0.10.0", default-features = false, features = ["default-cranelift"] } +serde_json = "1.0" \ No newline at end of file diff --git a/terra/contracts/token-bridge/src/byte_utils.rs b/terra/contracts/token-bridge/src/byte_utils.rs new file mode 100644 index 00000000..7e1c0f9b --- /dev/null +++ b/terra/contracts/token-bridge/src/byte_utils.rs @@ -0,0 +1,67 @@ +use cosmwasm_std::{CanonicalAddr, StdError, StdResult}; + +pub trait ByteUtils { + fn get_u8(&self, index: usize) -> u8; + fn get_u16(&self, index: usize) -> u16; + fn get_u32(&self, index: usize) -> u32; + fn get_u64(&self, index: usize) -> u64; + + fn get_u128_be(&self, index: usize) -> u128; + /// High 128 then low 128 + fn get_u256(&self, index: usize) -> (u128, u128); + fn get_address(&self, index: usize) -> CanonicalAddr; + fn get_bytes32(&self, index: usize) -> &[u8]; +} + +impl ByteUtils for &[u8] { + fn get_u8(&self, index: usize) -> u8 { + self[index] + } + fn get_u16(&self, index: usize) -> u16 { + let mut bytes: [u8; 16 / 8] = [0; 16 / 8]; + bytes.copy_from_slice(&self[index..index + 2]); + u16::from_be_bytes(bytes) + } + fn get_u32(&self, index: usize) -> u32 { + let mut bytes: [u8; 32 / 8] = [0; 32 / 8]; + bytes.copy_from_slice(&self[index..index + 4]); + u32::from_be_bytes(bytes) + } + fn get_u64(&self, index: usize) -> u64 { + let mut bytes: [u8; 64 / 8] = [0; 64 / 8]; + bytes.copy_from_slice(&self[index..index + 8]); + u64::from_be_bytes(bytes) + } + fn get_u128_be(&self, index: usize) -> u128 { + let mut bytes: [u8; 128 / 8] = [0; 128 / 8]; + bytes.copy_from_slice(&self[index..index + 128 / 8]); + u128::from_be_bytes(bytes) + } + fn get_u256(&self, index: usize) -> (u128, u128) { + (self.get_u128_be(index), self.get_u128_be(index + 128 / 8)) + } + fn get_address(&self, index: usize) -> CanonicalAddr { + // 32 bytes are reserved for addresses, but only the last 20 bytes are taken by the actual address + CanonicalAddr::from(&self[index + 32 - 20..index + 32]) + } + fn get_bytes32(&self, index: usize) -> &[u8] { + &self[index..index + 32] + } +} + +pub fn extend_address_to_32(addr: &CanonicalAddr) -> Vec { + let mut result: Vec = vec![0; 12]; + result.extend(addr.as_slice()); + result +} + +pub fn extend_string_to_32(s: &String) -> StdResult> { + let bytes = s.as_bytes(); + if bytes.len() > 32 { + return Err(StdError::generic_err("string more than 32 ")) + } + + let mut result = vec![0; 32 - bytes.len()]; + result.extend(bytes); + Ok(result) +} \ No newline at end of file diff --git a/terra/contracts/token-bridge/src/contract.rs b/terra/contracts/token-bridge/src/contract.rs new file mode 100644 index 00000000..e6651169 --- /dev/null +++ b/terra/contracts/token-bridge/src/contract.rs @@ -0,0 +1,548 @@ +use crate::msg::WrappedRegistryResponse; +use cosmwasm_std::{ + log, to_binary, Api, Binary, CanonicalAddr, CosmosMsg, Env, Extern, HandleResponse, HumanAddr, + InitResponse, Querier, QueryRequest, StdError, StdResult, Storage, Uint128, WasmMsg, WasmQuery, +}; + +use crate::byte_utils::ByteUtils; +use crate::byte_utils::{extend_address_to_32, extend_string_to_32}; +use crate::error::ContractError; +use crate::msg::{HandleMsg, InitMsg, QueryMsg}; +use crate::state::{ + bridge_contracts, bridge_contracts_read, config, config_read, wrapped_asset, + wrapped_asset_address, wrapped_asset_address_read, wrapped_asset_read, Action, AssetMeta, + ConfigInfo, TokenBridgeMessage, TransferInfo, +}; + +use cw20_base::msg::HandleMsg as TokenMsg; +use cw20_base::msg::QueryMsg as TokenQuery; + +use wormhole::msg::HandleMsg as WormholeHandleMsg; +use wormhole::msg::QueryMsg as WormholeQueryMsg; + +use wormhole::state::ParsedVAA; + +use cw20::TokenInfoResponse; + +use cw20_wrapped::msg::HandleMsg as WrappedMsg; +use cw20_wrapped::msg::InitMsg as WrappedInit; +use cw20_wrapped::msg::QueryMsg as WrappedQuery; +use cw20_wrapped::msg::{InitHook, WrappedAssetInfoResponse}; + +use sha3::{Digest, Keccak256}; + +// Chain ID of Terra +const CHAIN_ID: u16 = 3; + +const WRAPPED_ASSET_UPDATING: &str = "updating"; + +pub fn init( + deps: &mut Extern, + _env: Env, + msg: InitMsg, +) -> StdResult { + // Save general wormhole info + let state = ConfigInfo { + owner: msg.owner, + wormhole_contract: msg.wormhole_contract, + wrapped_asset_code_id: msg.wrapped_asset_code_id, + }; + config(&mut deps.storage).save(&state)?; + + Ok(InitResponse::default()) +} + +pub fn parse_vaa( + deps: &mut Extern, + block_time: u64, + data: &Binary, +) -> StdResult { + 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) +} + +pub fn handle( + deps: &mut Extern, + env: Env, + msg: HandleMsg, +) -> StdResult { + match msg { + HandleMsg::RegisterAssetHook { asset_id } => { + handle_register_asset(deps, env, &asset_id.as_slice()) + } + HandleMsg::InitiateTransfer { + asset, + amount, + recipient_chain, + recipient, + nonce, + } => handle_initiate_transfer( + deps, + env, + asset, + amount, + recipient_chain, + recipient.as_slice().to_vec(), + nonce, + ), + HandleMsg::SubmitVaa { data } => submit_vaa(deps, env, &data), + HandleMsg::RegisterChain { + chain_id, + chain_address, + } => handle_register_chain(deps, env, chain_id, chain_address.as_slice().to_vec()), + HandleMsg::CreateAssetMeta { + asset_address, + nonce, + } => handle_create_asset_meta(deps, env, &asset_address, nonce), + } +} + +fn handle_register_chain( + deps: &mut Extern, + env: Env, + chain_id: u16, + chain_address: Vec, +) -> StdResult { + let cfg = config_read(&deps.storage).load()?; + + if env.message.sender != cfg.owner { + return Err(StdError::unauthorized()); + } + + 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(&mut deps.storage); + bucket.save(&chain_id.to_be_bytes(), &chain_address)?; + + Ok(HandleResponse { + messages: vec![], + log: vec![ + log("chain_id", chain_id), + log("chain_address", hex::encode(chain_address)), + ], + data: None, + }) +} + +/// Handle wrapped asset registration messages +fn handle_register_asset( + deps: &mut Extern, + env: Env, + asset_id: &[u8], +) -> StdResult { + let mut bucket = wrapped_asset(&mut deps.storage); + let result = bucket.load(asset_id); + let result = result.map_err(|_| ContractError::RegistrationForbidden.std())?; + if result != HumanAddr::from(WRAPPED_ASSET_UPDATING) { + return ContractError::AssetAlreadyRegistered.std_err(); + } + + bucket.save(asset_id, &env.message.sender)?; + + let contract_address: CanonicalAddr = deps.api.canonical_address(&env.message.sender)?; + wrapped_asset_address(&mut deps.storage) + .save(contract_address.as_slice(), &asset_id.to_vec())?; + + Ok(HandleResponse { + messages: vec![], + log: vec![ + log("action", "register_asset"), + log("asset_id", format!("{:?}", asset_id)), + log("contract_addr", env.message.sender), + ], + data: None, + }) +} + +fn handle_attest_meta( + deps: &mut Extern, + env: Env, + data: &Vec, +) -> StdResult { + let meta = AssetMeta::deserialize(data)?; + if CHAIN_ID == meta.token_chain { + return Err(StdError::generic_err("matching chain id, kinda cringe")); + } + + let cfg = config_read(&deps.storage).load()?; + let asset_id = build_asset_id(meta.token_chain, &meta.token_address.as_slice()); + + wrapped_asset(&mut deps.storage).save(&asset_id, &HumanAddr::from(WRAPPED_ASSET_UPDATING))?; + + Ok(HandleResponse { + messages: vec![CosmosMsg::Wasm(WasmMsg::Instantiate { + code_id: cfg.wrapped_asset_code_id, + msg: to_binary(&WrappedInit { + asset_chain: meta.token_chain, + asset_address: meta.token_address.to_vec().into(), + decimals: meta.decimals, + mint: None, + init_hook: Some(InitHook { + contract_addr: env.contract.address, + msg: to_binary(&HandleMsg::RegisterAssetHook { + asset_id: asset_id.to_vec().into(), + })?, + }), + })?, + send: vec![], + label: None, + })], + log: vec![], + data: None, + }) +} + +fn handle_create_asset_meta( + deps: &mut Extern, + env: Env, + asset_address: &HumanAddr, + nonce: u32, +) -> StdResult { + let cfg = config_read(&deps.storage).load()?; + + let request = QueryRequest::<()>::Wasm(WasmQuery::Smart { + contract_addr: asset_address.clone(), + msg: to_binary(&TokenQuery::TokenInfo {})?, + }); + + let asset_canonical = deps.api.canonical_address(asset_address)?; + let token_info: TokenInfoResponse = deps.querier.custom_query(&request)?; + + let meta: AssetMeta = AssetMeta { + token_chain: CHAIN_ID, + token_address: extend_address_to_32(&asset_canonical), + decimals: token_info.decimals, + symbol: extend_string_to_32(&token_info.symbol)?, + name: extend_string_to_32(&token_info.name)?, + }; + + let token_bridge_message = TokenBridgeMessage { + action: Action::ATTEST_META, + payload: meta.serialize().to_vec(), + }; + + Ok(HandleResponse { + messages: vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cfg.wormhole_contract, + msg: to_binary(&WormholeHandleMsg::PostMessage { + message: Binary::from(token_bridge_message.serialize()), + nonce, + })?, + // forward coins sent to this message + send: env.message.sent_funds.clone(), + })], + log: vec![ + log("meta.token_chain", CHAIN_ID), + log("meta.token", asset_address), + log("meta.nonce", nonce), + log("meta.block_time", env.block.time), + ], + data: None, + }) +} + +fn submit_vaa( + deps: &mut Extern, + env: Env, + data: &Binary, +) -> StdResult { + let vaa = parse_vaa(deps, env.block.time, data)?; + let data = vaa.payload; + + let message = TokenBridgeMessage::deserialize(&data)?; + + let result = match message.action { + Action::TRANSFER => handle_complete_transfer( + deps, + env, + vaa.emitter_chain, + vaa.emitter_address, + &message.payload, + ), + Action::ATTEST_META => handle_attest_meta(deps, env, &message.payload), + _ => ContractError::InvalidVAAAction.std_err(), + }; + return result; +} + +fn handle_complete_transfer( + deps: &mut Extern, + env: Env, + emitter_chain: u16, + emitter_address: Vec, + data: &Vec, +) -> StdResult { + let transfer_info = TransferInfo::deserialize(&data)?; + + 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::unauthorized()); + } + + if transfer_info.recipient_chain != CHAIN_ID { + return Err(StdError::generic_err( + "you sent the message to the wrong chain, idiot", + )); + } + + let token_chain = transfer_info.token_chain; + let target_address = (&transfer_info.recipient.as_slice()).get_address(0); + + let (not_supported_amount, amount) = transfer_info.amount; + + // Check high 128 bit of amount value to be empty + if not_supported_amount != 0 { + return ContractError::AmountTooHigh.std_err(); + } + + if token_chain != CHAIN_ID { + let asset_address = transfer_info.token_address; + let asset_id = build_asset_id(token_chain, &asset_address); + + // Check if this asset is already deployed + let contract_addr = wrapped_asset_read(&deps.storage).load(&asset_id).ok(); + + return if let Some(contract_addr) = contract_addr { + // Asset already deployed, just mint + + let recipient = deps + .api + .human_address(&target_address) + .or_else(|_| ContractError::WrongTargetAddressFormat.std_err())?; + + Ok(HandleResponse { + messages: vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.clone(), + msg: to_binary(&WrappedMsg::Mint { + recipient: recipient.clone(), + amount: Uint128::from(amount), + })?, + send: vec![], + })], + log: vec![ + log("action", "complete_transfer_wrapped"), + log("contract", contract_addr), + log("recipient", recipient), + log("amount", amount), + ], + data: None, + }) + } else { + Err(StdError::generic_err("Wrapped asset not deployed. To deploy, invoke CreateWrapped with the associated AssetMeta")) + }; + } else { + let token_address = transfer_info.token_address.as_slice().get_address(0); + + let recipient = deps.api.human_address(&target_address)?; + let contract_addr = deps.api.human_address(&token_address)?; + Ok(HandleResponse { + messages: vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr.clone(), + msg: to_binary(&TokenMsg::Transfer { + recipient: recipient.clone(), + amount: Uint128::from(amount), + })?, + send: vec![], + })], + log: vec![ + log("action", "complete_transfer_native"), + log("recipient", recipient), + log("contract", contract_addr), + log("amount", amount), + ], + data: None, + }) + } +} + +fn handle_initiate_transfer( + deps: &mut Extern, + env: Env, + asset: HumanAddr, + amount: Uint128, + recipient_chain: u16, + recipient: Vec, + nonce: u32, +) -> StdResult { + // if recipient_chain == CHAIN_ID { + // return ContractError::SameSourceAndTarget.std_err(); + // } + + if amount.is_zero() { + return ContractError::AmountTooLow.std_err(); + } + + let asset_chain: u16; + let asset_address: Vec; + + let cfg: ConfigInfo = config_read(&deps.storage).load()?; + let asset_canonical: CanonicalAddr = deps.api.canonical_address(&asset)?; + + let mut messages: Vec = vec![]; + + match wrapped_asset_address_read(&deps.storage).load(asset_canonical.as_slice()) { + Ok(_) => { + // This is a deployed wrapped asset, burn it + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: asset.clone(), + msg: to_binary(&WrappedMsg::Burn { + account: env.message.sender.clone(), + amount, + })?, + send: vec![], + })); + let request = QueryRequest::<()>::Wasm(WasmQuery::Smart { + contract_addr: asset, + msg: to_binary(&WrappedQuery::WrappedAssetInfo {})?, + }); + let wrapped_token_info: WrappedAssetInfoResponse = + deps.querier.custom_query(&request)?; + asset_chain = wrapped_token_info.asset_chain; + asset_address = wrapped_token_info.asset_address.as_slice().to_vec(); + } + Err(_) => { + // This is a regular asset, transfer its balance + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: asset, + msg: to_binary(&TokenMsg::TransferFrom { + owner: env.message.sender.clone(), + recipient: env.contract.address.clone(), + amount, + })?, + send: vec![], + })); + asset_address = extend_address_to_32(&asset_canonical); + asset_chain = CHAIN_ID; + } + }; + + let transfer_info = TransferInfo { + token_chain: asset_chain, + token_address: asset_address.clone(), + amount: (0, amount.u128()), + recipient_chain, + recipient: recipient.clone(), + }; + + 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(&WormholeHandleMsg::PostMessage { + message: Binary::from(token_bridge_message.serialize()), + nonce, + })?, + // forward coins sent to this message + send: env.message.sent_funds.clone(), + })); + + Ok(HandleResponse { + messages, + log: vec![ + log("transfer.token_chain", asset_chain), + log("transfer.token", hex::encode(asset_address)), + log( + "transfer.sender", + hex::encode(extend_address_to_32( + &deps.api.canonical_address(&env.message.sender)?, + )), + ), + log("transfer.recipient_chain", recipient_chain), + log("transfer.recipient", hex::encode(recipient)), + log("transfer.amount", amount), + log("transfer.nonce", nonce), + log("transfer.block_time", env.block.time), + ], + data: None, + }) +} + +pub fn query( + deps: &Extern, + msg: QueryMsg, +) -> StdResult { + match msg { + QueryMsg::WrappedRegistry { chain, address } => { + to_binary(&query_wrapped_registry(deps, chain, address.as_slice())?) + } + } +} + +pub fn query_wrapped_registry( + deps: &Extern, + chain: u16, + address: &[u8], +) -> StdResult { + 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 build_asset_id(chain: u16, address: &[u8]) -> Vec { + let mut asset_id: Vec = 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() +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::{to_binary, Binary, StdResult}; + + #[test] + fn test_me() -> StdResult<()> { + let x = vec![ + 1u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 96u8, 180u8, 94u8, 195u8, 0u8, 0u8, + 0u8, 1u8, 0u8, 3u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 38u8, + 229u8, 4u8, 215u8, 149u8, 163u8, 42u8, 54u8, 156u8, 236u8, 173u8, 168u8, 72u8, 220u8, + 100u8, 90u8, 154u8, 159u8, 160u8, 215u8, 0u8, 91u8, 48u8, 44u8, 48u8, 44u8, 51u8, 44u8, + 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, + 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 53u8, 55u8, 44u8, 52u8, + 54u8, 44u8, 50u8, 53u8, 53u8, 44u8, 53u8, 48u8, 44u8, 50u8, 52u8, 51u8, 44u8, 49u8, + 48u8, 54u8, 44u8, 49u8, 50u8, 50u8, 44u8, 49u8, 49u8, 48u8, 44u8, 49u8, 50u8, 53u8, + 44u8, 56u8, 56u8, 44u8, 55u8, 51u8, 44u8, 49u8, 56u8, 57u8, 44u8, 50u8, 48u8, 55u8, + 44u8, 49u8, 48u8, 52u8, 44u8, 56u8, 51u8, 44u8, 49u8, 49u8, 57u8, 44u8, 49u8, 50u8, + 55u8, 44u8, 49u8, 57u8, 50u8, 44u8, 49u8, 52u8, 55u8, 44u8, 56u8, 57u8, 44u8, 48u8, + 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, + 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, + 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, + 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, + 44u8, 48u8, 44u8, 51u8, 44u8, 50u8, 51u8, 50u8, 44u8, 48u8, 44u8, 51u8, 44u8, 48u8, + 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, + 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 48u8, 44u8, 53u8, 51u8, 44u8, 49u8, 49u8, + 54u8, 44u8, 52u8, 56u8, 44u8, 49u8, 49u8, 54u8, 44u8, 49u8, 52u8, 57u8, 44u8, 49u8, + 48u8, 56u8, 44u8, 49u8, 49u8, 51u8, 44u8, 56u8, 44u8, 48u8, 44u8, 50u8, 51u8, 50u8, + 44u8, 52u8, 57u8, 44u8, 49u8, 53u8, 50u8, 44u8, 49u8, 44u8, 50u8, 56u8, 44u8, 50u8, + 48u8, 51u8, 44u8, 50u8, 49u8, 50u8, 44u8, 50u8, 50u8, 49u8, 44u8, 50u8, 52u8, 49u8, + 44u8, 56u8, 53u8, 44u8, 49u8, 48u8, 57u8, 93u8, + ]; + let b = Binary::from(x.clone()); + let y = b.as_slice().to_vec(); + assert_eq!(x, y); + Ok(()) + } +} diff --git a/terra/contracts/token-bridge/src/error.rs b/terra/contracts/token-bridge/src/error.rs new file mode 100644 index 00000000..2975eafc --- /dev/null +++ b/terra/contracts/token-bridge/src/error.rs @@ -0,0 +1,114 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + /// Invalid VAA version + #[error("InvalidVersion")] + InvalidVersion, + + /// Guardian set with this index does not exist + #[error("InvalidGuardianSetIndex")] + InvalidGuardianSetIndex, + + /// Guardian set expiration date is zero or in the past + #[error("GuardianSetExpired")] + GuardianSetExpired, + + /// Not enough signers on the VAA + #[error("NoQuorum")] + NoQuorum, + + /// Wrong guardian index order, order must be ascending + #[error("WrongGuardianIndexOrder")] + WrongGuardianIndexOrder, + + /// Some problem with signature decoding from bytes + #[error("CannotDecodeSignature")] + CannotDecodeSignature, + + /// Some problem with public key recovery from the signature + #[error("CannotRecoverKey")] + CannotRecoverKey, + + /// Recovered pubkey from signature does not match guardian address + #[error("GuardianSignatureError")] + GuardianSignatureError, + + /// VAA action code not recognized + #[error("InvalidVAAAction")] + InvalidVAAAction, + + /// VAA guardian set is not current + #[error("NotCurrentGuardianSet")] + NotCurrentGuardianSet, + + /// Only 128-bit amounts are supported + #[error("AmountTooHigh")] + AmountTooHigh, + + /// Amount should be higher than zero + #[error("AmountTooLow")] + AmountTooLow, + + /// Source and target chain ids must be different + #[error("SameSourceAndTarget")] + SameSourceAndTarget, + + /// Target chain id must be the same as the current CHAIN_ID + #[error("WrongTargetChain")] + WrongTargetChain, + + /// Wrapped asset init hook sent twice for the same asset id + #[error("AssetAlreadyRegistered")] + AssetAlreadyRegistered, + + /// Guardian set must increase in steps of 1 + #[error("GuardianSetIndexIncreaseError")] + GuardianSetIndexIncreaseError, + + /// VAA was already executed + #[error("VaaAlreadyExecuted")] + VaaAlreadyExecuted, + + /// Message sender not permitted to execute this operation + #[error("PermissionDenied")] + PermissionDenied, + + /// Could not decode target address from canonical to human-readable form + #[error("WrongTargetAddressFormat")] + WrongTargetAddressFormat, + + /// More signatures than active guardians found + #[error("TooManySignatures")] + TooManySignatures, + + /// Wrapped asset not found in the registry + #[error("AssetNotFound")] + AssetNotFound, + + /// Generic error when there is a problem with VAA structure + #[error("InvalidVAA")] + InvalidVAA, + + /// Thrown when fee is enabled for the action, but was not sent with the transaction + #[error("FeeTooLow")] + FeeTooLow, + + /// Registering asset outside of the wormhole + #[error("RegistrationForbidden")] + RegistrationForbidden, +} + +impl ContractError { + pub fn std(&self) -> StdError { + StdError::GenericErr { + msg: format!("{}", self), + backtrace: None, + } + } + + pub fn std_err(&self) -> Result { + Err(self.std()) + } +} diff --git a/terra/contracts/token-bridge/src/lib.rs b/terra/contracts/token-bridge/src/lib.rs new file mode 100644 index 00000000..b40a7846 --- /dev/null +++ b/terra/contracts/token-bridge/src/lib.rs @@ -0,0 +1,14 @@ +#[cfg(test)] +#[macro_use] +extern crate lazy_static; + +mod byte_utils; +pub mod contract; +mod error; +pub mod msg; +pub mod state; + +pub use crate::error::ContractError; + +#[cfg(all(target_arch = "wasm32", not(feature = "library")))] +cosmwasm_std::create_entry_points!(contract); diff --git a/terra/contracts/token-bridge/src/msg.rs b/terra/contracts/token-bridge/src/msg.rs new file mode 100644 index 00000000..f81773c3 --- /dev/null +++ b/terra/contracts/token-bridge/src/msg.rs @@ -0,0 +1,60 @@ +use cosmwasm_std::{Binary, HumanAddr, Uint128}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InitMsg { + pub owner: HumanAddr, + pub wormhole_contract: HumanAddr, + pub wrapped_asset_code_id: u64, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum HandleMsg { + + + RegisterAssetHook { + asset_id: Binary, + }, + + InitiateTransfer { + asset: HumanAddr, + amount: Uint128, + recipient_chain: u16, + recipient: Binary, + nonce: u32, + }, + + SubmitVaa { + data: Binary, + }, + + RegisterChain { + chain_id: u16, + chain_address: Binary, + }, + + CreateAssetMeta { + asset_address: HumanAddr, + nonce: u32, + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + WrappedRegistry { chain: u16, address: Binary }, +} + +#[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 }, +} diff --git a/terra/contracts/token-bridge/src/state.rs b/terra/contracts/token-bridge/src/state.rs new file mode 100644 index 00000000..36765836 --- /dev/null +++ b/terra/contracts/token-bridge/src/state.rs @@ -0,0 +1,186 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{HumanAddr, StdResult, Storage}; +use cosmwasm_storage::{ + bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton, + Singleton, +}; + +use crate::byte_utils::ByteUtils; + + +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: &[u8] = b"bridge_contracts"; + +// Guardian set information +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct ConfigInfo { + // Current active guardian set + pub owner: HumanAddr, + pub wormhole_contract: HumanAddr, + pub wrapped_asset_code_id: u64, +} + +pub fn config(storage: &mut S) -> Singleton { + singleton(storage, CONFIG_KEY) +} + +pub fn config_read(storage: &S) -> ReadonlySingleton { + singleton_read(storage, CONFIG_KEY) +} + +pub fn bridge_contracts(storage: &mut S) -> Bucket> { + bucket(BRIDGE_CONTRACTS, storage) +} + +pub fn bridge_contracts_read(storage: &S) -> ReadonlyBucket> { + bucket_read(BRIDGE_CONTRACTS, storage) +} + +pub fn wrapped_asset(storage: &mut S) -> Bucket { + bucket(WRAPPED_ASSET_KEY, storage) +} + +pub fn wrapped_asset_read(storage: &S) -> ReadonlyBucket { + bucket_read(WRAPPED_ASSET_KEY, storage) +} + +pub fn wrapped_asset_address(storage: &mut S) -> Bucket> { + bucket(WRAPPED_ASSET_ADDRESS_KEY, storage) +} + +pub fn wrapped_asset_address_read(storage: &S) -> ReadonlyBucket> { + bucket_read(WRAPPED_ASSET_ADDRESS_KEY, storage) +} + + + + +pub struct Action; + +impl Action { + pub const TRANSFER: u8 = 0; + pub const ATTEST_META: u8 = 1; +} + +// 0 u8 action +// 1 [u8] payload + +pub struct TokenBridgeMessage { + pub action: u8, + pub payload: Vec, +} + +impl TokenBridgeMessage { + pub fn deserialize(data: &Vec) -> StdResult { + 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 { + [self.action.to_be_bytes().to_vec(), self.payload.clone()].concat() + } +} + +// 0 u16 token_chain +// 2 [u8; 32] token_address +// 34 u256 amount +// 66 u16 recipient_chain +// 68 [u8; 32] recipient + +pub struct TransferInfo { + pub token_chain: u16, + pub token_address: Vec, + pub amount: (u128, u128), + pub recipient_chain: u16, + pub recipient: Vec, +} + +impl TransferInfo { + pub fn deserialize(data: &Vec) -> StdResult { + let data = data.as_slice(); + let token_chain = data.get_u16(0); + let token_address = data.get_bytes32(2).to_vec(); + let amount = data.get_u256(34); + let recipient_chain = data.get_u16(66); + let recipient = data.get_bytes32(68).to_vec(); + + Ok(TransferInfo { + token_chain, + token_address, + amount, + recipient_chain, + recipient, + }) + } + pub fn serialize(&self) -> Vec { + [ + self.token_chain.to_be_bytes().to_vec(), + self.token_address.clone(), + self.amount.0.to_be_bytes().to_vec(), + self.amount.1.to_be_bytes().to_vec(), + self.recipient_chain.to_be_bytes().to_vec(), + self.recipient.to_vec(), + ] + .concat() + } +} + +//PayloadID uint8 = 2 +// // Address of the token. Left-zero-padded if shorter than 32 bytes +// TokenAddress [32]uint8 +// // Chain ID of the token +// TokenChain uint16 +// // Number of decimals of the token (big-endian uint256) +// Decimals [32]uint8 +// // Symbol of the token (UTF-8) +// Symbol [32]uint8 +// // Name of the token (UTF-8) +// Name [32]uint8 + +pub struct AssetMeta { + pub token_chain: u16, + pub token_address: Vec, + pub decimals: u8, + pub symbol: Vec, + pub name: Vec, +} + +impl AssetMeta { + pub fn deserialize(data: &Vec) -> StdResult { + let data = data.as_slice(); + let token_chain = data.get_u16(0); + let token_address = data.get_bytes32(2).to_vec(); + let decimals = data.get_u8(34); + let symbol = data.get_bytes32(35).to_vec(); + let name = data.get_bytes32(67).to_vec(); + + Ok(AssetMeta { + token_chain, + token_address, + decimals, + symbol, + name, + }) + } + + pub fn serialize(&self) -> Vec { + [ + self.token_chain.to_be_bytes().to_vec(), + self.token_address.clone(), + self.decimals.to_be_bytes().to_vec(), + self.symbol.clone(), + self.name.clone(), + ] + .concat() + } +} diff --git a/terra/contracts/token-bridge/tests/integration.rs b/terra/contracts/token-bridge/tests/integration.rs new file mode 100644 index 00000000..cdcd6f0d --- /dev/null +++ b/terra/contracts/token-bridge/tests/integration.rs @@ -0,0 +1,90 @@ +static WASM: &[u8] = include_bytes!("../../../target/wasm32-unknown-unknown/release/wormhole.wasm"); + +use cosmwasm_std::{from_slice, Env, HumanAddr, InitResponse, Coin}; +use cosmwasm_storage::to_length_prefixed; +use cosmwasm_vm::testing::{init, mock_env, mock_instance, MockApi, MockQuerier, MockStorage}; +use cosmwasm_vm::{Api, Instance, Storage}; + +use wormhole::msg::InitMsg; +use wormhole::state::{ConfigInfo, GuardianAddress, GuardianSetInfo, CONFIG_KEY}; + +use hex; + +enum TestAddress { + INITIALIZER, +} + +impl TestAddress { + fn value(&self) -> HumanAddr { + match self { + TestAddress::INITIALIZER => HumanAddr::from("initializer"), + } + } +} + +fn mock_env_height(signer: &HumanAddr, height: u64, time: u64) -> Env { + let mut env = mock_env(signer, &[]); + env.block.height = height; + env.block.time = time; + env +} + +fn get_config_info(storage: &S) -> ConfigInfo { + let key = to_length_prefixed(CONFIG_KEY); + let data = storage + .get(&key) + .0 + .expect("error getting data") + .expect("data should exist"); + from_slice(&data).expect("invalid data") +} + +fn do_init( + height: u64, + guardians: &Vec, +) -> Instance { + let mut deps = mock_instance(WASM, &[]); + let init_msg = InitMsg { + initial_guardian_set: GuardianSetInfo { + addresses: guardians.clone(), + expiration_time: 100, + }, + guardian_set_expirity: 50, + wrapped_asset_code_id: 999, + }; + let env = mock_env_height(&TestAddress::INITIALIZER.value(), height, 0); + let owner = deps + .api + .canonical_address(&TestAddress::INITIALIZER.value()) + .0 + .unwrap(); + let res: InitResponse = init(&mut deps, env, init_msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // query the store directly + deps.with_storage(|storage| { + assert_eq!( + get_config_info(storage), + ConfigInfo { + guardian_set_index: 0, + guardian_set_expirity: 50, + wrapped_asset_code_id: 999, + owner, + fee: Coin::new(10000, "uluna"), + } + ); + Ok(()) + }) + .unwrap(); + deps +} + +#[test] +fn init_works() { + let guardians = vec![GuardianAddress::from(GuardianAddress { + bytes: hex::decode("beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe") + .expect("Decoding failed") + .into(), + })]; + let _deps = do_init(111, &guardians); +} diff --git a/terra/contracts/wormhole/src/byte_utils.rs b/terra/contracts/wormhole/src/byte_utils.rs index 850dbccf..7e1c0f9b 100644 --- a/terra/contracts/wormhole/src/byte_utils.rs +++ b/terra/contracts/wormhole/src/byte_utils.rs @@ -1,8 +1,11 @@ -use cosmwasm_std::CanonicalAddr; +use cosmwasm_std::{CanonicalAddr, StdError, StdResult}; pub trait ByteUtils { fn get_u8(&self, index: usize) -> u8; + fn get_u16(&self, index: usize) -> u16; fn get_u32(&self, index: usize) -> u32; + fn get_u64(&self, index: usize) -> u64; + fn get_u128_be(&self, index: usize) -> u128; /// High 128 then low 128 fn get_u256(&self, index: usize) -> (u128, u128); @@ -14,11 +17,21 @@ impl ByteUtils for &[u8] { fn get_u8(&self, index: usize) -> u8 { self[index] } + fn get_u16(&self, index: usize) -> u16 { + let mut bytes: [u8; 16 / 8] = [0; 16 / 8]; + bytes.copy_from_slice(&self[index..index + 2]); + u16::from_be_bytes(bytes) + } fn get_u32(&self, index: usize) -> u32 { let mut bytes: [u8; 32 / 8] = [0; 32 / 8]; bytes.copy_from_slice(&self[index..index + 4]); u32::from_be_bytes(bytes) } + fn get_u64(&self, index: usize) -> u64 { + let mut bytes: [u8; 64 / 8] = [0; 64 / 8]; + bytes.copy_from_slice(&self[index..index + 8]); + u64::from_be_bytes(bytes) + } fn get_u128_be(&self, index: usize) -> u128 { let mut bytes: [u8; 128 / 8] = [0; 128 / 8]; bytes.copy_from_slice(&self[index..index + 128 / 8]); @@ -41,3 +54,14 @@ pub fn extend_address_to_32(addr: &CanonicalAddr) -> Vec { result.extend(addr.as_slice()); result } + +pub fn extend_string_to_32(s: &String) -> StdResult> { + let bytes = s.as_bytes(); + if bytes.len() > 32 { + return Err(StdError::generic_err("string more than 32 ")) + } + + let mut result = vec![0; 32 - bytes.len()]; + result.extend(bytes); + Ok(result) +} \ No newline at end of file diff --git a/terra/contracts/wormhole/src/contract.rs b/terra/contracts/wormhole/src/contract.rs index 14f4d8f3..f5c25c88 100644 --- a/terra/contracts/wormhole/src/contract.rs +++ b/terra/contracts/wormhole/src/contract.rs @@ -1,29 +1,19 @@ -use crate::msg::WrappedRegistryResponse; use cosmwasm_std::{ - log, to_binary, Api, Binary, CanonicalAddr, CosmosMsg, BankMsg, Env, Extern, HandleResponse, HumanAddr, - InitResponse, Querier, QueryRequest, StdResult, Storage, Uint128, WasmMsg, WasmQuery, Coin, has_coins, + has_coins, log, to_binary, Api, BankMsg, Binary, Coin, CosmosMsg, Env, Extern, HandleResponse, + HumanAddr, InitResponse, Querier, StdError, StdResult, Storage, }; use crate::byte_utils::extend_address_to_32; use crate::byte_utils::ByteUtils; use crate::error::ContractError; -use crate::msg::{GuardianSetInfoResponse, HandleMsg, InitMsg, QueryMsg, GetStateResponse}; +use crate::msg::{ + GetAddressHexResponse, GetStateResponse, GuardianSetInfoResponse, HandleMsg, InitMsg, QueryMsg, +}; use crate::state::{ config, config_read, guardian_set_get, guardian_set_set, vaa_archive_add, vaa_archive_check, - wrapped_asset, wrapped_asset_address, wrapped_asset_address_read, wrapped_asset_read, - ConfigInfo, GuardianAddress, GuardianSetInfo, ParsedVAA, + ConfigInfo, GuardianAddress, GuardianSetInfo, ParsedVAA, WormholeGovernance, }; -use cw20_base::msg::HandleMsg as TokenMsg; -use cw20_base::msg::QueryMsg as TokenQuery; - -use cw20::TokenInfoResponse; - -use cw20_wrapped::msg::HandleMsg as WrappedMsg; -use cw20_wrapped::msg::InitMsg as WrappedInit; -use cw20_wrapped::msg::QueryMsg as WrappedQuery; -use cw20_wrapped::msg::{InitHook, InitMint, WrappedAssetInfoResponse}; - use k256::ecdsa::recoverable::Id as RecoverableId; use k256::ecdsa::recoverable::Signature as RecoverableSignature; use k256::ecdsa::Signature; @@ -36,14 +26,12 @@ use generic_array::GenericArray; use std::convert::TryFrom; // Chain ID of Terra -const CHAIN_ID: u8 = 3; +const CHAIN_ID: u16 = 3; // Lock assets fee amount and denomination const FEE_AMOUNT: u128 = 10000; const FEE_DENOMINATION: &str = "uluna"; -const WRAPPED_ASSET_UPDATING: &str = "updating"; - pub fn init( deps: &mut Extern, env: Env, @@ -53,9 +41,8 @@ pub fn init( let state = ConfigInfo { guardian_set_index: 0, guardian_set_expirity: msg.guardian_set_expirity, - wrapped_asset_code_id: msg.wrapped_asset_code_id, owner: deps.api.canonical_address(&env.message.sender)?, - fee: Coin::new(FEE_AMOUNT, FEE_DENOMINATION), // 0.01 Luna (or 10000 uluna) fee by default + fee: Coin::new(FEE_AMOUNT, FEE_DENOMINATION), // 0.01 Luna (or 10000 uluna) fee by default }; config(&mut deps.storage).save(&state)?; @@ -75,26 +62,14 @@ pub fn handle( msg: HandleMsg, ) -> StdResult { match msg { - HandleMsg::SubmitVAA { vaa } => handle_submit_vaa(deps, env, &vaa.as_slice()), - HandleMsg::RegisterAssetHook { asset_id } => { - handle_register_asset(deps, env, &asset_id.as_slice()) + HandleMsg::PostMessage { message, nonce } => { + handle_post_message(deps, env, &message.as_slice(), nonce) } - HandleMsg::LockAssets { - asset, - recipient, - amount, - target_chain, - nonce, - } => handle_lock_assets( - deps, - env, - asset, - amount, - recipient.as_slice(), - target_chain, - nonce, - ), - HandleMsg::TransferFee { amount, recipient } => handle_transfer_fee(deps, env, amount, recipient), + // HandleMsg::SubmitVAA { vaa } => handle_submit_vaa(deps, env, &vaa.as_slice()), + HandleMsg::TransferFee { amount, recipient } => { + handle_transfer_fee(deps, env, amount, recipient) + } + HandleMsg::SubmitVAA { vaa } => handle_submit_vaa(deps, env, vaa.as_slice()), } } @@ -105,17 +80,24 @@ fn handle_submit_vaa( data: &[u8], ) -> StdResult { let state = config_read(&deps.storage).load()?; - - let vaa = parse_and_verify_vaa(&deps.storage, data, env.block.time)?; - let result = match vaa.action { - 0x01 => { + let vaa = parse_and_verify_vaa(&deps.storage, data, env.block.time)?; + if vaa.emitter_chain != 0u16 { + // chain 0 is the wormhole chain ? + return Err(StdError::generic_err( + "governance actions may only come from chain 0", + )); + } + + let gov = WormholeGovernance::deserialize(&vaa.payload)?; + + let result = match gov.action { + 0u8 => { if vaa.guardian_set_index != state.guardian_set_index { return ContractError::NotCurrentGuardianSet.std_err(); } - vaa_update_guardian_set(deps, env, vaa.payload.as_slice()) + vaa_update_guardian_set(deps, env, gov.payload.as_slice()) } - 0x10 => vaa_transfer(deps, env, vaa.payload.as_slice()), _ => ContractError::InvalidVAAAction.std_err(), }; @@ -152,7 +134,7 @@ fn parse_and_verify_vaa( if guardian_set.expiration_time != 0 && guardian_set.expiration_time < block_time { return ContractError::GuardianSetExpired.std_err(); } - if vaa.len_signers < guardian_set.quorum() { + if (vaa.len_signers as usize) < guardian_set.quorum() { return ContractError::NoQuorum.std_err(); } @@ -197,37 +179,6 @@ fn parse_and_verify_vaa( Ok(vaa) } -/// Handle wrapped asset registration messages -fn handle_register_asset( - deps: &mut Extern, - env: Env, - asset_id: &[u8], -) -> StdResult { - let mut bucket = wrapped_asset(&mut deps.storage); - let result = bucket.load(asset_id); - let result = result.map_err(|_| ContractError::RegistrationForbidden.std())?; - if result != HumanAddr::from(WRAPPED_ASSET_UPDATING) { - return ContractError::AssetAlreadyRegistered.std_err(); - } - - bucket.save(asset_id, &env.message.sender)?; - - let contract_address: CanonicalAddr = - deps.api.canonical_address(&env.message.sender)?; - wrapped_asset_address(&mut deps.storage) - .save(contract_address.as_slice(), &asset_id.to_vec())?; - - Ok(HandleResponse { - messages: vec![], - log: vec![ - log("action", "register_asset"), - log("asset_id", format!("{:?}", asset_id)), - log("contract_addr", env.message.sender), - ], - data: None, - }) -} - fn vaa_update_guardian_set( deps: &mut Extern, env: Env, @@ -298,227 +249,32 @@ fn vaa_update_guardian_set( }) } -fn vaa_transfer( +fn handle_post_message( deps: &mut Extern, env: Env, - data: &[u8], -) -> StdResult { - /* Payload format: - 0 uint32 nonce - 4 uint8 source_chain - 5 uint8 target_chain - 6 [32]uint8 source_address - 38 [32]uint8 target_address - 70 uint8 token_chain - 71 [32]uint8 token_address - 103 uint8 decimals - 104 uint256 amount */ - - const SOURCE_CHAIN_POS: usize = 4; - const TARGET_CHAIN_POS: usize = 5; - const TARGET_ADDRESS_POS: usize = 38; - const TOKEN_CHAIN_POS: usize = 70; - const TOKEN_ADDRESS_POS: usize = 71; - const DECIMALS_POS: usize = 103; - const AMOUNT_POS: usize = 104; - const PAYLOAD_LEN: usize = 136; - - if PAYLOAD_LEN > data.len() { - return ContractError::InvalidVAA.std_err(); - } - - let source_chain = data.get_u8(SOURCE_CHAIN_POS); - let target_chain = data.get_u8(TARGET_CHAIN_POS); - - let target_address = data.get_address(TARGET_ADDRESS_POS); - - let token_chain = data.get_u8(TOKEN_CHAIN_POS); - let (not_supported_amount, amount) = data.get_u256(AMOUNT_POS); - - // Check high 128 bit of amount value to be empty - if not_supported_amount != 0 { - return ContractError::AmountTooHigh.std_err(); - } - - // Check if source and target chains are different - if source_chain == target_chain { - return ContractError::SameSourceAndTarget.std_err(); - } - - // Check if transfer is incoming - if target_chain != CHAIN_ID { - return ContractError::WrongTargetChain.std_err(); - } - - if token_chain != CHAIN_ID { - let asset_address = data.get_bytes32(TOKEN_ADDRESS_POS); - let asset_id = build_asset_id(token_chain, asset_address); - - let mut messages: Vec = vec![]; - - // Check if this asset is already deployed - let contract_addr = wrapped_asset_read(&deps.storage).load(&asset_id).ok(); - let contract_addr = contract_addr.filter(|addr| addr != &HumanAddr::from(WRAPPED_ASSET_UPDATING)); - - if let Some(contract_addr) = contract_addr { - // Asset already deployed, just mint - messages.push(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr, - msg: to_binary(&WrappedMsg::Mint { - recipient: deps - .api - .human_address(&target_address) - .or_else(|_| ContractError::WrongTargetAddressFormat.std_err())?, - amount: Uint128::from(amount), - })?, - send: vec![], - })); - } else { - // Asset is not deployed yet, deploy and mint - wrapped_asset(&mut deps.storage).save(&asset_id, &HumanAddr::from(WRAPPED_ASSET_UPDATING))?; - - let state = config_read(&deps.storage).load()?; - messages.push(CosmosMsg::Wasm(WasmMsg::Instantiate { - code_id: state.wrapped_asset_code_id, - msg: to_binary(&WrappedInit { - asset_chain: token_chain, - asset_address: asset_address.to_vec().into(), - decimals: data.get_u8(DECIMALS_POS), - mint: Some(InitMint { - recipient: deps - .api - .human_address(&target_address) - .or_else(|_| ContractError::WrongTargetAddressFormat.std_err())?, - amount: Uint128::from(amount), - }), - init_hook: Some(InitHook { - contract_addr: env.contract.address, - msg: to_binary(&HandleMsg::RegisterAssetHook { - asset_id: asset_id.to_vec().into(), - })?, - }), - })?, - send: vec![], - label: None, - })); - } - - Ok(HandleResponse { - messages, - log: vec![], - data: None, - }) - } else { - let token_address = data.get_address(TOKEN_ADDRESS_POS); - - Ok(HandleResponse { - messages: vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: deps.api.human_address(&token_address)?, - msg: to_binary(&TokenMsg::Transfer { - recipient: deps.api.human_address(&target_address)?, - amount: Uint128::from(amount), - })?, - send: vec![], - })], - log: vec![], - data: None, - }) - } -} - -fn handle_lock_assets( - deps: &mut Extern, - env: Env, - asset: HumanAddr, - amount: Uint128, - recipient: &[u8], - target_chain: u8, + message: &[u8], nonce: u32, ) -> StdResult { - if target_chain == CHAIN_ID { - return ContractError::SameSourceAndTarget.std_err(); - } - - if amount.is_zero() { - return ContractError::AmountTooLow.std_err(); - } - let state = config_read(&deps.storage).load()?; - + // Check fee if !has_coins(env.message.sent_funds.as_ref(), &state.fee) { return ContractError::FeeTooLow.std_err(); } - let asset_chain: u8; - let asset_address: Vec; - - // Query asset details - let request = QueryRequest::<()>::Wasm(WasmQuery::Smart { - contract_addr: asset.clone(), - msg: to_binary(&TokenQuery::TokenInfo {})?, - }); - let token_info: TokenInfoResponse = deps.querier.custom_query(&request)?; - - let decimals: u8 = token_info.decimals; - - let asset_canonical: CanonicalAddr = deps.api.canonical_address(&asset)?; - - let mut messages: Vec = vec![]; - - match wrapped_asset_address_read(&deps.storage).load(asset_canonical.as_slice()) { - Ok(_) => { - // This is a deployed wrapped asset, burn it - messages.push(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: asset.clone(), - msg: to_binary(&WrappedMsg::Burn { - account: env.message.sender.clone(), - amount, - })?, - send: vec![], - })); - let request = QueryRequest::<()>::Wasm(WasmQuery::Smart { - contract_addr: asset, - msg: to_binary(&WrappedQuery::WrappedAssetInfo {})?, - }); - let wrapped_token_info: WrappedAssetInfoResponse = - deps.querier.custom_query(&request)?; - asset_chain = wrapped_token_info.asset_chain; - asset_address = wrapped_token_info.asset_address.as_slice().to_vec(); - } - Err(_) => { - // This is a regular asset, transfer its balance - messages.push(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: asset, - msg: to_binary(&TokenMsg::TransferFrom { - owner: env.message.sender.clone(), - recipient: env.contract.address.clone(), - amount, - })?, - send: vec![], - })); - asset_address = extend_address_to_32(&asset_canonical); - asset_chain = CHAIN_ID; - } - }; - Ok(HandleResponse { - messages, + messages: vec![], log: vec![ - log("locked.target_chain", target_chain), - log("locked.token_chain", asset_chain), - log("locked.token_decimals", decimals), - log("locked.token", hex::encode(asset_address)), + log("message.message", hex::encode(message)), log( - "locked.sender", + "message.sender", hex::encode(extend_address_to_32( &deps.api.canonical_address(&env.message.sender)?, )), ), - log("locked.recipient", hex::encode(recipient)), - log("locked.amount", amount), - log("locked.nonce", nonce), - log("locked.block_time", env.block.time), + log("message.chain_id", CHAIN_ID), + log("message.nonce", nonce), + log("message.block_time", env.block.time), ], data: None, }) @@ -537,7 +293,7 @@ pub fn handle_transfer_fee( } Ok(HandleResponse { - messages: vec![CosmosMsg::Bank(BankMsg::Send{ + messages: vec![CosmosMsg::Bank(BankMsg::Send { from_address: env.contract.address, to_address: recipient, amount: vec![amount], @@ -553,13 +309,13 @@ pub fn query( ) -> StdResult { match msg { QueryMsg::GuardianSetInfo {} => to_binary(&query_guardian_set_info(deps)?), - QueryMsg::WrappedRegistry { chain, address } => { - to_binary(&query_wrapped_registry(deps, chain, address.as_slice())?) - } - QueryMsg::VerifyVAA { vaa, block_time } => { - to_binary(&query_parse_and_verify_vaa(deps, &vaa.as_slice(), block_time)?) - }, + QueryMsg::VerifyVAA { vaa, block_time } => to_binary(&query_parse_and_verify_vaa( + deps, + &vaa.as_slice(), + block_time, + )?), QueryMsg::GetState {} => to_binary(&query_state(deps)?), + QueryMsg::QueryAddressHex { address } => to_binary(&query_address_hex(deps, &address)?), } } @@ -575,19 +331,6 @@ pub fn query_guardian_set_info( Ok(res) } -pub fn query_wrapped_registry( - deps: &Extern, - chain: u8, - address: &[u8], -) -> StdResult { - 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(), - } -} - pub fn query_parse_and_verify_vaa( deps: &Extern, data: &[u8], @@ -596,13 +339,21 @@ pub fn query_parse_and_verify_vaa( parse_and_verify_vaa(&deps.storage, data, block_time) } +// returns the hex of the 32 byte address we use for some address on this chain +pub fn query_address_hex( + deps: &Extern, + address: &HumanAddr, +) -> StdResult { + Ok(GetAddressHexResponse { + hex: hex::encode(extend_address_to_32(&deps.api.canonical_address(&address)?)), + }) +} + pub fn query_state( deps: &Extern, ) -> StdResult { let state = config_read(&deps.storage).load()?; - let res = GetStateResponse { - fee: state.fee, - }; + let res = GetStateResponse { fee: state.fee }; Ok(res) } @@ -631,16 +382,6 @@ fn keys_equal(a: &VerifyKey, b: &GuardianAddress) -> bool { true } -fn build_asset_id(chain: u8, address: &[u8]) -> Vec { - let mut asset_id: Vec = vec![]; - asset_id.push(chain); - asset_id.extend_from_slice(address); - - let mut hasher = Keccak256::new(); - hasher.update(asset_id); - hasher.finalize().to_vec() -} - #[cfg(test)] mod tests { use super::*; @@ -1009,7 +750,7 @@ mod tests { const LOCK_AMOUNT: u128 = 10000000000; const LOCK_RECIPIENT: &str = "0000000000000000000011223344556677889900"; const LOCK_TARGET: u8 = 1; - const LOCK_WRAPPED_CHAIN: u8 = 2; + const LOCK_WRAPPED_CHAIN: u16 = 2; const LOCK_WRAPPED_ASSET: &str = "112233445566ff"; const LOCKED_DECIMALS: u8 = 11; const ADDRESS_EXTENSION: &str = "000000000000000000000000"; @@ -1096,7 +837,8 @@ mod tests { }; do_init_with_guardians(&mut deps, 1); - let result = submit_msg_with_fee(&mut deps, MSG_LOCK.clone(), Coin::new(10000, "uluna")).unwrap(); + let result = + submit_msg_with_fee(&mut deps, MSG_LOCK.clone(), Coin::new(10000, "uluna")).unwrap(); let expected_logs = vec![ log("locked.target_chain", LOCK_TARGET), @@ -1160,7 +902,7 @@ mod tests { let register_msg = HandleMsg::RegisterAssetHook { asset_id: Binary::from(LOCK_ASSET_ID), }; - + let result = submit_msg_with_sender( &mut deps, register_msg.clone(), @@ -1185,7 +927,9 @@ mod tests { asset_id: Binary::from(LOCK_ASSET_ID), }; - wrapped_asset(&mut deps.storage).save(&LOCK_ASSET_ID, &HumanAddr::from(WRAPPED_ASSET_UPDATING)).unwrap(); + wrapped_asset(&mut deps.storage) + .save(&LOCK_ASSET_ID, &HumanAddr::from(WRAPPED_ASSET_UPDATING)) + .unwrap(); let result = submit_msg_with_sender( &mut deps, @@ -1196,7 +940,8 @@ mod tests { assert!(result.is_ok()); - let result = submit_msg_with_fee(&mut deps, MSG_LOCK.clone(), Coin::new(10000, "uluna")).unwrap(); + let result = + submit_msg_with_fee(&mut deps, MSG_LOCK.clone(), Coin::new(10000, "uluna")).unwrap(); let expected_logs = vec![ log("locked.target_chain", LOCK_TARGET), @@ -1305,7 +1050,13 @@ mod tests { let decoded_vaa: Binary = hex::decode(VAA_VALID_TRANSFER_3_SIGS) .expect("Decoding failed") .into(); - let result = query(&deps, QueryMsg::VerifyVAA { vaa: decoded_vaa, block_time: env.block.time }); + let result = query( + &deps, + QueryMsg::VerifyVAA { + vaa: decoded_vaa, + block_time: env.block.time, + }, + ); assert!(result.is_ok()); } @@ -1328,7 +1079,13 @@ mod tests { let decoded_vaa: Binary = hex::decode(VAA_VALID_TRANSFER_3_SIGS) .expect("Decoding failed") .into(); - let result = query(&deps, QueryMsg::VerifyVAA { vaa: decoded_vaa, block_time: env.block.time }); + let result = query( + &deps, + QueryMsg::VerifyVAA { + vaa: decoded_vaa, + block_time: env.block.time, + }, + ); assert_eq!(result, ContractError::GuardianSignatureError.std_err()); } diff --git a/terra/contracts/wormhole/src/msg.rs b/terra/contracts/wormhole/src/msg.rs index b0413bde..5b19b67c 100644 --- a/terra/contracts/wormhole/src/msg.rs +++ b/terra/contracts/wormhole/src/msg.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Binary, HumanAddr, Uint128, Coin}; +use cosmwasm_std::{Binary, HumanAddr, Coin}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -8,7 +8,6 @@ use crate::state::{GuardianAddress, GuardianSetInfo}; pub struct InitMsg { pub initial_guardian_set: GuardianSetInfo, pub guardian_set_expirity: u64, - pub wrapped_asset_code_id: u64, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -17,15 +16,9 @@ pub enum HandleMsg { SubmitVAA { vaa: Binary, }, - RegisterAssetHook { - asset_id: Binary, - }, - LockAssets { - asset: HumanAddr, - amount: Uint128, - recipient: Binary, - target_chain: u8, - nonce: u32, + PostMessage { + message: Binary, + nonce: u32 }, TransferFee { amount: Coin, @@ -37,9 +30,9 @@ pub enum HandleMsg { #[serde(rename_all = "snake_case")] pub enum QueryMsg { GuardianSetInfo {}, - WrappedRegistry { chain: u8, address: Binary }, VerifyVAA { vaa: Binary, block_time: u64 }, GetState {}, + QueryAddressHex { address: HumanAddr } } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] @@ -60,3 +53,9 @@ pub struct WrappedRegistryResponse { pub struct GetStateResponse { pub fee: Coin, } + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct GetAddressHexResponse { + pub hex: String, +} diff --git a/terra/contracts/wormhole/src/state.rs b/terra/contracts/wormhole/src/state.rs index 28faacbc..dbca314b 100644 --- a/terra/contracts/wormhole/src/state.rs +++ b/terra/contracts/wormhole/src/state.rs @@ -26,9 +26,6 @@ pub struct ConfigInfo { // Period for which a guardian set stays active after it has been replaced pub guardian_set_expirity: u64, - // Code id for wrapped asset contract - pub wrapped_asset_code_id: u64, - // Contract owner address, it can make contract active/inactive pub owner: CanonicalAddr, @@ -39,12 +36,18 @@ pub struct ConfigInfo { // Validator Action Approval(VAA) data #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct ParsedVAA { + pub version: u8, pub guardian_set_index: u32, - pub len_signers: usize, - pub hash: Vec, - pub action: u8, + pub timestamp: u64, + pub nonce: u32, + pub len_signers: u8, + + pub emitter_chain: u16, + pub emitter_address: Vec, pub payload: Vec, + + pub hash: Vec } impl ParsedVAA { @@ -60,9 +63,12 @@ impl ParsedVAA { 1 [65]uint8 signature body: - 0 uint32 unix seconds - 4 uint8 action - 5 [payload_size]uint8 payload */ + 0 uint64 timestamp (unix in seconds) + 8 uint32 nonce + 12 uint16 emitter_chain + 14 [32]uint8 emitter_address + 46 []uint8 payload + */ pub const HEADER_LEN: usize = 6; pub const SIGNATURE_LEN: usize = 66; @@ -70,8 +76,10 @@ impl ParsedVAA { pub const GUARDIAN_SET_INDEX_POS: usize = 1; pub const LEN_SIGNER_POS: usize = 5; - pub const VAA_ACTION_POS: usize = 4; - pub const VAA_PAYLOAD_POS: usize = 5; + pub const VAA_NONCE_POS: usize = 8; + pub const VAA_EMITTER_CHAIN_POS: usize = 12; + pub const VAA_EMITTER_ADDRESS_POS: usize = 14; + pub const VAA_PAYLOAD_POS: usize = 46; // Signature data offsets in the signature block pub const SIG_DATA_POS: usize = 1; @@ -101,16 +109,23 @@ impl ParsedVAA { if body_offset + Self::VAA_PAYLOAD_POS > data.len() { return ContractError::InvalidVAA.std_err(); } - let action = data.get_u8(body_offset + Self::VAA_ACTION_POS); - let payload = &data[body_offset + Self::VAA_PAYLOAD_POS..]; + + let timestamp = data.get_u64(body_offset); + let nonce = data.get_u32(body_offset + Self::VAA_NONCE_POS); + let emitter_chain = data.get_u16(body_offset + Self::VAA_EMITTER_CHAIN_POS); + let emitter_address = data.get_bytes32(body_offset + Self::VAA_EMITTER_ADDRESS_POS).to_vec(); + let payload = data[body_offset + Self::VAA_PAYLOAD_POS..].to_vec(); Ok(ParsedVAA { version, guardian_set_index, - len_signers, + timestamp, + nonce, + len_signers: len_signers as u8, + emitter_chain, + emitter_address, + payload, hash, - action, - payload: payload.to_vec(), }) } } @@ -141,6 +156,10 @@ pub struct GuardianSetInfo { impl GuardianSetInfo { pub fn quorum(&self) -> usize { + // allow quorum of 0 for testing purposes... + if self.addresses.len() == 0 { + return 0; + } ((self.addresses.len() * 10 / 3) * 2) / 10 + 1 } } @@ -165,11 +184,11 @@ pub fn guardian_set_set( index: u32, data: &GuardianSetInfo, ) -> StdResult<()> { - bucket(GUARDIAN_SET_KEY, storage).save(&index.to_le_bytes(), data) + bucket(GUARDIAN_SET_KEY, storage).save(&index.to_be_bytes(), data) } pub fn guardian_set_get(storage: &S, index: u32) -> StdResult { - bucket_read(GUARDIAN_SET_KEY, storage).load(&index.to_le_bytes()) + bucket_read(GUARDIAN_SET_KEY, storage).load(&index.to_be_bytes()) } pub fn vaa_archive_add(storage: &mut S, hash: &[u8]) -> StdResult<()> { @@ -199,6 +218,25 @@ pub fn wrapped_asset_address_read(storage: &S) -> ReadonlyBucket, +} + +impl WormholeGovernance { + pub fn deserialize(data: &Vec) -> StdResult { + let data = data.as_slice(); + let action = data.get_u8(0); + let payload = &data[1..]; + + Ok(WormholeGovernance { + action, + payload: payload.to_vec(), + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -235,4 +273,10 @@ mod tests { assert_eq!(build_guardian_set(25).quorum(), 17); assert_eq!(build_guardian_set(100).quorum(), 67); } + + #[test] + fn test_deserialize() { + let x = vec![1u8,0u8,0u8,0u8,1u8,0u8,0u8,0u8,0u8,0u8,96u8,180u8,80u8,111u8,0u8,0u8,0u8,1u8,0u8,3u8,0u8,0u8,0u8,0u8,0u8,0u8,0u8,0u8,0u8,0u8,0u8,0u8,120u8,73u8,153u8,19u8,90u8,170u8,138u8,60u8,165u8,145u8,68u8,104u8,133u8,47u8,221u8,219u8,221u8,216u8,120u8,157u8,0u8,91u8,48u8,44u8,48u8,44u8,51u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,53u8,54u8,44u8,50u8,51u8,51u8,44u8,49u8,44u8,49u8,49u8,49u8,44u8,49u8,54u8,55u8,44u8,49u8,57u8,48u8,44u8,50u8,48u8,51u8,44u8,49u8,54u8,44u8,49u8,55u8,54u8,44u8,50u8,49u8,56u8,44u8,50u8,53u8,49u8,44u8,49u8,51u8,49u8,44u8,51u8,57u8,44u8,49u8,54u8,44u8,49u8,57u8,53u8,44u8,50u8,50u8,55u8,44u8,49u8,52u8,57u8,44u8,50u8,51u8,54u8,44u8,49u8,57u8,48u8,44u8,50u8,49u8,50u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,51u8,44u8,50u8,51u8,50u8,44u8,48u8,44u8,51u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,48u8,44u8,53u8,51u8,44u8,49u8,49u8,54u8,44u8,52u8,56u8,44u8,49u8,49u8,54u8,44u8,49u8,52u8,57u8,44u8,49u8,48u8,56u8,44u8,49u8,49u8,51u8,44u8,56u8,44u8,48u8,44u8,50u8,51u8,50u8,44u8,52u8,57u8,44u8,49u8,53u8,50u8,44u8,49u8,44u8,50u8,56u8,44u8,50u8,48u8,51u8,44u8,50u8,49u8,50u8,44u8,50u8,50u8,49u8,44u8,50u8,52u8,49u8,44u8,56u8,53u8,44u8,49u8,48u8,57u8,93u8]; + ParsedVAA::deserialize(x.as_slice()); + } } diff --git a/terra/deploy.py b/terra/deploy.py new file mode 100644 index 00000000..95538e15 --- /dev/null +++ b/terra/deploy.py @@ -0,0 +1,269 @@ +from terra_sdk.client.localterra import AsyncLocalTerra +from terra_sdk.core.auth import StdFee +import asyncio +from terra_sdk.core.wasm import ( + MsgStoreCode, + MsgInstantiateContract, + MsgExecuteContract, +) +from terra_sdk.util.contract import get_code_id, get_contract_address, read_file_as_b64 +import os +import base64 +import pprint + + +lt = AsyncLocalTerra(gas_prices={"uusd": "0.15"}) +terra = lt +deployer = lt.wallets["test1"] + +sequence = asyncio.get_event_loop().run_until_complete(deployer.sequence()) + + +async def sign_and_broadcast(*msgs): + + global sequence + try: + tx = await deployer.create_and_sign_tx( + msgs=msgs, fee=StdFee(30000000, "20000000uusd"), sequence=sequence + ) + result = await terra.tx.broadcast(tx) + sequence += 1 + if result.is_tx_error(): + raise Exception(result.raw_log) + return result + except: + sequence = await deployer.sequence() + raise + + +async def store_contract(contract_name): + parent_dir = os.path.dirname(__file__) + contract_bytes = read_file_as_b64(f"{parent_dir}/artifacts/{contract_name}.wasm") + store_code = MsgStoreCode(deployer.key.acc_address, contract_bytes) + + result = await sign_and_broadcast(store_code) + code_id = get_code_id(result) + print(f"Code id for {contract_name} is {code_id}") + return code_id + + +async def store_contracts(): + + parent_dir = os.path.dirname(__file__) + contract_names = [ + i[:-5] for i in os.listdir(f"{parent_dir}/artifacts") if i.endswith(".wasm") + ] + + return { + contract_name: await store_contract(contract_name) + for contract_name in contract_names + } + + +class ContractQuerier: + def __init__(self, address): + self.address = address + + def __getattr__(self, item): + async def result_fxn(**kwargs): + kwargs = convert_contracts_to_addr(kwargs) + return await terra.wasm.contract_query(self.address, {item: kwargs}) + + return result_fxn + + +class Contract: + @staticmethod + async def create(code_id, **kwargs): + kwargs = convert_contracts_to_addr(kwargs) + instantiate = MsgInstantiateContract(deployer.key.acc_address, code_id, kwargs) + result = await sign_and_broadcast(instantiate) + return Contract(get_contract_address(result)) + + def __init__(self, address): + self.address = address + + def __getattr__(self, item): + async def result_fxn(coins=None, **kwargs): + kwargs = convert_contracts_to_addr(kwargs) + execute = MsgExecuteContract( + deployer.key.acc_address, self.address, {item: kwargs}, coins=coins + ) + return await sign_and_broadcast(execute) + + return result_fxn + + @property + def query(self): + return ContractQuerier(self.address) + + +def convert_contracts_to_addr(obj): + if type(obj) == dict: + return {k: convert_contracts_to_addr(v) for k, v in obj.items()} + if type(obj) in {list, tuple}: + return [convert_contracts_to_addr(i) for i in obj] + if type(obj) == Contract: + return obj.address + return obj + + +def to_bytes(n, length, byteorder="big"): + return int(n).to_bytes(length, byteorder=byteorder) + + +def assemble_vaa(emitter_chain, emitter_address, payload): + import time + + # version, guardian set index, len signatures + header = to_bytes(1, 1) + to_bytes(0, 4) + to_bytes(0, 1) + # timestamp, nonce, emitter_chain + body = to_bytes(time.time(), 8) + to_bytes(1, 4) + to_bytes(emitter_chain, 2) + # emitter_address, vaa payload + body += emitter_address + payload + + return header + body + + +async def main(): + code_ids = await store_contracts() + print(code_ids) + wormhole = await Contract.create( + code_id=code_ids["wormhole"], + guardian_set_expirity=10 ** 15, + initial_guardian_set={"addresses": [], "expiration_time": 10 ** 15}, + ) + + token_bridge = await Contract.create( + code_id=code_ids["token_bridge"], + owner=deployer.key.acc_address, + wormhole_contract=wormhole, + wrapped_asset_code_id=int(code_ids["cw20_wrapped"]), + ) + + mock_token = await Contract.create( + code_id=code_ids["cw20_base"], + name="MOCK", + symbol="MCK", + decimals=6, + initial_balances=[{"address": deployer.key.acc_address, "amount": "100000000"}], + mint=None, + ) + + raw_addr = deployer.key.raw_address + recipient = b"\0" * 12 + raw_addr + recipient = base64.b64encode(recipient) + + print( + "Balance before initiate transfer", + await mock_token.query.balance(address=deployer.key.acc_address), + ) + + await mock_token.increase_allowance(spender=token_bridge, amount="1000") + bridge_canonical = bytes.fromhex( + (await wormhole.query.query_address_hex(address=token_bridge))["hex"] + ) + await token_bridge.register_chain( + chain_id=3, chain_address=base64.b64encode(bridge_canonical).decode("utf-8") + ) + + resp = await token_bridge.initiate_transfer( + asset=mock_token, + amount="1000", + recipient_chain=3, + recipient=recipient.decode("utf-8"), + nonce=0, + coins={"uluna": "10000"}, + ) + + print( + "Balance after initiate transfer", + await mock_token.query.balance(address=deployer.key.acc_address), + ) + + logs = resp.logs[0].events_by_type + transfer_data = { + k: v[0] for k, v in logs["from_contract"].items() if k.startswith("message") + } + vaa = assemble_vaa( + transfer_data["message.chain_id"], + bytes.fromhex(transfer_data["message.sender"]), + bytes.fromhex(transfer_data["message.message"]), + ) + + await token_bridge.submit_vaa(data=base64.b64encode(vaa).decode("utf-8")) + + print( + "Balance after complete transfer", + await mock_token.query.balance(address=deployer.key.acc_address), + ) + + # pretend there exists another bridge contract with the same address but on solana + await token_bridge.register_chain( + chain_id=1, chain_address=base64.b64encode(bridge_canonical).decode("utf-8") + ) + + resp = await token_bridge.create_asset_meta( + asset_address=mock_token, + nonce=1, + coins={"uluna": "10000"}, + ) + + logs = resp.logs[0].events_by_type + create_meta_data = { + k: v[0] for k, v in logs["from_contract"].items() if k.startswith("message") + } + message_bytes = bytes.fromhex(create_meta_data["message.message"]) + + # switch the chain of the asset meta to say its from solana + message_bytes = message_bytes[:1] + to_bytes(1, 2) + message_bytes[3:] + vaa = assemble_vaa( + 1, # totally came from solana + bytes.fromhex(create_meta_data["message.sender"]), + message_bytes, + ) + + # attest this metadata and make a wrapped asset from solana + resp = await token_bridge.submit_vaa(data=base64.b64encode(vaa).decode("utf-8")) + + wrapped_token = Contract(get_contract_address(resp)) + + # now send from solana... + message_bytes = bytes.fromhex(transfer_data["message.message"]) + message_bytes = message_bytes[:1] + to_bytes(1, 2) + message_bytes[3:] + + vaa = assemble_vaa( + 1, # totally came from solana + bytes.fromhex(transfer_data["message.sender"]), + message_bytes, + ) + print( + "Balance before completing transfer from solana", + await wrapped_token.query.balance(address=deployer.key.acc_address), + ) + + await token_bridge.submit_vaa(data=base64.b64encode(vaa).decode("utf-8")) + + print( + "Balance after completing transfer from solana", + await wrapped_token.query.balance(address=deployer.key.acc_address), + ) + + await wrapped_token.increase_allowance(spender=token_bridge, amount="1000") + resp = await token_bridge.initiate_transfer( + asset=wrapped_token, + amount="1000", + recipient_chain=1, + recipient=recipient.decode("utf-8"), + nonce=0, + coins={"uluna": "10000"}, + ) + + print( + "Balance after completing transfer to solana", + await wrapped_token.query.balance(address=deployer.key.acc_address), + ) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main())