sui: implement contracts

This commit is contained in:
Josh Siegel 2023-01-12 19:36:42 +00:00 committed by Csongor Kiss
parent 53d554d93b
commit 62eb6aec15
65 changed files with 7445 additions and 3 deletions

2
sui/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
env.sh
sui.log.*

View File

@ -1,4 +1,25 @@
FROM ghcr.io/wormhole-foundation/sui:0.15.0@sha256:ba5740e42ac41306719114ce96d9752a12bbfa500749ede64fa76a250113993b as sui-node
FROM ghcr.io/wormhole-foundation/sui:0.21.1@sha256:59b91529e426b44c152b40ad0e7a6a7aafc8225722b5d7e331056a4d65845015 as sui
RUN dnf -y install make git
COPY README.md cert.pem* /certs/
RUN if [ -e /certs/cert.pem ]; then cp /certs/cert.pem /etc/ssl/certs/ca-certificates.crt; fi
RUN if [ -e /certs/cert.pem ]; then git config --global http.sslCAInfo /certs/cert.pem; fi
WORKDIR /tmp
RUN sui genesis -f
COPY scripts/start_node.sh /tmp
COPY scripts/funder.sh /tmp
COPY scripts/start_node.sh .
COPY scripts/funder.sh .
COPY wormhole/ wormhole
COPY token_bridge/ token_bridge
# COPY examples/ examples
COPY Makefile Makefile
FROM sui AS tests
WORKDIR /tmp
RUN --mount=type=cache,target=/root/.move,id=move_cache \
make test

13
sui/Makefile Normal file
View File

@ -0,0 +1,13 @@
CONTRACT_DIRS := wormhole token_bridge
TARGETS := build test
.PHONY: $(TARGETS)
$(TARGETS):
$(foreach dir,$(CONTRACT_DIRS), make -C $(dir) $@ &&) true
test-docker:
DOCKER_BUILDKIT=1 docker build -f Dockerfile --target tests .
sui_export:
DOCKER_BUILDKIT=1 docker build --progress plain -f Dockerfile.export -t near-export -o type=local,dest=$$HOME/.cargo/bin .

112
sui/NOTES.md Normal file
View File

@ -0,0 +1,112 @@
brew install cmake
rustup install stable-x86_64-apple-darwin
#rustup target add stable-x86_64-apple-darwin
rustup target add x86_64-apple-darwin
=== Building
% ./node_builder.sh
=== Running
% ./start_node.sh
# If you don't remember your newly generated address
% sui client addresses
Showing 1 results.
0x13b3cb89cf3226d3b860294fc75dc6c91f0c5ecf
# Give yourself some money
% scripts/faucet.sh `sui client addresses | tail -1`
# Looking at the prefunded address
% sui client objects --address 0x13b3cb89cf3226d3b860294fc75dc6c91f0c5ecf
=== Boot tilt
# fund our standard account
We don't run a faucet since it doesn't always unlock the client LOCK files. So, instead we just steal a chunk of coins
from the default accounts created when the node was initialized. Once sui is showing as live...
``` sh
% kubectl exec -it sui-0 -c sui-node -- /tmp/funder.sh
```
# getting into the sui k8s node (if you need to crawl around)
kubectl exec -it sui-0 -c sui-node -- /bin/bash
kubectl exec -it guardian-0 -c guardiand -- /bin/bash
# setup the client.yaml
``` sh
% rm -rf $HOME/.sui
% sui keytool import "daughter exclude wheat pudding police weapon giggle taste space whip satoshi occur" ed25519
% sui client
```
point it at http://localhost:9000. The key you create doesn't matter.
# edit $HOME/.sui/sui_config/client.yaml
``` sh
sed -i -e 's/active_address.*/active_address: "0x13b3cb89cf3226d3b860294fc75dc6c91f0c5ecf"/' ~/.sui/sui_config/client.yaml
```
# deploy the contract
``` sh
% scripts/deploy.sh
```
# start the watcher
``` sh
% . env.sh
% python3 tests/ws.py
```
# publish a message (different window)
``` sh
% . env.sh
% scripts/publish_message.sh
```
==
docker run -it -v `pwd`:`pwd` -w `pwd` --net=host ghcr.io/wormhole-foundation/sui:0.16.0 bash
dnf -y install git make
``` sh
% rm -rf $HOME/.sui
% sui keytool import "daughter exclude wheat pudding police weapon giggle taste space whip satoshi occur" secp256k1
% sui client
```
to get a new emitter
kubectl exec -it sui-0 -c sui-node -- /tmp/funder.sh
scripts/deploy.sh
. env.sh
sui client call --function get_new_emitter --module wormhole --package $WORM_PACKAGE --gas-budget 20000 --args \"$WORM_STATE\"
sui client objects
scripts/publish_message.sh 0x165ef7366c4267c6506bcf63d2419556f34f48d6
curl -s -X POST -d '{"jsonrpc":"2.0", "id": 1, "method": "sui_getEvents", "params": [{"MoveEvent": "0xf4179152ab02e4212d7e7b20f37a9a86ab6d50fb::state::WormholeMessage"}, null, 10, true]}' -H 'Content-Type: application/json' http://127.0.0.1:9002 | jq
curl -s -X POST -d '{"jsonrpc":"2.0", "id": 1, "method": "sui_getEvents", "params": [{"Transaction": "cL+uWFEVcQrkAiOxOJmaK7JmlOJdE3/8X5JFbJwBxCQ="}, null, 10, true]}' -H 'Content-Type: application/json' http://127.0.0.1:9002 | jq
"txhash": "0x70bfae585115710ae40223b138999a2bb26694e25d137ffc5f92456c9c01c424", "txhash_b58": "8b8Bn8MUqAWeVz2BE5hMicC9KaRkV6UM4v1JLWGUjxcT", "
Digest: cL+uWFEVcQrkAiOxOJmaK7JmlOJdE3/8X5JFbJwBxCQ=
kubectl exec -it guardian-0 -- /guardiand admin send-observation-request --socket /tmp/admin.sock 21 70bfae585115710ae40223b138999a2bb26694e25d137ffc5f92456c9c01c424
// curl -s -X POST -d '{"jsonrpc":"2.0", "id": 1, "method": "sui_getCommitteeInfo", "params": []}' -H 'Content-Type: application/json' http://127.0.0.1:9002 | jq

14
sui/README.md Normal file
View File

@ -0,0 +1,14 @@
# Installation
Make sure your Cargo version is at least 1.64.0 and then follow the steps below:
- https://docs.sui.io/build/install
# Sui CLI
- do `sui start` to spin up a local network
- do `rpc-server` to start a server for handling rpc calls
- do `sui-faucet` to start a faucet for requesting funds from active-address
# TODOs
- The move dependencies are currently pinned to a version that matches the
docker image for reproducibility. These should be regularly updated to track
any upstream changes before the mainnet release.

11
sui/coin/Move.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "Coin"
version = "0.0.1"
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework", rev = "2d709054a08d904b9229a2472af679f210af3827" }
TokenBridge = { local = "../token_bridge"}
Wormhole = { local = "../wormhole"}
[addresses]
coin="0x0"

View File

@ -0,0 +1,24 @@
module coin::coin {
use sui::transfer;
use sui::tx_context::{Self, TxContext};
use token_bridge::wrapped;
struct COIN has drop {}
fun init(coin_witness: COIN, ctx: &mut TxContext) {
// Step 1. Paste token attestation VAA below.
let vaa_bytes = x"0100000000010080366065746148420220f25a6275097370e8db40984529a6676b7a5fc9feb11755ec49ca626b858ddfde88d15601f85ab7683c5f161413b0412143241c700aff010000000100000001000200000000000000000000000000000000000000000000000000000000deadbeef000000000150eb23000200000000000000000000000000000000000000000000000000000000beefface00020c424545460000000000000000000000000000000000000000000000000000000042656566206661636520546f6b656e0000000000000000000000000000000000";
let new_wrapped_coin = wrapped::create_wrapped_coin(vaa_bytes, coin_witness, ctx);
transfer::transfer(
new_wrapped_coin,
tx_context::sender(ctx)
);
}
#[test_only]
public fun test_init(ctx: &mut TxContext) {
init(COIN {}, ctx)
}
}

15
sui/scripts/create_wrapped.sh Executable file
View File

@ -0,0 +1,15 @@
#!/bin/bash -f
set -euo pipefail
cd "$(dirname "$0")"
. ../env.sh
echo "Creating wrapped asset..."
echo "$COIN_PACKAGE::coin_witness::COIN_WITNESS"
sui client call --function register_wrapped_coin \
--module wrapped --package $TOKEN_PACKAGE --gas-budget 20000 \
--args "$WORM_STATE" "$TOKEN_STATE" "$NEW_WRAPPED_COIN" \
--type-args "$COIN_PACKAGE::coin_witness::COIN_WITNESS"

54
sui/scripts/deploy.sh Executable file
View File

@ -0,0 +1,54 @@
#!/bin/bash -f
set -euo pipefail
cd "$(dirname "$0")"/..
#Transaction Kind : Publish
#----- Transaction Effects ----
#Status : Success
#Created Objects:
# - ID: 0x069b6d8ea50a0b0756518cb08ddbbad2babf8ae0 <= STATE , Owner: Account Address ( 0xe6a09658743da40b0f48c4da1f3fa0d34797d0d3 <= OWNER )
# - ID: 0x73fc05ae6f172f90b12a98cf3ad0b669d6b70e5b <= PACKAGE , Owner: Immutable
cd wormhole
sed -i -e 's/wormhole = .*/wormhole = "0x0"/' Move.toml
make build
sui client publish --gas-budget 10000 | tee publish.log
grep ID: publish.log | head -2 > ids.log
WORM_PACKAGE=$(grep "Immutable" ids.log | sed -e 's/^.*: \(.*\) ,.*/\1/')
sed -i -e "s/wormhole = .*/wormhole = \"$WORM_PACKAGE\"/" Move.toml
WORM_DEPLOYER_CAPABILITY=$(grep -v "Immutable" ids.log | sed -e 's/^.*: \(.*\) ,.*/\1/')
WORM_OWNER=$(grep -v "Immutable" ids.log | sed -e 's/^.*( \(.*\) )/\1/')
cd ../token_bridge
sed -i -e 's/token_bridge = .*/token_bridge = "0x0"/' Move.toml
make build
sui client publish --gas-budget 10000 | tee publish.log
grep ID: publish.log | head -2 > ids.log
TOKEN_PACKAGE=$(grep "Immutable" ids.log | sed -e 's/^.*: \(.*\) ,.*/\1/')
sed -i -e "s/token_bridge = .*/token_bridge = \"$TOKEN_PACKAGE\"/" Move.toml
TOKEN_DEPLOYER_CAPABILITY=$(grep -v "Immutable" ids.log | sed -e 's/^.*: \(.*\) ,.*/\1/')
TOKEN_OWNER=$(grep -v "Immutable" ids.log | sed -e 's/^.*( \(.*\) )/\1/')
sui client call --function init_and_share_state --module state --package $WORM_PACKAGE --gas-budget 20000 --args \"$WORM_DEPLOYER_CAPABILITY\" 0 0 "[190,250,66,157,87,205,24,183,248,164,217,26,45,169,171,74,240,93,15,190]" "[[190,250,66,157,87,205,24,183,248,164,217,26,45,169,171,74,240,93,15,190]]" | tee wormhole.log
WORM_STATE=$(grep Shared wormhole.log | head -1 | sed -e 's/^.*: \(.*\) ,.*/\1/')
sui client call --function get_new_emitter --module wormhole --package $WORM_PACKAGE --gas-budget 20000 --args \"$WORM_STATE\" | tee emitter.log
TOKEN_EMITTER_CAPABILITY=$(grep ID: emitter.log | head -1 | sed -e 's/^.*: \(.*\) ,.*/\1/')
sui client call --function init_and_share_state --module bridge_state --package $TOKEN_PACKAGE --gas-budget 20000 --args "$TOKEN_DEPLOYER_CAPABILITY" "$TOKEN_EMITTER_CAPABILITY" | tee token.log
TOKEN_STATE=$(grep Shared token.log | head -1 | sed -e 's/^.*: \(.*\) ,.*/\1/')
{ echo "export WORM_PACKAGE=$WORM_PACKAGE";
echo "export WORM_DEPLOYER_CAPABILITY=$WORM_DEPLOYER_CAPABILITY";
echo "export WORM_OWNER=$WORM_OWNER";
echo "export TOKEN_PACKAGE=$TOKEN_PACKAGE";
echo "export TOKEN_DEPLOYER_CAPABILITY=$TOKEN_DEPLOYER_CAPABILITY";
echo "export TOKEN_OWNER=$TOKEN_OWNER";
echo "export WORM_STATE=$WORM_STATE";
echo "export TOKEN_EMITTER_CAPABILITY=$TOKEN_EMITTER_CAPABILITY";
echo "export TOKEN_STATE=$TOKEN_STATE";
} > ../env.sh

14
sui/scripts/deploy_coin.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash -f
set -euo pipefail
cd "$(dirname "$0")"/..
. env.sh
sui client publish --gas-budget 20000 --path coin | tee publish.log
grep ID: publish.log | head -2 > ids.log
NEW_WRAPPED_COIN=$(grep "Account Address" ids.log | sed -e 's/^.*: \(.*\) ,.*/\1/')
COIN_PACKAGE=$(grep "Immutable" ids.log | sed -e 's/^.*: \(.*\) ,.*/\1/')
echo "export NEW_WRAPPED_COIN=$NEW_WRAPPED_COIN" >> env.sh
echo "export COIN_PACKAGE=$COIN_PACKAGE" >> env.sh

2
sui/scripts/faucet.sh Executable file
View File

@ -0,0 +1,2 @@
#
curl -X POST -d '{"FixedAmountRequest":{"recipient": "'"$1"'"}}' -H 'Content-Type: application/json' http://127.0.0.1:5003/gas

View File

@ -0,0 +1,10 @@
import { Ed25519Keypair, JsonRpcProvider, RawSigner } from '@mysten/sui.js';
// Generate a new Secp256k1 Keypair
const keypair = new Ed25519Keypair();
const signer = new RawSigner(
keypair,
new JsonRpcProvider('https://gateway.devnet.sui.io:443')
);
console.log(keypair)

View File

@ -0,0 +1,5 @@
#!/bin/bash -f
. env.sh
sui client call --function get_new_emitter --module wormhole --package $WORM_PACKAGE --gas-budget 20000 --args \"$WORM_STATE\"

View File

@ -0,0 +1,24 @@
#!/usr/bin/env bash
# This dev script imports and funds an account, following the steps in
# `sui/NOTES.md`. It also deploys the core/token bridge contracts.
# Remove directory for idempotency
rm -rf $HOME/.sui
# Import key so we have a deterministic address and make it the default account
sui keytool import "daughter exclude wheat pudding police weapon giggle taste space whip satoshi occur" ed25519
sui client << EOF
y
http://localhost:9000
dev
0
EOF
sed -i -e 's/active_address.*/active_address: "0x13b3cb89cf3226d3b860294fc75dc6c91f0c5ecf"/' ~/.sui/sui_config/client.yaml
# Fund account
kubectl exec -it sui-0 -c sui-node -- /tmp/funder.sh
# Deploy contracts
DIR_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
$DIR_PATH/deploy.sh

View File

@ -0,0 +1,5 @@
#!/bin/bash -f
. env.sh
sui client call --function init_and_share_state --module bridge_state --package $TOKEN_PACKAGE --gas-budget 20000 --args \"$TOKEN_STATE\" \"$EMITTER_CAP\"

5
sui/scripts/init_wormhole.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash -f
. env.sh
sui client call --function init_and_share_state --module state --package $WORM_PACKAGE --gas-budget 20000 --args \"$WORM_STATE\" 0 0 [190,250,66,157,87,205,24,183,248,164,217,26,45,169,171,74,240,93,15,190] [190,250,66,157,87,205,24,183,248,164,217,26,45,169,171,74,240,93,15,190]

11
sui/scripts/node_builder.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
source $HOME/.cargo/env
git clone https://github.com/MystenLabs/sui.git --branch devnet
cd sui
cargo --locked install --path crates/sui
cargo --locked install --path crates/sui-faucet
cargo --locked install --path crates/sui-gateway
cargo --locked install --path crates/sui-node

804
sui/scripts/package-lock.json generated Normal file
View File

@ -0,0 +1,804 @@
{
"name": "sui-scripts",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "sui-scripts",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@mysten/sui.js": "^0.10.0",
"axios": "^1.0.0",
"node-fetch": "^3.2.10"
}
},
"node_modules/@mysten/sui.js": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@mysten/sui.js/-/sui.js-0.10.0.tgz",
"integrity": "sha512-z9K34+jQBzeUCTcroTExBzYPCNHohyuz1sR85HYkbymDqdRCTi1IcfBzZDinWibZlk0sZhJtjnozxHklsPvYLQ==",
"dependencies": {
"bn.js": "^5.2.0",
"buffer": "^6.0.3",
"cross-fetch": "^3.1.5",
"jayson": "^3.6.6",
"js-sha3": "^0.8.0",
"lossless-json": "^1.0.5",
"tweetnacl": "^1.0.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "12.20.55",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="
},
"node_modules/@types/ws": {
"version": "7.4.7",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz",
"integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.0.0.tgz",
"integrity": "sha512-SsHsGFN1qNPFT5QhSoSD37SHDfGyLSW5AESmyLk2JeCMHv5g0I9g0Hz/zQHx2KNe0jGXh2q2hAm7OdkXm360CA==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/bn.js": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
"integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"node_modules/cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"dependencies": {
"node-fetch": "2.6.7"
}
},
"node_modules/cross-fetch/node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
"integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==",
"engines": {
"node": ">= 12"
}
},
"node_modules/delay": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz",
"integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"node_modules/es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==",
"dependencies": {
"es6-promise": "^4.0.3"
}
},
"node_modules/eyes": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
"integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==",
"engines": {
"node": "> 0.1.90"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/isomorphic-ws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
"peerDependencies": {
"ws": "*"
}
},
"node_modules/jayson": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/jayson/-/jayson-3.7.0.tgz",
"integrity": "sha512-tfy39KJMrrXJ+mFcMpxwBvFDetS8LAID93+rycFglIQM4kl3uNR3W4lBLE/FFhsoUCEox5Dt2adVpDm/XtebbQ==",
"dependencies": {
"@types/connect": "^3.4.33",
"@types/node": "^12.12.54",
"@types/ws": "^7.4.4",
"commander": "^2.20.3",
"delay": "^5.0.0",
"es6-promisify": "^5.0.0",
"eyes": "^0.1.8",
"isomorphic-ws": "^4.0.1",
"json-stringify-safe": "^5.0.1",
"JSONStream": "^1.3.5",
"lodash": "^4.17.20",
"uuid": "^8.3.2",
"ws": "^7.4.5"
},
"bin": {
"jayson": "bin/jayson.js"
},
"engines": {
"node": ">=8"
}
},
"node_modules/js-sha3": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
},
"node_modules/jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
"integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
"engines": [
"node >= 0.2.0"
]
},
"node_modules/JSONStream": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
"integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
"dependencies": {
"jsonparse": "^1.2.0",
"through": ">=2.2.7 <3"
},
"bin": {
"JSONStream": "bin.js"
},
"engines": {
"node": "*"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lossless-json": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-1.0.5.tgz",
"integrity": "sha512-RicKUuLwZVNZ6ZdJHgIZnSeA05p8qWc5NW0uR96mpPIjN9WDLUg9+kj1esQU1GkPn9iLZVKatSQK5gyiaFHgJA=="
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.2.10",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz",
"integrity": "sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
"engines": {
"node": ">= 8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/ws": {
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
},
"dependencies": {
"@mysten/sui.js": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@mysten/sui.js/-/sui.js-0.10.0.tgz",
"integrity": "sha512-z9K34+jQBzeUCTcroTExBzYPCNHohyuz1sR85HYkbymDqdRCTi1IcfBzZDinWibZlk0sZhJtjnozxHklsPvYLQ==",
"requires": {
"bn.js": "^5.2.0",
"buffer": "^6.0.3",
"cross-fetch": "^3.1.5",
"jayson": "^3.6.6",
"js-sha3": "^0.8.0",
"lossless-json": "^1.0.5",
"tweetnacl": "^1.0.3"
}
},
"@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "12.20.55",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="
},
"@types/ws": {
"version": "7.4.7",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz",
"integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==",
"requires": {
"@types/node": "*"
}
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.0.0.tgz",
"integrity": "sha512-SsHsGFN1qNPFT5QhSoSD37SHDfGyLSW5AESmyLk2JeCMHv5g0I9g0Hz/zQHx2KNe0jGXh2q2hAm7OdkXm360CA==",
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"bn.js": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
"integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="
},
"buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
"requires": {
"node-fetch": "2.6.7"
},
"dependencies": {
"node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": {
"whatwg-url": "^5.0.0"
}
}
}
},
"data-uri-to-buffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
"integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA=="
},
"delay": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz",
"integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw=="
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==",
"requires": {
"es6-promise": "^4.0.3"
}
},
"eyes": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
"integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="
},
"fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"requires": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
}
},
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"requires": {
"fetch-blob": "^3.1.2"
}
},
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"isomorphic-ws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
"requires": {}
},
"jayson": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/jayson/-/jayson-3.7.0.tgz",
"integrity": "sha512-tfy39KJMrrXJ+mFcMpxwBvFDetS8LAID93+rycFglIQM4kl3uNR3W4lBLE/FFhsoUCEox5Dt2adVpDm/XtebbQ==",
"requires": {
"@types/connect": "^3.4.33",
"@types/node": "^12.12.54",
"@types/ws": "^7.4.4",
"commander": "^2.20.3",
"delay": "^5.0.0",
"es6-promisify": "^5.0.0",
"eyes": "^0.1.8",
"isomorphic-ws": "^4.0.1",
"json-stringify-safe": "^5.0.1",
"JSONStream": "^1.3.5",
"lodash": "^4.17.20",
"uuid": "^8.3.2",
"ws": "^7.4.5"
}
},
"js-sha3": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
},
"jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
"integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="
},
"JSONStream": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
"integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
"requires": {
"jsonparse": "^1.2.0",
"through": ">=2.2.7 <3"
}
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lossless-json": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-1.0.5.tgz",
"integrity": "sha512-RicKUuLwZVNZ6ZdJHgIZnSeA05p8qWc5NW0uR96mpPIjN9WDLUg9+kj1esQU1GkPn9iLZVKatSQK5gyiaFHgJA=="
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"requires": {
"mime-db": "1.52.0"
}
},
"node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
},
"node-fetch": {
"version": "3.2.10",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz",
"integrity": "sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==",
"requires": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"web-streams-polyfill": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q=="
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"ws": {
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
"requires": {}
}
}
}

16
sui/scripts/package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "@wormhole-foundation/sui-scripts",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@mysten/sui.js": "^0.10.0",
"axios": "^1.0.0",
"node-fetch": "^3.2.10"
}
}

5
sui/scripts/publish_message.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash -f
. env.sh
sui client call --function publish_message_free --module wormhole --package $WORM_PACKAGE --gas-budget 20000 --args \"$1\" \"$WORM_STATE\" 400 [2]

3
sui/scripts/setup_rust.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y

0
sui/scripts/test.ts Normal file
View File

13
sui/tests/go.mod Normal file
View File

@ -0,0 +1,13 @@
module watcher
go 1.17
require (
github.com/ethereum/go-ethereum v1.10.26 // indirect
github.com/tidwall/gjson v1.14.3 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/net v0.1.0 // indirect
golang.org/x/sys v0.1.0 // indirect
)

14
sui/tests/go.sum Normal file
View File

@ -0,0 +1,14 @@
github.com/ethereum/go-ethereum v1.10.26 h1:i/7d9RBBwiXCEuyduBQzJw/mKmnvzsN14jqBmytw72s=
github.com/ethereum/go-ethereum v1.10.26/go.mod h1:EYFyF19u3ezGLD4RqOkLq+ZCXzYbLoNDdZlMt7kyKFg=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

147
sui/tests/watcher.go Normal file
View File

@ -0,0 +1,147 @@
package main
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/tidwall/gjson"
"golang.org/x/net/websocket"
"log"
"os"
"time"
eth_common "github.com/ethereum/go-ethereum/common"
)
type SuiResult struct {
Timestamp int64 `json:"timestamp"`
TxDigest string `json:"txDigest"`
Event struct {
MoveEvent struct {
PackageID string `json:"packageId"`
TransactionModule string `json:"transactionModule"`
Sender string `json:"sender"`
Type string `json:"type"`
Fields *struct {
ConsistencyLevel uint8 `json:"consistency_level"`
Nonce uint64 `json:"nonce"`
Payload string `json:"payload"`
Sender uint64 `json:"sender"`
Sequence uint64 `json:"sequence"`
} `json:"fields"`
Bcs string `json:"bcs"`
} `json:"moveEvent"`
} `json:"event"`
}
type SuiEventMsg struct {
Jsonrpc string `json:"jsonrpc"`
Method *string `json:"method"`
ID *int `json:"id"`
result *int `json:"result"`
Params *struct {
Subscription int64 `json:"subscription"`
Result *SuiResult `json:"result"`
} `json:"params"`
}
type SuiTxnQuery struct {
Jsonrpc string `json:"jsonrpc"`
Result struct {
Data []SuiResult `json:"data"`
NextCursor interface{} `json:"nextCursor"`
} `json:"result"`
ID int `json:"id"`
}
func inspectBody(body gjson.Result) error {
txDigest := body.Get("txDigest")
timestamp := body.Get("timestamp")
packageId := body.Get("event.moveEvent.packageId") // defense in depth: check this
account := body.Get("event.moveEvent.sender") // defense in depth: check this
consistency_level := body.Get("event.moveEvent.fields.consistency_level")
nonce := body.Get("event.moveEvent.fields.nonce")
payload := body.Get("event.moveEvent.fields.payload")
sender := body.Get("event.moveEvent.fields.sender")
sequence := body.Get("event.moveEvent.fields.sequence")
if !txDigest.Exists() || !timestamp.Exists() || !packageId.Exists() || !account.Exists() || !consistency_level.Exists() || !nonce.Exists() || !payload.Exists() || !sender.Exists() || !sequence.Exists() {
return errors.New("block parse error")
}
id, err := base64.StdEncoding.DecodeString(txDigest.String())
if err != nil {
fmt.Printf("txDigest decode error: %s\n", txDigest.String())
return err
}
var txHash = eth_common.BytesToHash(id) // 32 bytes = d3b136a6a182a40554b2fafbc8d12a7a22737c10c81e33b33d1dcb74c532708b
fmt.Printf("\ntxHash: %s\n", txHash)
pl, err := base64.StdEncoding.DecodeString(payload.String())
if err != nil {
fmt.Printf("payload decode error\n")
return err
}
fmt.Printf("\npl: %s\n", pl)
return nil
}
func main() {
origin := "http://localhost/"
url := "ws://localhost:9001"
ws, err := websocket.Dial(url, "", origin)
if err != nil {
log.Fatal(err)
}
s := fmt.Sprintf(`{"jsonrpc":"2.0", "id": 1, "method": "sui_subscribeEvent", "params": [{"Package": "%s"}]}`, os.Getenv("WORM_PACKAGE"))
fmt.Printf("Sending: %s.\n", s)
if _, err := ws.Write([]byte(s)); err != nil {
log.Fatal(err)
}
for {
var msg = make([]byte, 4096)
var n int
ws.SetReadDeadline(time.Now().Local().Add(1_000_000_000))
if n, err = ws.Read(msg); err != nil {
fmt.Printf("err")
} else {
fmt.Printf("\nReceived: %s.\n", msg[:n])
parsedMsg := gjson.ParseBytes(msg[:n])
var res SuiEventMsg
err = json.Unmarshal(msg[:n], &res)
if err != nil {
fmt.Printf("SuiEventMsg: %s", err.Error())
}
if res.Method != nil {
fmt.Printf("%s\n", *res.Method)
} else {
fmt.Printf("Method nil\n")
}
if res.ID != nil {
fmt.Printf("%d\n", *res.ID)
} else {
fmt.Printf("ID nil\n")
}
result := parsedMsg.Get("params.result")
if !result.Exists() {
// Other messages come through on the channel.. we can ignore them safely
continue
}
fmt.Printf("inspect body called\n")
err := inspectBody(result)
if err != nil {
fmt.Printf("inspectBody: %s", err.Error())
}
}
}
}

94
sui/tests/ws.py Normal file
View File

@ -0,0 +1,94 @@
import websocket
import _thread
import time
import rel
import os
import pprint
import json
import base64
# https://github.com/MystenLabs/sui/pull/5113
# {
# "jsonrpc": "2.0",
# "method": "sui_subscribeEvent",
# "params": {
# "subscription": 1805876586195140,
# "result": {
# "timestamp": 1666704112752,
# "txDigest": "ckB13AaG+OHrO0Ha3I8IK3ERanYHmHAI0jSXnqk9R+I=",
# "event": {
# "moveEvent": {
# "packageId": "0xbd99019f3c8f9d08b5498fedcc97e1c24cddff88",
# "transactionModule": "wormhole",
# "sender": "0xdc2f7334400a353c6a9303235b578477202809c6",
# "type": "0xbd99019f3c8f9d08b5498fedcc97e1c24cddff88::state::WormholeMessage",
# "fields": {
# "consistency_level": 0,
# "nonce": 400,
# "payload": "Ag==",
# "sender": "0xdc2f7334400a353c6a9303235b578477202809c6",
# "sequence": 19,
# "timestamp": 0
# },
# "bcs": "3C9zNEAKNTxqkwMjW1eEdyAoCcYTAAAAAAAAAJABAAAAAAAAAQIAAAAAAAAAAAA="
# }
# }
# }
# }
# }
# curl -s -X POST -d '{"jsonrpc":"2.0", "id": 1, "method": "sui_getEventsByTransaction", "params": ["KgsiF8pCF61N02zX2oMFYLWQdrbxkOD1ypBxND752No=", 2]}' -H 'Content-Type: application/json' http://127.0.0.1:9000 | jq
# {
# "jsonrpc": "2.0",
# "result": [
# {
# "timestamp": 1666704112752,
# "txDigest": "ckB13AaG+OHrO0Ha3I8IK3ERanYHmHAI0jSXnqk9R+I=",
# "event": {
# "moveEvent": {
# "packageId": "0xbd99019f3c8f9d08b5498fedcc97e1c24cddff88",
# "transactionModule": "wormhole",
# "sender": "0xdc2f7334400a353c6a9303235b578477202809c6",
# "type": "0xbd99019f3c8f9d08b5498fedcc97e1c24cddff88::state::WormholeMessage",
# "bcs": "3C9zNEAKNTxqkwMjW1eEdyAoCcYTAAAAAAAAAJABAAAAAAAAAQIAAAAAAAAAAAA="
# }
# }
# }
# ],
# "id": 1
# }
def on_message(ws, message):
v = json.loads(message)
print(json.dumps(v, indent=4))
if "params" in v:
tx = v["params"]["result"]["txDigest"]
#tx = base64.standard_b64decode(tx)
print(tx + " -> " + base64.standard_b64decode(tx).hex())
pl = v["params"]["result"]["event"]["moveEvent"]["fields"]["payload"]
pl = base64.standard_b64decode(pl)
print(pl.hex())
def on_error(ws, error):
print(error)
def on_close(ws, close_status_code, close_msg):
print("### closed ###")
def on_open(ws):
print("Opened connection")
ws.send("{\"jsonrpc\":\"2.0\", \"id\": 1, \"method\": \"sui_subscribeEvent\", \"params\": [{\"Package\": \"" + os.getenv("WORM_PACKAGE") + "\"}]}")
if __name__ == "__main__":
ws = websocket.WebSocketApp("ws://localhost:9001",
on_open=on_open,
on_message=on_message,
on_error=on_error,
on_close=on_close)
ws.run_forever(dispatcher=rel) # Set dispatcher to automatic reconnection
rel.signal(2, rel.abort) # Keyboard Interrupt
rel.dispatch()

14
sui/token_bridge/Makefile Normal file
View File

@ -0,0 +1,14 @@
-include ../../Makefile.help
.PHONY: artifacts
artifacts: build
.PHONY: build
## Build contract
build:
sui move build
.PHONY: test
## Run tests
test:
sui move test

View File

@ -0,0 +1,10 @@
[package]
name = "TokenBridge"
version = "0.0.1"
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework", rev = "2d709054a08d904b9229a2472af679f210af3827" }
Wormhole = { local = "../wormhole" }
[addresses]
token_bridge = "0x0"

View File

@ -0,0 +1,60 @@
# Sui Wormhole Token Bridge Design
TODO: make sure this is up to date
The Token Bridge is responsible for storing treasury caps and locked tokens and exposing functions for initiating and completing transfers, which are gated behind VAAs. It also supports token attestations from foreign chains (which must be done once prior to transfer), contract upgrades, and chain registration.
## Token Attestation
TODO: up to date implementation notes
The sui RPC provides a way to get the object id for CoinMetadata objects:
https://github.com/MystenLabs/sui/pull/6281/files#diff-80bf625d87d89549275351d95cfdfab4a6c2a1311804adbc5f1a7fcff225f049R430
we should document that this will only work for coins whose metadata object is
either shared or frozen. This seems to be the case at least for all example
coins, so we can probably expect most coins to follow this pattern. Ones that
don't, however, will not be transferrable through the token bridge
## Creating new Coin Types
TODO: up to date implementation notes
Internally, `create_wrapped_coin` calls `coin::create_currency<CoinType>(witness, decimals, ctx)`, obtains a treasury cap, and finally stores
the treasury cap inside of a `TreasuryCapContainer` object, whose usage is restricted by the functions in its defining module (in particular, gated by VAAs). The `TreasuryCapContainer` is mutably shared so users can access it in a permissionless way. The reason that the treasury cap itself
is not mutably shared is that users would be able to use it to mint/burn tokens without limitation.
## Initiating and Completing Transfers
The Token Bridge stores both coins transferred by the user for lockup and treasury caps used for minting/burning wrapped assets. To this end, we implement two structs, which are both mutably shared and whose usage is restricted by VAA-gated functions defined in their parent modules.
```rust
struct TreasuryCapContainer<T> {
t: TreasuryCap<T>,
}
```
```rust
struct CoinStore<T> {
coins: coin<T>,
}
```
Accordingly, we define the following functions for initiating and completing transfers. There is a version of each for wrapped and native coins, because we can't store info about `CoinType` within `BridgeState`. There does not seem to be a way of introspecting the CoinType to determine whether it represents a native or wrapped asset. In addition, we have to use either a `TreasuryCapStore` or `CoinStore` depending on whether we want to initiate or complete a transfer for a native or wrapped asset, which leads to different function signatures.
### `complete_transfer_wrapped<T>(treasury_cap_store: &mut TreasuryCapStore<T>)`
- Use treasury cap to mint wrapped assets to recipient
### `complete_transfer_native<T>(store: &mut CoinStore<T>)`
- Idea is to extract coins from token_bridge and give them to the recipient. We pass in a mutably shared `CoinStore` object, which contains balance or coin objects belonging to token bridge. Coins are extracted from this object and passed to the recipient.
### `transfer_native<T>(coin: Coin<T>, store: &mut CoinStore<T>)`
- Transfer user-supplied native coins to `CoinStore`
### `transfer_wrapped<T>(treasury_cap_store: &mut TreasuryCapStore<T>)`
- Use the treasury cap to burn some user-supplied wrapped assets
## Contract Upgrades
Not yet supported in Sui.
## Bridge State
TODO: up to date implementation notes

View File

@ -0,0 +1,146 @@
module token_bridge::attest_token {
use sui::sui::SUI;
use sui::coin::{Coin, CoinMetadata};
use sui::tx_context::TxContext;
use wormhole::state::{State as WormholeState};
use token_bridge::bridge_state::{Self as state, BridgeState};
use token_bridge::asset_meta::{Self, AssetMeta};
public entry fun attest_token<CoinType>(
wormhole_state: &mut WormholeState,
bridge_state: &mut BridgeState,
coin_meta: &CoinMetadata<CoinType>,
fee_coins: Coin<SUI>,
ctx: &mut TxContext
) {
let asset_meta = attest_token_internal(
wormhole_state,
bridge_state,
coin_meta,
ctx
);
let payload = asset_meta::encode(asset_meta);
let nonce = 0;
state::publish_message(
wormhole_state,
bridge_state,
nonce,
payload,
fee_coins
);
}
fun attest_token_internal<CoinType>(
wormhole_state: &mut WormholeState,
bridge_state: &mut BridgeState,
coin_meta: &CoinMetadata<CoinType>,
ctx: &mut TxContext
): AssetMeta {
let asset_meta =
state::register_native_asset<CoinType>(wormhole_state, bridge_state, coin_meta, ctx);
return asset_meta
}
#[test_only]
public fun test_attest_token_internal<CoinType>(
wormhole_state: &mut WormholeState,
bridge_state: &mut BridgeState,
coin_meta: &CoinMetadata<CoinType>,
ctx: &mut TxContext
): AssetMeta {
attest_token_internal<CoinType>(
wormhole_state,
bridge_state,
coin_meta,
ctx
)
}
}
#[test_only]
module token_bridge::attest_token_test{
use sui::test_scenario::{Self, Scenario, next_tx, ctx, take_shared, return_shared};
use sui::coin::{CoinMetadata};
use wormhole::state::{State};
use token_bridge::string32::{Self};
use token_bridge::bridge_state::{BridgeState};
use token_bridge::attest_token::{test_attest_token_internal};
use token_bridge::bridge_state_test::{set_up_wormhole_core_and_token_bridges};
use token_bridge::native_coin_witness::{Self, NATIVE_COIN_WITNESS};
use token_bridge::asset_meta::{Self};
fun scenario(): Scenario { test_scenario::begin(@0x123233) }
fun people(): (address, address, address) { (@0x124323, @0xE05, @0xFACE) }
#[test]
fun test_attest_token(){
let test = scenario();
let (admin, _, _) = people();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin); {
native_coin_witness::test_init(ctx(&mut test));
};
next_tx(&mut test, admin); {
let wormhole_state = take_shared<State>(&test);
let bridge_state = take_shared<BridgeState>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
let asset_meta = test_attest_token_internal<NATIVE_COIN_WITNESS>(
&mut wormhole_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
assert!(asset_meta::get_decimals(&asset_meta)==10, 0);
assert!(asset_meta::get_symbol(&asset_meta)==string32::from_bytes(x"00"), 0);
assert!(asset_meta::get_name(&asset_meta)==string32::from_bytes(x"11"), 0);
return_shared<State>(wormhole_state);
return_shared<BridgeState>(bridge_state);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
};
test_scenario::end(test);
}
#[test]
#[expected_failure(abort_code = 0, location=0000000000000000000000000000000000000002::dynamic_field)]
fun test_attest_token_twice_fails(){
let test = scenario();
let (admin, _, _) = people();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin); {
native_coin_witness::test_init(ctx(&mut test));
};
next_tx(&mut test, admin); {
let wormhole_state = take_shared<State>(&test);
let bridge_state = take_shared<BridgeState>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
let _asset_meta_1 = test_attest_token_internal<NATIVE_COIN_WITNESS>(
&mut wormhole_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
let _asset_meta_2 = test_attest_token_internal<NATIVE_COIN_WITNESS>(
&mut wormhole_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
return_shared<State>(wormhole_state);
return_shared<BridgeState>(bridge_state);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
};
test_scenario::end(test);
}
}

View File

@ -0,0 +1,510 @@
module token_bridge::bridge_state {
use std::option::{Self, Option};
use std::ascii::{Self};
use sui::object::{Self, UID};
use sui::vec_map::{Self, VecMap};
use sui::tx_context::{TxContext};
use sui::coin::{Self, Coin, TreasuryCap, CoinMetadata};
use sui::transfer::{Self};
use sui::tx_context::{Self};
use sui::sui::SUI;
use token_bridge::string32;
use token_bridge::dynamic_set;
use token_bridge::asset_meta::{Self, AssetMeta};
use wormhole::external_address::{Self, ExternalAddress};
use wormhole::myu16::{U16};
use wormhole::wormhole::{Self};
use wormhole::state::{Self as wormhole_state, State as WormholeState};
use wormhole::emitter::{EmitterCapability};
use wormhole::set::{Self, Set};
const E_IS_NOT_WRAPPED_ASSET: u64 = 0;
const E_IS_NOT_REGISTERED_NATIVE_ASSET: u64 = 1;
const E_COIN_TYPE_HAS_NO_REGISTERED_INTEGER_ADDRESS: u64 = 2;
const E_COIN_TYPE_HAS_REGISTERED_INTEGER_ADDRESS: u64 = 3;
const E_ORIGIN_CHAIN_MISMATCH: u64 = 4;
const E_ORIGIN_ADDRESS_MISMATCH: u64 = 5;
const E_IS_WRAPPED_ASSET: u64 = 6;
friend token_bridge::vaa;
friend token_bridge::register_chain;
friend token_bridge::wrapped;
friend token_bridge::complete_transfer;
friend token_bridge::transfer_tokens;
friend token_bridge::attest_token;
#[test_only]
friend token_bridge::bridge_state_test;
#[test_only]
friend token_bridge::complete_transfer_test;
#[test_only]
friend token_bridge::token_bridge_vaa_test;
/// Capability for creating a bridge state object, granted to sender when this
/// module is deployed
struct DeployerCapability has key, store {id: UID}
/// WrappedAssetInfo<CoinType> stores all the metadata about a wrapped asset
struct WrappedAssetInfo<phantom CoinType> has key, store {
id: UID,
token_chain: U16,
token_address: ExternalAddress,
treasury_cap: TreasuryCap<CoinType>,
}
struct NativeAssetInfo<phantom CoinType> has key, store {
id: UID,
// Even though we can look up token_chain at any time from wormhole state,
// it can be more efficient to store it here locally so we don't have to do lookups.
custody: Coin<CoinType>,
asset_meta: AssetMeta,
}
/// OriginInfo is a non-Sui object that stores info about a tokens native token
/// chain and address
struct OriginInfo<phantom CoinType> has store, copy, drop {
token_chain: U16,
token_address: ExternalAddress,
}
public fun get_token_chain_from_origin_info<CoinType>(origin_info: &OriginInfo<CoinType>): U16 {
return origin_info.token_chain
}
public fun get_token_address_from_origin_info<CoinType>(origin_info: &OriginInfo<CoinType>): ExternalAddress {
return origin_info.token_address
}
public fun get_origin_info_from_wrapped_asset_info<CoinType>(wrapped_asset_info: &WrappedAssetInfo<CoinType>): OriginInfo<CoinType> {
OriginInfo { token_chain: wrapped_asset_info.token_chain, token_address: wrapped_asset_info.token_address }
}
public fun get_origin_info_from_native_asset_info<CoinType>(native_asset_info: &NativeAssetInfo<CoinType>): OriginInfo<CoinType> {
let asset_meta = &native_asset_info.asset_meta;
let token_chain = asset_meta::get_token_chain(asset_meta);
let token_address = asset_meta::get_token_address(asset_meta);
OriginInfo { token_chain, token_address }
}
public(friend) fun create_wrapped_asset_info<CoinType>(
token_chain: U16,
token_address: ExternalAddress,
treasury_cap: TreasuryCap<CoinType>,
ctx: &mut TxContext
): WrappedAssetInfo<CoinType> {
return WrappedAssetInfo {
id: object::new(ctx),
token_chain,
token_address,
treasury_cap
}
}
// Integer label for coin types registered with Wormhole
struct NativeIdRegistry has key, store {
id: UID,
index: u64, // next index to use
}
fun next_native_id(registry: &mut NativeIdRegistry): ExternalAddress {
use wormhole::serialize::serialize_u64;
let cur_index = registry.index;
registry.index = cur_index + 1;
let bytes = std::vector::empty<u8>();
serialize_u64(&mut bytes, cur_index);
external_address::from_bytes(bytes)
}
// Treasury caps, token stores, consumed VAAs, registered emitters, etc.
// are stored as dynamic fields of bridge state.
struct BridgeState has key, store {
id: UID,
/// Set of consumed VAA hashes
consumed_vaas: Set<vector<u8>>,
/// Token bridge owned emitter capability
emitter_cap: EmitterCapability,
/// Mapping of bridge contracts on other chains
registered_emitters: VecMap<U16, ExternalAddress>,
native_id_registry: NativeIdRegistry,
}
fun init(ctx: &mut TxContext) {
transfer::transfer(DeployerCapability{id: object::new(ctx)}, tx_context::sender(ctx));
}
#[test_only]
public fun test_init(ctx: &mut TxContext) {
transfer::transfer(DeployerCapability{id: object::new(ctx)}, tx_context::sender(ctx));
}
// converts owned state object into a shared object, so that anyone can get a reference to &mut State
// and pass it into various functions
public entry fun init_and_share_state(
deployer: DeployerCapability,
emitter_cap: EmitterCapability,
ctx: &mut TxContext
) {
let DeployerCapability{ id } = deployer;
object::delete(id);
let state = BridgeState {
id: object::new(ctx),
consumed_vaas: set::new(ctx),
emitter_cap,
registered_emitters: vec_map::empty(),
native_id_registry: NativeIdRegistry {
id: object::new(ctx),
index: 1,
}
};
// permanently shares state
transfer::share_object(state);
}
public(friend) fun deposit<CoinType>(
bridge_state: &mut BridgeState,
coin: Coin<CoinType>,
) {
// TODO: create custom errors for each dynamic_set::borrow_mut
let native_asset = dynamic_set::borrow_mut<NativeAssetInfo<CoinType>>(&mut bridge_state.id);
coin::join<CoinType>(&mut native_asset.custody, coin);
}
public(friend) fun withdraw<CoinType>(
_verified_coin_witness: VerifiedCoinType<CoinType>,
bridge_state: &mut BridgeState,
value: u64,
ctx: &mut TxContext
): Coin<CoinType> {
let native_asset = dynamic_set::borrow_mut<NativeAssetInfo<CoinType>>(&mut bridge_state.id);
coin::split<CoinType>(&mut native_asset.custody, value, ctx)
}
public(friend) fun mint<CoinType>(
_verified_coin_witness: VerifiedCoinType<CoinType>,
bridge_state: &mut BridgeState,
value: u64,
ctx: &mut TxContext,
): Coin<CoinType> {
let wrapped_info = dynamic_set::borrow_mut<WrappedAssetInfo<CoinType>>(&mut bridge_state.id);
coin::mint<CoinType>(&mut wrapped_info.treasury_cap, value, ctx)
}
public(friend) fun burn<CoinType>(
bridge_state: &mut BridgeState,
coin: Coin<CoinType>,
) {
let wrapped_info = dynamic_set::borrow_mut<WrappedAssetInfo<CoinType>>(&mut bridge_state.id);
coin::burn<CoinType>(&mut wrapped_info.treasury_cap, coin);
}
public(friend) fun publish_message(
wormhole_state: &mut WormholeState,
bridge_state: &mut BridgeState,
nonce: u64,
payload: vector<u8>,
message_fee: Coin<SUI>,
): u64 {
wormhole::publish_message(
&mut bridge_state.emitter_cap,
wormhole_state,
nonce,
payload,
message_fee,
)
}
/// getters
public fun vaa_is_consumed(state: &BridgeState, hash: vector<u8>): bool {
set::contains(&state.consumed_vaas, hash)
}
public fun get_registered_emitter(state: &BridgeState, chain_id: &U16): Option<ExternalAddress> {
if (vec_map::contains(&state.registered_emitters, chain_id)) {
option::some(*vec_map::get(&state.registered_emitters, chain_id))
} else {
option::none()
}
}
public fun is_wrapped_asset<CoinType>(bridge_state: &BridgeState): bool {
dynamic_set::exists_<WrappedAssetInfo<CoinType>>(&bridge_state.id)
}
public fun is_registered_native_asset<CoinType>(bridge_state: &BridgeState): bool {
dynamic_set::exists_<NativeAssetInfo<CoinType>>(&bridge_state.id)
}
/// Returns the origin information for a CoinType
public fun origin_info<CoinType>(bridge_state: &BridgeState): OriginInfo<CoinType> {
if (is_wrapped_asset<CoinType>(bridge_state)) {
get_wrapped_asset_origin_info<CoinType>(bridge_state)
} else {
get_registered_native_asset_origin_info(bridge_state)
}
}
/// A value of type `VerifiedCoinType<T>` witnesses the fact that the type
/// `T` has been verified to correspond to a particular chain id and token
/// address (may be either a wrapped or native asset).
/// The verification is performed by `verify_coin_type`.
///
/// This is important because the coin type is an input to several
/// functions, and is thus untrusted. Most coin-related functionality
/// requires passing in a coin type generic argument.
/// When transferring tokens *out*, that type instantiation determines the
/// token bridge's behaviour, and thus we just take whatever was supplied.
/// When transferring tokens *in*, it's the transfer VAA that determines
/// which coin should be used via the origin chain and origin address
/// fields.
///
/// For technical reasons, the latter case still requires a type argument to
/// be passed in (since Move does not support existential types, so we must
/// rely on old school universal quantification). We must thus verify that
/// the supplied type corresponds to the origin info in the VAA.
///
/// Accordingly, the `mint` and `withdraw` operations are gated by this
/// witness type, since these two operations require a VAA to supply the
/// token information. This ensures that those two functions can't be called
/// without first verifying the `CoinType`.
struct VerifiedCoinType<phantom CoinType> has copy, drop {}
/// See the documentation for `VerifiedCoinType` above.
public fun verify_coin_type<CoinType>(
bridge_state: &BridgeState,
token_chain: U16,
token_address: ExternalAddress
): VerifiedCoinType<CoinType> {
let coin_origin = origin_info<CoinType>(bridge_state);
assert!(coin_origin.token_chain == token_chain, E_ORIGIN_CHAIN_MISMATCH);
assert!(coin_origin.token_address == token_address, E_ORIGIN_ADDRESS_MISMATCH);
VerifiedCoinType {}
}
public fun get_wrapped_asset_origin_info<CoinType>(bridge_state: &BridgeState): OriginInfo<CoinType> {
assert!(is_wrapped_asset<CoinType>(bridge_state), E_IS_NOT_WRAPPED_ASSET);
let wrapped_asset_info = dynamic_set::borrow<WrappedAssetInfo<CoinType>>(&bridge_state.id);
get_origin_info_from_wrapped_asset_info(wrapped_asset_info)
}
public fun get_registered_native_asset_origin_info<CoinType>(bridge_state: &BridgeState): OriginInfo<CoinType> {
let native_asset_info = dynamic_set::borrow<NativeAssetInfo<CoinType>>(&bridge_state.id);
get_origin_info_from_native_asset_info(native_asset_info)
}
/// setters
public(friend) fun set_registered_emitter(state: &mut BridgeState, chain_id: U16, emitter: ExternalAddress) {
if (vec_map::contains<U16, ExternalAddress>(&mut state.registered_emitters, &chain_id)){
vec_map::remove<U16, ExternalAddress>(&mut state.registered_emitters, &chain_id);
};
vec_map::insert<U16, ExternalAddress>(&mut state.registered_emitters, chain_id, emitter);
}
/// dynamic ops
public(friend) fun store_consumed_vaa(bridge_state: &mut BridgeState, vaa: vector<u8>) {
set::add(&mut bridge_state.consumed_vaas, vaa);
}
public(friend) fun register_wrapped_asset<CoinType>(bridge_state: &mut BridgeState, wrapped_asset_info: WrappedAssetInfo<CoinType>) {
dynamic_set::add<WrappedAssetInfo<CoinType>>(&mut bridge_state.id, wrapped_asset_info);
}
public(friend) fun register_native_asset<CoinType>(
wormhole_state: &WormholeState,
bridge_state: &mut BridgeState,
coin_meta: &CoinMetadata<CoinType>,
ctx: &mut TxContext
): AssetMeta {
assert!(!is_wrapped_asset<CoinType>(bridge_state), E_IS_WRAPPED_ASSET); // TODO - test
let asset_meta = asset_meta::create(
next_native_id(&mut bridge_state.native_id_registry),
wormhole_state::get_chain_id(wormhole_state), // TODO: should we just hardcode this?
coin::get_decimals<CoinType>(coin_meta), // decimals
string32::from_bytes(ascii::into_bytes(coin::get_symbol<CoinType>(coin_meta))), // symbol
string32::from_string(&coin::get_name<CoinType>(coin_meta)) // name
);
let native_asset_info = NativeAssetInfo<CoinType> {
id: object::new(ctx),
custody: coin::zero(ctx),
asset_meta,
};
dynamic_set::add<NativeAssetInfo<CoinType>>(&mut bridge_state.id, native_asset_info);
asset_meta
}
}
#[test_only]
module token_bridge::bridge_state_test{
use sui::test_scenario::{Self, Scenario, next_tx, ctx, take_from_address, take_shared, return_shared};
use sui::coin::{CoinMetadata};
use wormhole::state::{State};
use wormhole::test_state::{init_wormhole_state};
use wormhole::wormhole::{Self};
use wormhole::external_address::{Self};
use token_bridge::bridge_state::{Self as state, BridgeState, DeployerCapability};
use token_bridge::native_coin_witness::{Self, NATIVE_COIN_WITNESS};
use token_bridge::native_coin_witness_v2::{Self, NATIVE_COIN_WITNESS_V2};
fun scenario(): Scenario { test_scenario::begin(@0x123233) }
fun people(): (address, address, address) { (@0x124323, @0xE05, @0xFACE) }
#[test]
fun test_state_setters() {
test_state_setters_(scenario())
}
#[test]
fun test_coin_type_addressing(){
test_coin_type_addressing_(scenario())
}
#[test]
#[expected_failure(abort_code = 0, location=0000000000000000000000000000000000000002::dynamic_field)]
fun test_coin_type_addressing_failure_case(){
test_coin_type_addressing_failure_case_(scenario())
}
public fun set_up_wormhole_core_and_token_bridges(admin: address, test: Scenario): Scenario {
// init and share wormhole core bridge
test = init_wormhole_state(test, admin);
// call init for token bridge to get deployer cap
next_tx(&mut test, admin); {
state::test_init(ctx(&mut test));
};
// register for emitter cap and init_and_share token bridge
next_tx(&mut test, admin); {
let wormhole_state = take_shared<State>(&test);
let my_emitter = wormhole::register_emitter(&mut wormhole_state, ctx(&mut test));
let deployer = take_from_address<DeployerCapability>(&test, admin);
state::init_and_share_state(deployer, my_emitter, ctx(&mut test));
return_shared<State>(wormhole_state);
};
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
return_shared<BridgeState>(bridge_state);
};
return test
}
fun test_state_setters_(test: Scenario) {
let (admin, _, _) = people();
test = set_up_wormhole_core_and_token_bridges(admin, test);
//test BridgeState setter and getter functions
next_tx(&mut test, admin); {
let state = take_shared<BridgeState>(&test);
// test store consumed vaa
state::store_consumed_vaa(&mut state, x"1234");
assert!(state::vaa_is_consumed(&state, x"1234"), 0);
// TODO - test store coin store
// TODO - test store treasury cap
return_shared<BridgeState>(state);
};
test_scenario::end(test);
}
fun test_coin_type_addressing_(test: Scenario) {
let (admin, _, _) = people();
test = set_up_wormhole_core_and_token_bridges(admin, test);
//test coin type addressing
next_tx(&mut test, admin); {
native_coin_witness::test_init(ctx(&mut test));
native_coin_witness_v2::test_init(ctx(&mut test));
};
next_tx(&mut test, admin); {
let wormhole_state = take_shared<State>(&test);
let bridge_state = take_shared<BridgeState>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
state::register_native_asset<NATIVE_COIN_WITNESS>(
&mut wormhole_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
let origin_info = state::origin_info<NATIVE_COIN_WITNESS>(&bridge_state);
let address = state::get_token_address_from_origin_info(&origin_info);
assert!(address == external_address::from_bytes(x"01"), 0);
let coin_meta_v2 = take_shared<CoinMetadata<NATIVE_COIN_WITNESS_V2>>(&test);
state::register_native_asset<NATIVE_COIN_WITNESS_V2>(
&mut wormhole_state,
&mut bridge_state,
&coin_meta_v2,
ctx(&mut test)
);
let origin_info = state::origin_info<NATIVE_COIN_WITNESS_V2>(&bridge_state);
let address = state::get_token_address_from_origin_info(&origin_info);
assert!(address == external_address::from_bytes(x"02"), 0);
return_shared<State>(wormhole_state);
return_shared<BridgeState>(bridge_state);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS_V2>>(coin_meta_v2);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
};
test_scenario::end(test);
}
fun test_coin_type_addressing_failure_case_(test: Scenario) {
let (admin, _, _) = people();
test = set_up_wormhole_core_and_token_bridges(admin, test);
//test coin type addressing
next_tx(&mut test, admin); {
native_coin_witness::test_init(ctx(&mut test));
native_coin_witness_v2::test_init(ctx(&mut test));
};
next_tx(&mut test, admin); {
let wormhole_state = take_shared<State>(&test);
let bridge_state = take_shared<BridgeState>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
state::register_native_asset<NATIVE_COIN_WITNESS>(
&mut wormhole_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
let origin_info = state::origin_info<NATIVE_COIN_WITNESS>(&bridge_state);
let address = state::get_token_address_from_origin_info(&origin_info);
assert!(address == external_address::from_bytes(x"01"), 0);
// aborts because trying to re-register native coin
state::register_native_asset<NATIVE_COIN_WITNESS>(
&mut wormhole_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
return_shared<State>(wormhole_state);
return_shared<BridgeState>(bridge_state);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
};
test_scenario::end(test);
}
}

View File

@ -0,0 +1,769 @@
module token_bridge::complete_transfer {
use sui::tx_context::{TxContext};
use sui::transfer::{Self as transfer_object};
use sui::coin::{Self, CoinMetadata};
use wormhole::state::{State as WormholeState};
use wormhole::external_address::{Self};
use token_bridge::bridge_state::{Self, BridgeState, VerifiedCoinType};
use token_bridge::vaa::{Self};
use token_bridge::transfer::{Self, Transfer};
use token_bridge::normalized_amount::{denormalize};
const E_INVALID_TARGET: u64 = 0;
public entry fun submit_vaa<CoinType>(
wormhole_state: &mut WormholeState,
bridge_state: &mut BridgeState,
coin_meta: &CoinMetadata<CoinType>,
vaa: vector<u8>,
fee_recipient: address,
ctx: &mut TxContext
) {
let vaa = vaa::parse_verify_and_replay_protect(
wormhole_state,
bridge_state,
vaa,
ctx
);
let transfer = transfer::parse(wormhole::myvaa::destroy(vaa));
let token_chain = transfer::get_token_chain(&transfer);
let token_address = transfer::get_token_address(&transfer);
let verified_coin_witness = bridge_state::verify_coin_type<CoinType>(
bridge_state,
token_chain,
token_address
);
complete_transfer<CoinType>(
verified_coin_witness,
&transfer,
wormhole_state,
bridge_state,
coin_meta,
fee_recipient,
ctx
);
}
// complete transfer with arbitrary Transfer request and without the VAA
// for native tokens
#[test_only]
public fun test_complete_transfer<CoinType>(
transfer: &Transfer,
wormhole_state: &mut WormholeState,
bridge_state: &mut BridgeState,
coin_meta: &CoinMetadata<CoinType>,
fee_recipient: address,
ctx: &mut TxContext
) {
let token_chain = transfer::get_token_chain(transfer);
let token_address = transfer::get_token_address(transfer);
let verified_coin_witness = bridge_state::verify_coin_type<CoinType>(
bridge_state,
token_chain,
token_address
);
complete_transfer<CoinType>(
verified_coin_witness,
transfer,
wormhole_state,
bridge_state,
coin_meta,
fee_recipient,
ctx
);
}
fun complete_transfer<CoinType>(
verified_coin_witness: VerifiedCoinType<CoinType>,
transfer: &Transfer,
wormhole_state: &mut WormholeState,
bridge_state: &mut BridgeState,
coin_meta: &CoinMetadata<CoinType>,
fee_recipient: address,
ctx: &mut TxContext
) {
let to_chain = transfer::get_to_chain(transfer);
let this_chain = wormhole::state::get_chain_id(wormhole_state);
assert!(to_chain == this_chain, E_INVALID_TARGET);
let recipient = external_address::to_address(&transfer::get_to(transfer));
let decimals = coin::get_decimals(coin_meta);
let amount = denormalize(transfer::get_amount(transfer), decimals);
let fee_amount = denormalize(transfer::get_fee(transfer), decimals);
let recipient_coins;
if (bridge_state::is_wrapped_asset<CoinType>(bridge_state)) {
recipient_coins = bridge_state::mint<CoinType>(
verified_coin_witness,
bridge_state,
amount,
ctx
);
} else {
recipient_coins = bridge_state::withdraw<CoinType>(
verified_coin_witness,
bridge_state,
amount,
ctx
);
};
// take out fee from the recipient's coins. `extract` will revert
// if fee > amount
let fee_coins = coin::split(&mut recipient_coins, fee_amount, ctx);
transfer_object::transfer(recipient_coins, recipient);
transfer_object::transfer(fee_coins, fee_recipient);
}
}
#[test_only]
module token_bridge::complete_transfer_test {
use std::bcs::{Self};
use sui::test_scenario::{Self, Scenario, next_tx, return_shared, take_shared, ctx, take_from_address, return_to_address};
use sui::coin::{Self, Coin, CoinMetadata};
use wormhole::myu16::{Self as u16};
use wormhole::external_address::{Self};
use token_bridge::normalized_amount::{Self};
use token_bridge::transfer::{Self, Transfer};
use token_bridge::bridge_state::{Self, BridgeState};
use token_bridge::coin_witness::{Self, COIN_WITNESS};
use token_bridge::coin_witness_test::{test_register_wrapped_};
use token_bridge::complete_transfer::{Self};
use token_bridge::native_coin_witness::{Self, NATIVE_COIN_WITNESS};
use token_bridge::native_coin_witness_v2::{Self, NATIVE_COIN_WITNESS_V2};
use token_bridge::bridge_state_test::{set_up_wormhole_core_and_token_bridges};
use wormhole::state::{Self as wormhole_state, State};
fun scenario(): Scenario { test_scenario::begin(@0x123233) }
fun people(): (address, address, address) { (@0x124323, @0xE05, @0xFACE) }
struct OTHER_COIN_WITNESS has drop {}
#[test]
fun test_complete_native_transfer(){
let (admin, fee_recipient_person, _) = people();
let test = scenario();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin);{
native_coin_witness::test_init(ctx(&mut test));
};
// register native asset type with the token bridge
next_tx(&mut test, admin);{
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
bridge_state::register_native_asset<NATIVE_COIN_WITNESS>(
&mut worm_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
};
// create a treasury cap for the native asset type, mint some tokens,
// and deposit the native tokens into the token bridge
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let t_cap = take_shared<coin::TreasuryCap<NATIVE_COIN_WITNESS>>(&test);
let coins = coin::mint<NATIVE_COIN_WITNESS>(&mut t_cap, 10000000000, ctx(&mut test));
bridge_state::deposit<NATIVE_COIN_WITNESS>(&mut bridge_state, coins);
return_shared<coin::TreasuryCap<NATIVE_COIN_WITNESS>>(t_cap);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
};
// complete transfer, sending native tokens to a recipient address
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
let to = admin;
let amount = 1000000000;
let fee_amount = 100000000;
let decimals = 10;
let token_address = external_address::from_bytes(x"01");
let token_chain = wormhole_state::get_chain_id(&worm_state);
let to_chain = wormhole_state::get_chain_id(&worm_state);
let transfer: Transfer = transfer::create(
normalized_amount::normalize(amount, decimals),
token_address,
token_chain,
external_address::from_bytes(bcs::to_bytes(&to)),
to_chain,
normalized_amount::normalize(fee_amount, decimals),
);
complete_transfer::test_complete_transfer<NATIVE_COIN_WITNESS>(
&transfer,
&mut worm_state,
&mut bridge_state,
&coin_meta,
fee_recipient_person,
ctx(&mut test)
);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
};
// check balances after
next_tx(&mut test, admin);{
let coins = take_from_address<Coin<NATIVE_COIN_WITNESS>>(&test, admin);
assert!(coin::value<NATIVE_COIN_WITNESS>(&coins) == 900000000, 0);
return_to_address<Coin<NATIVE_COIN_WITNESS>>(admin, coins);
let fee_coins = take_from_address<Coin<NATIVE_COIN_WITNESS>>(&test, fee_recipient_person);
assert!(coin::value<NATIVE_COIN_WITNESS>(&fee_coins) == 100000000, 0);
return_to_address<Coin<NATIVE_COIN_WITNESS>>(fee_recipient_person, fee_coins);
};
test_scenario::end(test);
}
#[test]
fun test_complete_native_transfer_10_decimals(){
let (admin, fee_recipient_person, _) = people();
let test = scenario();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin);{
native_coin_witness::test_init(ctx(&mut test));
};
// register native asset type with the token bridge
next_tx(&mut test, admin);{
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
bridge_state::register_native_asset<NATIVE_COIN_WITNESS>(
&mut worm_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
native_coin_witness::test_init(ctx(&mut test));
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
};
// create a treasury cap for the native asset type, mint some tokens,
// and deposit the native tokens into the token bridge
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let t_cap = take_shared<coin::TreasuryCap<NATIVE_COIN_WITNESS>>(&test);
let coins = coin::mint<NATIVE_COIN_WITNESS>(&mut t_cap, 10000000000, ctx(&mut test));
bridge_state::deposit<NATIVE_COIN_WITNESS>(&mut bridge_state, coins);
return_shared<coin::TreasuryCap<NATIVE_COIN_WITNESS>>(t_cap);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
};
// complete transfer, sending native tokens to a recipient address
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
let to = admin;
// dust at the end gets rounded to nothing, since 10-8=2 digits are lopped off
let amount = 1000000079;
let fee_amount = 100000000;
let decimals = 10;
let token_address = external_address::from_bytes(x"01");
let token_chain = wormhole_state::get_chain_id(&worm_state);
let to_chain = wormhole_state::get_chain_id(&worm_state);
let transfer: Transfer = transfer::create(
normalized_amount::normalize(amount, decimals),
token_address,
token_chain,
external_address::from_bytes(bcs::to_bytes(&to)),
to_chain,
normalized_amount::normalize(fee_amount, decimals),
);
complete_transfer::test_complete_transfer<NATIVE_COIN_WITNESS>(
&transfer,
&mut worm_state,
&mut bridge_state,
&coin_meta,
fee_recipient_person,
ctx(&mut test)
);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
};
// check balances after
next_tx(&mut test, admin);{
let coins = take_from_address<Coin<NATIVE_COIN_WITNESS>>(&test, admin);
assert!(coin::value<NATIVE_COIN_WITNESS>(&coins) == 900000000, 0);
return_to_address<Coin<NATIVE_COIN_WITNESS>>(admin, coins);
let fee_coins = take_from_address<Coin<NATIVE_COIN_WITNESS>>(&test, fee_recipient_person);
assert!(coin::value<NATIVE_COIN_WITNESS>(&fee_coins) == 100000000, 0);
return_to_address<Coin<NATIVE_COIN_WITNESS>>(fee_recipient_person, fee_coins);
};
test_scenario::end(test);
}
#[test]
fun test_complete_native_transfer_4_decimals(){
let (admin, fee_recipient_person, _) = people();
let test = scenario();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin);{
native_coin_witness_v2::test_init(ctx(&mut test));
};
// register native asset type with the token bridge
next_tx(&mut test, admin);{
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS_V2>>(&test);
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
bridge_state::register_native_asset<NATIVE_COIN_WITNESS_V2>(
&mut worm_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS_V2>>(coin_meta);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
};
// create a treasury cap for the native asset type, mint some tokens,
// and deposit the native tokens into the token bridge
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let t_cap = take_shared<coin::TreasuryCap<NATIVE_COIN_WITNESS_V2>>(&test);
let coins = coin::mint<NATIVE_COIN_WITNESS_V2>(&mut t_cap, 10000000000, ctx(&mut test));
bridge_state::deposit<NATIVE_COIN_WITNESS_V2>(&mut bridge_state, coins);
return_shared<coin::TreasuryCap<NATIVE_COIN_WITNESS_V2>>(t_cap);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
};
// complete transfer, sending native tokens to a recipient address
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS_V2>>(&test);
let to = admin;
let amount = 100;
let fee_amount = 40;
let decimals = 4;
let token_address = external_address::from_bytes(x"01");
let token_chain = wormhole_state::get_chain_id(&worm_state);
let to_chain = wormhole_state::get_chain_id(&worm_state);
let transfer: Transfer = transfer::create(
normalized_amount::normalize(amount, decimals),
token_address,
token_chain,
external_address::from_bytes(bcs::to_bytes(&to)),
to_chain,
normalized_amount::normalize(fee_amount, decimals),
);
complete_transfer::test_complete_transfer<NATIVE_COIN_WITNESS_V2>(
&transfer,
&mut worm_state,
&mut bridge_state,
&coin_meta,
fee_recipient_person,
ctx(&mut test)
);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS_V2>>(coin_meta);
};
// check balances after
next_tx(&mut test, admin);{
let coins = take_from_address<Coin<NATIVE_COIN_WITNESS_V2>>(&test, admin);
assert!(coin::value<NATIVE_COIN_WITNESS_V2>(&coins) == 60, 0);
return_to_address<Coin<NATIVE_COIN_WITNESS_V2>>(admin, coins);
let fee_coins = take_from_address<Coin<NATIVE_COIN_WITNESS_V2>>(&test, fee_recipient_person);
assert!(coin::value<NATIVE_COIN_WITNESS_V2>(&fee_coins) == 40, 0);
return_to_address<Coin<NATIVE_COIN_WITNESS_V2>>(fee_recipient_person, fee_coins);
};
test_scenario::end(test);
}
#[test]
#[expected_failure(abort_code = 4, location=0000000000000000000000000000000000000000::bridge_state)] // E_ORIGIN_CHAIN_MISMATCH
fun test_complete_native_transfer_wrong_origin_chain(){
let (admin, fee_recipient_person, _) = people();
let test = scenario();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin);{
native_coin_witness::test_init(ctx(&mut test));
};
// register native asset type with the token bridge
next_tx(&mut test, admin);{
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
bridge_state::register_native_asset<NATIVE_COIN_WITNESS>(
&mut worm_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
native_coin_witness::test_init(ctx(&mut test));
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
};
// create a treasury cap for the native asset type, mint some tokens,
// and deposit the native tokens into the token bridge
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let t_cap = take_shared<coin::TreasuryCap<NATIVE_COIN_WITNESS>>(&test);
let coins = coin::mint<NATIVE_COIN_WITNESS>(&mut t_cap, 10000000000, ctx(&mut test));
bridge_state::deposit<NATIVE_COIN_WITNESS>(&mut bridge_state, coins);
return_shared<coin::TreasuryCap<NATIVE_COIN_WITNESS>>(t_cap);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
};
// attempt complete transfer
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
let to = admin;
let amount = 1000000000;
let fee_amount = 100000000;
let decimals = 8;
let token_address = external_address::from_bytes(x"01");
let token_chain = u16::from_u64(34); // wrong chain!
let to_chain = wormhole_state::get_chain_id(&worm_state);
let transfer: Transfer = transfer::create(
normalized_amount::normalize(amount, decimals),
token_address,
token_chain,
external_address::from_bytes(bcs::to_bytes(&to)),
to_chain,
normalized_amount::normalize(fee_amount, decimals),
);
complete_transfer::test_complete_transfer<NATIVE_COIN_WITNESS>(
&transfer,
&mut worm_state,
&mut bridge_state,
&coin_meta,
fee_recipient_person,
ctx(&mut test)
);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
};
test_scenario::end(test);
}
#[test]
#[expected_failure(abort_code = 5, location=0000000000000000000000000000000000000000::bridge_state)] // E_ORIGIN_ADDRESS_MISMATCH
fun test_complete_native_transfer_wrong_coin_address(){
let (admin, fee_recipient_person, _) = people();
let test = scenario();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin);{
native_coin_witness::test_init(ctx(&mut test));
};
// register native asset type with the token bridge
next_tx(&mut test, admin);{
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
bridge_state::register_native_asset<NATIVE_COIN_WITNESS>(
&mut worm_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
native_coin_witness::test_init(ctx(&mut test));
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
};
// create a treasury cap for the native asset type, mint some tokens,
// and deposit the native tokens into the token bridge
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let t_cap = take_shared<coin::TreasuryCap<NATIVE_COIN_WITNESS>>(&test);
let coins = coin::mint<NATIVE_COIN_WITNESS>(&mut t_cap, 10000000000, ctx(&mut test));
bridge_state::deposit<NATIVE_COIN_WITNESS>(&mut bridge_state, coins);
return_shared<coin::TreasuryCap<NATIVE_COIN_WITNESS>>(t_cap);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
};
// attempt complete transfer
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
let to = admin;
let amount = 1000000000;
let fee_amount = 100000000;
let decimals = 8;
let token_address = external_address::from_bytes(x"1111"); // wrong address!
let token_chain = wormhole_state::get_chain_id(&worm_state);
let to_chain = wormhole_state::get_chain_id(&worm_state);
let transfer: Transfer = transfer::create(
normalized_amount::normalize(amount, decimals),
token_address,
token_chain,
external_address::from_bytes(bcs::to_bytes(&to)),
to_chain,
normalized_amount::normalize(fee_amount, decimals),
);
complete_transfer::test_complete_transfer<NATIVE_COIN_WITNESS>(
&transfer,
&mut worm_state,
&mut bridge_state,
&coin_meta,
fee_recipient_person,
ctx(&mut test)
);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
};
test_scenario::end(test);
}
#[test]
#[expected_failure(abort_code = 2, location=0000000000000000000000000000000000000002::balance)] // E_TOO_MUCH_FEE
fun test_complete_native_transfer_too_much_fee(){
let (admin, fee_recipient_person, _) = people();
let test = scenario();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin);{
native_coin_witness::test_init(ctx(&mut test));
};
// register native asset type with the token bridge
next_tx(&mut test, admin);{
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
bridge_state::register_native_asset<NATIVE_COIN_WITNESS>(
&mut worm_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
native_coin_witness::test_init(ctx(&mut test));
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
};
// create a treasury cap for the native asset type, mint some tokens,
// and deposit the native tokens into the token bridge
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let t_cap = take_shared<coin::TreasuryCap<NATIVE_COIN_WITNESS>>(&test);
let coins = coin::mint<NATIVE_COIN_WITNESS>(&mut t_cap, 10000000000, ctx(&mut test));
bridge_state::deposit<NATIVE_COIN_WITNESS>(&mut bridge_state, coins);
return_shared<coin::TreasuryCap<NATIVE_COIN_WITNESS>>(t_cap);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
};
// attempt complete transfer
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
let to = admin;
let amount = 1000000000;
let fee_amount = 1000000001; // Too much fee! Can't be greater than amount
let decimals = 8;
let token_address = external_address::from_bytes(x"01");
let token_chain = wormhole_state::get_chain_id(&worm_state);
let to_chain = wormhole_state::get_chain_id(&worm_state);
let transfer: Transfer = transfer::create(
normalized_amount::normalize(amount, decimals),
token_address,
token_chain,
external_address::from_bytes(bcs::to_bytes(&to)),
to_chain,
normalized_amount::normalize(fee_amount, decimals),
);
complete_transfer::test_complete_transfer<NATIVE_COIN_WITNESS>(
&transfer,
&mut worm_state,
&mut bridge_state,
&coin_meta,
fee_recipient_person,
ctx(&mut test)
);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
};
test_scenario::end(test);
}
#[test]
#[expected_failure(abort_code = 1, location=0000000000000000000000000000000000000002::dynamic_field)] // E_WRONG_COIN_TYPE
fun test_complete_native_transfer_wrong_coin(){
let (admin, fee_recipient_person, _) = people();
let test = scenario();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin);{
native_coin_witness::test_init(ctx(&mut test));
};
next_tx(&mut test, admin);{
native_coin_witness_v2::test_init(ctx(&mut test));
};
// register native asset type with the token bridge
next_tx(&mut test, admin);{
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
bridge_state::register_native_asset<NATIVE_COIN_WITNESS>(
&mut worm_state,
&mut bridge_state,
&coin_meta,
ctx(&mut test)
);
native_coin_witness::test_init(ctx(&mut test));
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
};
// create a treasury cap for the native asset type, mint some tokens,
// and deposit the native tokens into the token bridge
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let t_cap = take_shared<coin::TreasuryCap<NATIVE_COIN_WITNESS>>(&test);
let coins = coin::mint<NATIVE_COIN_WITNESS>(&mut t_cap, 10000000000, ctx(&mut test));
bridge_state::deposit<NATIVE_COIN_WITNESS>(&mut bridge_state, coins);
return_shared<coin::TreasuryCap<NATIVE_COIN_WITNESS>>(t_cap);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
};
// attempt complete transfer with wrong coin type (NATIVE_COIN_WITNESS_V2)
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS_V2>>(&test);
let to = admin;
let amount = 1000000000;
let fee_amount = 10000000;
let decimals = 8;
let token_address = external_address::from_bytes(x"01");
let token_chain = wormhole_state::get_chain_id(&worm_state);
let to_chain = wormhole_state::get_chain_id(&worm_state);
let transfer: Transfer = transfer::create(
normalized_amount::normalize(amount, decimals),
token_address,
token_chain,
external_address::from_bytes(bcs::to_bytes(&to)),
to_chain,
normalized_amount::normalize(fee_amount, decimals),
);
complete_transfer::test_complete_transfer<NATIVE_COIN_WITNESS_V2>(
&transfer,
&mut worm_state,
&mut bridge_state,
&coin_meta,
fee_recipient_person,
ctx(&mut test)
);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
return_shared<CoinMetadata<NATIVE_COIN_WITNESS_V2>>(coin_meta);
};
test_scenario::end(test);
}
// the following test is for the "beefface" token from ethereum (chain id = 2),
// which has 8 decimals
#[test]
fun complete_wrapped_transfer_test(){
let (admin, fee_recipient_person, _) = people();
let scenario = scenario();
// First register foreign chain, create wrapped asset, register wrapped asset.
let test = test_register_wrapped_(admin, scenario);
next_tx(&mut test, admin);{
coin_witness::test_init(ctx(&mut test));
};
// Complete transfer of wrapped asset from foreign chain to this chain.
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let coin_meta = take_shared<CoinMetadata<COIN_WITNESS>>(&test);
let to = admin;
let amount = 1000000000;
let fee_amount = 100000000;
let decimals = 8;
let token_address = external_address::from_bytes(x"beefface");
let token_chain = u16::from_u64(2);
let to_chain = wormhole_state::get_chain_id(&worm_state);
let transfer: Transfer = transfer::create(
normalized_amount::normalize(amount, decimals),
token_address,
token_chain,
external_address::from_bytes(bcs::to_bytes(&to)),
to_chain,
normalized_amount::normalize(fee_amount, decimals),
);
complete_transfer::test_complete_transfer<COIN_WITNESS>(
&transfer,
&mut worm_state,
&mut bridge_state,
&coin_meta,
fee_recipient_person,
ctx(&mut test)
);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
return_shared<CoinMetadata<COIN_WITNESS>>(coin_meta);
};
// check balances after
next_tx(&mut test, admin);{
let coins = take_from_address<Coin<COIN_WITNESS>>(&test, admin);
assert!(coin::value<COIN_WITNESS>(&coins) == 900000000, 0);
return_to_address<Coin<COIN_WITNESS>>(admin, coins);
let fee_coins = take_from_address<Coin<COIN_WITNESS>>(&test, fee_recipient_person);
assert!(coin::value<COIN_WITNESS>(&fee_coins) == 100000000, 0);
return_to_address<Coin<COIN_WITNESS>>(fee_recipient_person, fee_coins);
};
test_scenario::end(test);
}
}

View File

@ -0,0 +1,45 @@
/// This module recovers a Diem-style storage model where objects are collected
/// into a heterogeneous global storage, identified by their type.
///
/// Under the hood, it uses dynamic object fields, but set up in a way that the
/// key is derived from the value's type.
module token_bridge::dynamic_set {
use sui::dynamic_object_field as ofield;
use sui::object::{UID};
/// Wrap the value type. Avoids key collisions with other uses of dynamic
/// fields.
struct Wrapper<phantom Value> has copy, drop, store {
}
public fun add<Value: key + store>(
object: &mut UID,
value: Value,
) {
ofield::add(object, Wrapper<Value>{}, value)
}
public fun borrow<Value: key + store>(
object: &UID,
): &Value {
ofield::borrow(object, Wrapper<Value>{})
}
public fun borrow_mut<Value: key + store>(
object: &mut UID,
): &mut Value {
ofield::borrow_mut(object, Wrapper<Value>{})
}
public fun remove<Value: key + store>(
object: &mut UID,
): Value {
ofield::remove(object, Wrapper<Value>{})
}
public fun exists_<Value: key + store>(
object: &UID,
): bool {
ofield::exists_<Wrapper<Value>>(object, Wrapper<Value>{})
}
}

View File

@ -0,0 +1,74 @@
/// Amounts in represented in token bridge VAAs are capped at 8 decimals. This
/// means that any amount that's given as having more decimals is truncated to 8
/// decimals. On the way out, these amount have to be scaled back to the
/// original decimal amount. This module defines `NormalizedAmount`, which
/// represents amounts that have been capped at 8 decimals.
///
/// The functions `normalize` and `denormalize` take care of convertion to/from
/// this type given the original amount's decimals.
module token_bridge::normalized_amount {
use wormhole::cursor::Cursor;
use wormhole::deserialize;
use wormhole::serialize;
struct NormalizedAmount has store, copy, drop {
amount: u64
}
#[test_only]
public fun get_amount(n: NormalizedAmount): u64 {
n.amount
}
public fun normalize(amount: u64, decimals: u8): NormalizedAmount {
if (decimals > 8) {
let n = decimals - 8;
while (n > 0){
amount = amount / 10;
n = n - 1;
}
};
NormalizedAmount { amount }
}
public fun denormalize(amount: NormalizedAmount, decimals: u8): u64 {
let NormalizedAmount { amount } = amount;
if (decimals > 8) {
let n = decimals - 8;
while (n > 0){
amount = amount * 10;
n = n - 1;
}
};
amount
}
public fun deserialize(cur: &mut Cursor<u8>): NormalizedAmount {
// in the VAA wire format, amounts are 32 bytes.
let amount = deserialize::deserialize_u256(cur);
NormalizedAmount { amount: wormhole::myu256::as_u64(amount) }
}
public fun serialize(buf: &mut vector<u8>, e: NormalizedAmount) {
let NormalizedAmount { amount } = e;
serialize::serialize_u256(buf, wormhole::myu256::from_u64(amount))
}
}
#[test_only]
module token_bridge::normalized_amount_test {
use token_bridge::normalized_amount;
#[test]
fun test_normalize_denormalize_amount() {
let a = 12345678910111;
let b = normalized_amount::normalize(a, 9);
let c = normalized_amount::denormalize(b, 9);
assert!(c == 12345678910110, 0);
let x = 12345678910111;
let y = normalized_amount::normalize(x, 5);
let z = normalized_amount::denormalize(y, 5);
assert!(z == x, 0);
}
}

View File

@ -0,0 +1,171 @@
/// The `string32` module defines the `String32` type which represents UTF8
/// encoded strings that are guaranteed to be 32 bytes long, with 0 padding on
/// the right.
module token_bridge::string32 {
use std::string::{Self, String};
use std::option;
use std::vector;
use wormhole::cursor::Cursor;
use wormhole::deserialize;
use wormhole::serialize;
const E_STRING_TOO_LONG: u64 = 0;
/// A `String32` holds a ut8 string which is guaranteed to be 32 bytes long.
struct String32 has copy, drop, store {
string: String
}
spec String32 {
invariant string::length(string) == 32;
}
/// Right-pads a `String` to a `String32` with 0 bytes.
/// Aborts if the string is longer than 32 bytes.
public fun right_pad(s: &String): String32 {
let length = string::length(s);
assert!(length <= 32, E_STRING_TOO_LONG);
let string = *string::bytes(s);
let zeros = 32 - length;
while ({
spec {
invariant zeros + vector::length(string) == 32;
};
zeros > 0
}) {
vector::push_back(&mut string, 0);
zeros = zeros - 1;
};
String32 { string: string::utf8(string) }
}
/// Internal function to take the first 32 bytes of a byte sequence and
/// convert to a utf8 `String`.
/// Takes the longest prefix that's valid utf8 and maximum 32 bytes.
///
/// Even if the input is valid utf8, the result might be shorter than 32
/// bytes, because the original string might have a multi-byte utf8
/// character at the 32 byte boundary, which, when split, results in an
/// invalid code point, so we remove it.
fun take(bytes: vector<u8>, n: u64): String {
while (vector::length(&bytes) > n) {
vector::pop_back(&mut bytes);
};
let utf8 = string::try_utf8(bytes);
while (option::is_none(&utf8)) {
vector::pop_back(&mut bytes);
utf8 = string::try_utf8(bytes);
};
option::extract(&mut utf8)
}
/// Takes the first `n` bytes of a `String`.
///
/// Even if the input string is longer than `n`, the resulting string might
/// be shorter because the original string might have a multi-byte utf8
/// character at the byte boundary, which, when split, results in an invalid
/// code point, so we remove it.
public fun take_utf8(str: String, n: u64): String {
take(*string::bytes(&str), n)
}
/// Truncates or right-pads a `String` to a `String32`.
/// Does not abort.
public fun from_string(s: &String): String32 {
right_pad(&take(*string::bytes(s), 32))
}
/// Truncates or right-pads a byte vector to a `String32`.
/// Does not abort.
public fun from_bytes(b: vector<u8>): String32 {
right_pad(&take(b, 32))
}
/// Converts `String32` to `String`, removing trailing 0s.
public fun to_string(s: &String32): String {
let String32 { string } = s;
let bytes = *string::bytes(string);
// keep dropping the last character while it's 0
while (!vector::is_empty(&bytes) &&
*vector::borrow(&bytes, vector::length(&bytes) - 1) == 0
) {
vector::pop_back(&mut bytes);
};
string::utf8(bytes)
}
/// Converts `String32` to a byte vector of length 32.
public fun to_bytes(s: &String32): vector<u8> {
*string::bytes(&s.string)
}
public fun deserialize(cur: &mut Cursor<u8>): String32 {
let bytes = deserialize::deserialize_vector(cur, 32);
from_bytes(bytes)
}
public fun serialize(buf: &mut vector<u8>, e: String32) {
serialize::serialize_vector(buf, to_bytes(&e))
}
}
#[test_only]
module token_bridge::string32_test {
use std::string;
use std::vector;
use token_bridge::string32;
#[test]
public fun test_right_pad() {
let result = string32::right_pad(&string::utf8(b"hello"));
assert!(string32::to_string(&result) == string::utf8(b"hello"), 0)
}
#[test]
#[expected_failure(abort_code = string32::E_STRING_TOO_LONG)]
public fun test_right_pad_fail() {
let too_long = string::utf8(b"this string is very very very very very very very very very very very very very very very long");
string32::right_pad(&too_long);
}
#[test]
public fun test_from_string_short() {
let result = string32::from_string(&string::utf8(b"hello"));
assert!(string32::to_string(&result) == string::utf8(b"hello"), 0)
}
#[test]
public fun test_from_string_long() {
let long = string32::from_string(&string::utf8(b"this string is very very very very very very very very very very very very very very very long"));
assert!(string32::to_string(&long) == string::utf8(b"this string is very very very ve"), 0)
}
#[test]
public fun test_from_string_weird_utf8() {
let string = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
assert!(vector::length(&string) == 31, 0);
// append the samaritan letter Alaf, a 3-byte utf8 character the move
// parser only allows ascii characters unfortunately (the character
// looks nice)
vector::append(&mut string, x"e0a080");
// it's valid utf8
let string = string::utf8(string);
// string length is bytes, not characters
assert!(string::length(&string) == 34, 0);
let padded = string32::from_string(&string);
// notice that the e0 byte got dropped at the end
assert!(string32::to_string(&padded) == string::utf8(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0)
}
#[test]
public fun test_from_bytes_invalid_utf8() {
// invalid utf8
let bytes = x"e0a0";
let result = string::utf8(b"");
assert!(string32::to_string(&string32::from_bytes(bytes)) == result, 0)
}
}

View File

@ -0,0 +1,229 @@
module token_bridge::register_chain {
use sui::tx_context::TxContext;
use wormhole::myu16::{Self as u16, U16};
use wormhole::cursor;
use wormhole::deserialize;
use wormhole::myvaa::{Self as corevaa};
use wormhole::external_address::{Self, ExternalAddress};
use wormhole::state::{State as WormholeState};
use token_bridge::vaa as token_bridge_vaa;
use token_bridge::bridge_state::{Self as bridge_state, BridgeState};
/// "TokenBridge" (left padded)
const TOKEN_BRIDGE: vector<u8> = x"000000000000000000000000000000000000000000546f6b656e427269646765";
const E_INVALID_MODULE: u64 = 0;
const E_INVALID_ACTION: u64 = 1;
const E_INVALID_TARGET: u64 = 2;
struct RegisterChain has copy, drop {
/// Chain ID
emitter_chain_id: U16,
/// Emitter address. Left-zero-padded if shorter than 32 bytes
emitter_address: ExternalAddress,
}
#[test_only]
public fun parse_payload_test(payload: vector<u8>): RegisterChain {
parse_payload(payload)
}
fun parse_payload(payload: vector<u8>): RegisterChain {
let cur = cursor::cursor_init(payload);
let target_module = deserialize::deserialize_vector(&mut cur, 32);
assert!(target_module == TOKEN_BRIDGE, E_INVALID_MODULE);
let action = deserialize::deserialize_u8(&mut cur);
assert!(action == 0x01, E_INVALID_ACTION);
// TODO(csongor): should we also accept a VAA directly?
// why would a registration VAA target a specific chain?
let target_chain = deserialize::deserialize_u16(&mut cur);
assert!(target_chain == u16::from_u64(0x0), E_INVALID_TARGET);
let emitter_chain_id = deserialize::deserialize_u16(&mut cur);
let emitter_address = external_address::deserialize(&mut cur);
cursor::destroy_empty(cur);
RegisterChain { emitter_chain_id, emitter_address }
}
public entry fun submit_vaa(wormhole_state: &mut WormholeState, bridge_state: &mut BridgeState, vaa: vector<u8>, ctx: &mut TxContext) {
let vaa = corevaa::parse_and_verify(wormhole_state, vaa, ctx);
corevaa::assert_governance(wormhole_state, &vaa);
token_bridge_vaa::replay_protect(bridge_state, &vaa);
let RegisterChain { emitter_chain_id, emitter_address } = parse_payload(corevaa::destroy(vaa));
bridge_state::set_registered_emitter(bridge_state, emitter_chain_id, emitter_address);
}
public fun get_emitter_chain_id(a: &RegisterChain): U16 {
a.emitter_chain_id
}
public fun get_emitter_address(a: &RegisterChain): ExternalAddress {
a.emitter_address
}
}
#[test_only]
module token_bridge::register_chain_test {
use std::option::{Self};
use sui::test_scenario::{Self, Scenario, next_tx, ctx, take_shared, return_shared};
use wormhole::state::{State};
//use wormhole::test_state::{init_wormhole_state};
//use wormhole::wormhole::{Self};
use wormhole::myu16::{Self as u16};
use wormhole::external_address::{Self};
use wormhole::myvaa::{Self as corevaa};
use token_bridge::bridge_state::{Self as bridge_state, BridgeState};
use token_bridge::register_chain::{Self, submit_vaa};
use token_bridge::bridge_state_test::{set_up_wormhole_core_and_token_bridges};
fun scenario(): Scenario { test_scenario::begin(@0x123233) }
fun people(): (address, address, address) { (@0x124323, @0xE05, @0xFACE) }
struct MyCoinType1 {}
/// Registration VAA for the etheruem token bridge 0xdeadbeef
const ETHEREUM_TOKEN_REG: vector<u8> = x"0100000000010015d405c74be6d93c3c33ed6b48d8db70dfb31e0981f8098b2a6c7583083e0c3343d4a1abeb3fc1559674fa067b0c0e2e9de2fafeaecdfeae132de2c33c9d27cc0100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000016911ae00000000000000000000000000000000000000000000546f6b656e427269646765010000000200000000000000000000000000000000000000000000000000000000deadbeef";
/// Another registration VAA for the ethereum token bridge, 0xbeefface
const ETHEREUM_TOKEN_REG_2:vector<u8> = x"01000000000100c2157fa1c14957dff26d891e4ad0d993ad527f1d94f603e3d2bb1e37541e2fbe45855ffda1efc7eb2eb24009a1585fa25a267815db97e4a9d4a5eb31987b5fb40100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000017ca43300000000000000000000000000000000000000000000546f6b656e427269646765010000000200000000000000000000000000000000000000000000000000000000beefface";
/// Registration VAA for the etheruem NFT bridge 0xdeadbeef
const ETHEREUM_NFT_REG: vector<u8> = x"0100000000010066cce2cb12d88c97d4975cba858bb3c35d6430003e97fced46a158216f3ca01710fd16cc394441a08fef978108ed80c653437f43bb2ca039226974d9512298b10000000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000018483540000000000000000000000000000000000000000000000004e4654427269646765010000000200000000000000000000000000000000000000000000000000000000deadbeef";
const ETH_ID: u64 = 2;
#[test]
fun test_parse(){
test_parse_(scenario())
}
#[test]
#[expected_failure(abort_code = 0, location=token_bridge::register_chain)]
fun test_parse_fail(){
test_parse_fail_(scenario())
}
#[test]
fun test_register_chain(){
test_register_chain_(scenario())
}
#[test]
#[expected_failure(abort_code = 0, location=0000000000000000000000000000000000000002::dynamic_field)]
fun test_replay_protect(){
test_replay_protect_(scenario())
}
#[test]
fun test_re_registration(){
test_re_registration_(scenario())
}
public fun test_parse_(test: Scenario) {
let (admin, _, _) = people();
next_tx(&mut test, admin); {
let vaa = corevaa::parse_test(ETHEREUM_TOKEN_REG);
let register_chain = register_chain::parse_payload_test(corevaa::destroy(vaa));
let chain = register_chain::get_emitter_chain_id(&register_chain);
let address = register_chain::get_emitter_address(&register_chain);
assert!(chain == u16::from_u64(ETH_ID), 0);
assert!(address == external_address::from_bytes(x"deadbeef"), 0);
};
test_scenario::end(test);
}
public fun test_parse_fail_(test: Scenario) {
let (admin, _, _) = people();
next_tx(&mut test, admin); {
let vaa = corevaa::parse_test(ETHEREUM_NFT_REG);
// this should fail because it's an NFT registration
let _register_chain = register_chain::parse_payload_test(corevaa::destroy(vaa));
};
test_scenario::end(test);
}
fun test_register_chain_(test: Scenario) {
let (admin, _, _) = people();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin); {
let wormhole_state = take_shared<State>(&test);
let bridge_state = take_shared<BridgeState>(&test);
submit_vaa(&mut wormhole_state, &mut bridge_state, ETHEREUM_TOKEN_REG, ctx(&mut test));
return_shared<State>(wormhole_state);
return_shared<BridgeState>(bridge_state);
};
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let addr = bridge_state::get_registered_emitter(&bridge_state, &u16::from_u64(ETH_ID));
assert!(addr == option::some(external_address::from_bytes(x"deadbeef")), 0);
return_shared<BridgeState>(bridge_state);
};
test_scenario::end(test);
}
public fun test_replay_protect_(test: Scenario) {
let (admin, _, _) = people();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin); {
let wormhole_state = take_shared<State>(&test);
let bridge_state = take_shared<BridgeState>(&test);
// submit vaa (register chain) twice - triggering replay protection
submit_vaa(&mut wormhole_state, &mut bridge_state, ETHEREUM_TOKEN_REG, ctx(&mut test));
submit_vaa(&mut wormhole_state, &mut bridge_state, ETHEREUM_TOKEN_REG, ctx(&mut test));
return_shared<State>(wormhole_state);
return_shared<BridgeState>(bridge_state);
};
test_scenario::end(test);
}
public fun test_re_registration_(test: Scenario) {
// first register chain using ETHEREUM_TOKEN_REG_1
let (admin, _, _) = people();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin); {
let wormhole_state = take_shared<State>(&test);
let bridge_state = take_shared<BridgeState>(&test);
submit_vaa(&mut wormhole_state, &mut bridge_state, ETHEREUM_TOKEN_REG, ctx(&mut test));
return_shared<State>(wormhole_state);
return_shared<BridgeState>(bridge_state);
};
next_tx(&mut test, admin); {
let bridge_state = take_shared<BridgeState>(&test);
let addr = bridge_state::get_registered_emitter(&bridge_state, &u16::from_u64(ETH_ID));
assert!(addr == option::some(external_address::from_bytes(x"deadbeef")), 0);
return_shared<BridgeState>(bridge_state);
};
next_tx(&mut test, admin); {
let wormhole_state = take_shared<State>(&test);
let bridge_state = take_shared<BridgeState>(&test);
// TODO(csongor): we register ethereum again, which overrides the
// previous one. This deviates from other chains (where this is
// rejected), but I think this is the right behaviour.
// Easy to change, should be discussed.
submit_vaa(&mut wormhole_state, &mut bridge_state, ETHEREUM_TOKEN_REG_2, ctx(&mut test));
let address = bridge_state::get_registered_emitter(&bridge_state, &u16::from_u64(ETH_ID));
assert!(address == option::some(external_address::from_bytes(x"beefface")), 0);
return_shared<State>(wormhole_state);
return_shared<BridgeState>(bridge_state);
};
test_scenario::end(test);
}
}

View File

@ -0,0 +1,99 @@
module token_bridge::asset_meta {
use std::vector::{Self};
use wormhole::serialize::{serialize_u8, serialize_u16, serialize_vector};
use wormhole::deserialize::{deserialize_u8, deserialize_u16, deserialize_vector};
use wormhole::cursor::{Self};
use wormhole::myu16::{U16};
use wormhole::external_address::{Self, ExternalAddress};
use token_bridge::string32::{Self, String32};
friend token_bridge::bridge_state;
friend token_bridge::wrapped;
//#[test_only]
//friend token_bridge::wrapped_test;
const E_INVALID_ACTION: u64 = 0;
struct AssetMeta has copy, store, drop {
/// Address of the token. Left-zero-padded if shorter than 32 bytes
token_address: ExternalAddress,
/// Chain ID of the token
token_chain: U16,
/// Number of decimals of the token (big-endian uint256)
decimals: u8,
/// Symbol of the token (UTF-8)
symbol: String32,
/// Name of the token (UTF-8)
name: String32,
}
public fun get_token_address(a: &AssetMeta): ExternalAddress {
a.token_address
}
public fun get_token_chain(a: &AssetMeta): U16 {
a.token_chain
}
public fun get_decimals(a: &AssetMeta): u8 {
a.decimals
}
public fun get_symbol(a: &AssetMeta): String32 {
a.symbol
}
public fun get_name(a: &AssetMeta): String32 {
a.name
}
public(friend) fun create(
token_address: ExternalAddress,
token_chain: U16,
decimals: u8,
symbol: String32,
name: String32,
): AssetMeta {
AssetMeta {
token_address,
token_chain,
decimals,
symbol,
name
}
}
public fun encode(meta: AssetMeta): vector<u8> {
let encoded = vector::empty<u8>();
serialize_u8(&mut encoded, 2);
serialize_vector(&mut encoded, external_address::get_bytes(&meta.token_address));
serialize_u16(&mut encoded, meta.token_chain);
serialize_u8(&mut encoded, meta.decimals);
string32::serialize(&mut encoded, meta.symbol);
string32::serialize(&mut encoded, meta.name);
encoded
}
public fun parse(meta: vector<u8>): AssetMeta {
let cur = cursor::cursor_init(meta);
let action = deserialize_u8(&mut cur);
assert!(action == 2, E_INVALID_ACTION);
let token_address = deserialize_vector(&mut cur, 32);
let token_chain = deserialize_u16(&mut cur);
let decimals = deserialize_u8(&mut cur);
let symbol = string32::deserialize(&mut cur);
let name = string32::deserialize(&mut cur);
cursor::destroy_empty(cur);
AssetMeta {
token_address: external_address::from_bytes(token_address),
token_chain,
decimals,
symbol,
name
}
}
}

View File

@ -0,0 +1,149 @@
module token_bridge::transfer {
use std::vector;
use wormhole::serialize::{
serialize_u8,
serialize_u16,
};
use wormhole::deserialize::{
deserialize_u8,
deserialize_u16,
};
use wormhole::cursor;
use wormhole::external_address::{Self, ExternalAddress};
use wormhole::myu16::U16;
use token_bridge::normalized_amount::{Self, NormalizedAmount};
friend token_bridge::transfer_tokens;
#[test_only]
friend token_bridge::complete_transfer_test;
#[test_only]
friend token_bridge::transfer_test;
const E_INVALID_ACTION: u64 = 0;
struct Transfer has drop {
/// Amount being transferred
amount: NormalizedAmount,
/// Address of the token. Left-zero-padded if shorter than 32 bytes
token_address: ExternalAddress,
/// Chain ID of the token
token_chain: U16,
/// Address of the recipient. Left-zero-padded if shorter than 32 bytes
to: ExternalAddress,
/// Chain ID of the recipient
to_chain: U16,
/// Amount of tokens that the user is willing to pay as relayer fee. Must be <= Amount.
fee: NormalizedAmount,
}
public fun get_amount(a: &Transfer): NormalizedAmount {
a.amount
}
public fun get_token_address(a: &Transfer): ExternalAddress {
a.token_address
}
public fun get_token_chain(a: &Transfer): U16 {
a.token_chain
}
public fun get_to(a: &Transfer): ExternalAddress {
a.to
}
public fun get_to_chain(a: &Transfer): U16 {
a.to_chain
}
public fun get_fee(a: &Transfer): NormalizedAmount {
a.fee
}
public(friend) fun create(
amount: NormalizedAmount,
token_address: ExternalAddress,
token_chain: U16,
to: ExternalAddress,
to_chain: U16,
fee: NormalizedAmount,
): Transfer {
Transfer {
amount,
token_address,
token_chain,
to,
to_chain,
fee,
}
}
public fun parse(transfer: vector<u8>): Transfer {
let cur = cursor::cursor_init(transfer);
let action = deserialize_u8(&mut cur);
assert!(action == 1, E_INVALID_ACTION);
let amount = normalized_amount::deserialize(&mut cur);
let token_address = external_address::deserialize(&mut cur);
let token_chain = deserialize_u16(&mut cur);
let to = external_address::deserialize(&mut cur);
let to_chain = deserialize_u16(&mut cur);
let fee = normalized_amount::deserialize(&mut cur);
cursor::destroy_empty(cur);
Transfer {
amount,
token_address,
token_chain,
to,
to_chain,
fee,
}
}
public fun encode(transfer: Transfer): vector<u8> {
let encoded = vector::empty<u8>();
serialize_u8(&mut encoded, 1);
normalized_amount::serialize(&mut encoded, transfer.amount);
external_address::serialize(&mut encoded, transfer.token_address);
serialize_u16(&mut encoded, transfer.token_chain);
external_address::serialize(&mut encoded, transfer.to);
serialize_u16(&mut encoded, transfer.to_chain);
normalized_amount::serialize(&mut encoded, transfer.fee);
encoded
}
}
#[test_only]
module token_bridge::transfer_test {
use token_bridge::transfer;
use token_bridge::normalized_amount;
use wormhole::external_address;
use wormhole::myu16::{Self as u16};
#[test]
public fun parse_roundtrip() {
let amount = normalized_amount::normalize(100, 8);
let token_address = external_address::from_bytes(x"beef");
let token_chain = u16::from_u64(1);
let to = external_address::from_bytes(x"cafe");
let to_chain = u16::from_u64(7);
let fee = normalized_amount::normalize(50, 8);
let transfer = transfer::create(
amount,
token_address,
token_chain,
to,
to_chain,
fee,
);
let transfer = transfer::parse(transfer::encode(transfer));
assert!(transfer::get_amount(&transfer) == amount, 0);
assert!(transfer::get_token_address(&transfer) == token_address, 0);
assert!(transfer::get_token_chain(&transfer) == token_chain, 0);
assert!(transfer::get_to(&transfer) == to, 0);
assert!(transfer::get_to_chain(&transfer) == to_chain, 0);
assert!(transfer::get_fee(&transfer) == fee, 0);
}
}

View File

@ -0,0 +1,44 @@
module token_bridge::transfer_result {
use wormhole::myu16::U16;
use wormhole::external_address::ExternalAddress;
use token_bridge::normalized_amount::NormalizedAmount;
friend token_bridge::transfer_tokens;
struct TransferResult {
/// Chain ID of the token
token_chain: U16,
/// Address of the token. Left-zero-padded if shorter than 32 bytes
token_address: ExternalAddress,
/// Amount being transferred
normalized_amount: NormalizedAmount,
/// Amount of tokens that the user is willing to pay as relayer fee. Must be <= Amount.
normalized_relayer_fee: NormalizedAmount,
}
public fun destroy(a: TransferResult): (U16, ExternalAddress, NormalizedAmount, NormalizedAmount) {
let TransferResult {
token_chain,
token_address,
normalized_amount,
normalized_relayer_fee
} = a;
(token_chain, token_address, normalized_amount, normalized_relayer_fee)
}
public(friend) fun create(
token_chain: U16,
token_address: ExternalAddress,
normalized_amount: NormalizedAmount,
normalized_relayer_fee: NormalizedAmount,
): TransferResult {
TransferResult {
token_chain,
token_address,
normalized_amount,
normalized_relayer_fee,
}
}
}

View File

@ -0,0 +1,122 @@
module token_bridge::transfer_with_payload {
use std::vector;
use wormhole::serialize::{
serialize_u8,
serialize_u16,
serialize_vector,
};
use wormhole::deserialize::{
deserialize_u8,
deserialize_u16,
};
use wormhole::cursor;
use wormhole::myu16::U16;
use wormhole::external_address::{Self, ExternalAddress};
use token_bridge::normalized_amount::{Self, NormalizedAmount};
friend token_bridge::transfer_tokens;
const E_INVALID_ACTION: u64 = 0;
struct TransferWithPayload has store, drop {
/// Amount being transferred (big-endian uint256)
amount: NormalizedAmount,
/// Address of the token. Left-zero-padded if shorter than 32 bytes
token_address: ExternalAddress,
/// Chain ID of the token
token_chain: U16,
/// Address of the recipient. Left-zero-padded if shorter than 32 bytes
to: ExternalAddress,
/// Chain ID of the recipient
to_chain: U16,
/// Address of the message sender. Left-zero-padded if shorter than 32 bytes
from_address: ExternalAddress,
/// An arbitrary payload
payload: vector<u8>,
}
public fun get_amount(a: &TransferWithPayload): NormalizedAmount {
a.amount
}
public fun get_token_address(a: &TransferWithPayload): ExternalAddress {
a.token_address
}
public fun get_token_chain(a: &TransferWithPayload): U16 {
a.token_chain
}
public fun get_to(a: &TransferWithPayload): ExternalAddress {
a.to
}
public fun get_to_chain(a: &TransferWithPayload): U16 {
a.to_chain
}
public fun get_from_address(a: &TransferWithPayload): ExternalAddress {
a.from_address
}
public fun get_payload(a: &TransferWithPayload): vector<u8> {
a.payload
}
public(friend) fun create(
amount: NormalizedAmount,
token_address: ExternalAddress,
token_chain: U16,
to: ExternalAddress,
to_chain: U16,
from_address: ExternalAddress,
payload: vector<u8>
): TransferWithPayload {
TransferWithPayload {
amount,
token_address,
token_chain,
to,
to_chain,
from_address,
payload,
}
}
public fun encode(transfer: TransferWithPayload): vector<u8> {
let encoded = vector::empty<u8>();
serialize_u8(&mut encoded, 3);
normalized_amount::serialize(&mut encoded, transfer.amount);
external_address::serialize(&mut encoded, transfer.token_address);
serialize_u16(&mut encoded, transfer.token_chain);
external_address::serialize(&mut encoded, transfer.to);
serialize_u16(&mut encoded, transfer.to_chain);
external_address::serialize(&mut encoded, transfer.from_address);
serialize_vector(&mut encoded, transfer.payload);
encoded
}
public fun parse(transfer: vector<u8>): TransferWithPayload {
let cur = cursor::cursor_init(transfer);
let action = deserialize_u8(&mut cur);
assert!(action == 3, E_INVALID_ACTION);
let amount = normalized_amount::deserialize(&mut cur);
let token_address = external_address::deserialize(&mut cur);
let token_chain = deserialize_u16(&mut cur);
let to = external_address::deserialize(&mut cur);
let to_chain = deserialize_u16(&mut cur);
let from_address = external_address::deserialize(&mut cur);
let payload = cursor::rest(cur);
TransferWithPayload {
amount,
token_address,
token_chain,
to,
to_chain,
from_address,
payload
}
}
}

View File

@ -0,0 +1,32 @@
#[test_only]
module token_bridge::native_coin_witness {
use std::option::{Self};
use sui::tx_context::{TxContext};
use sui::coin::{Self};
use sui::transfer::{Self};
struct NATIVE_COIN_WITNESS has drop {}
// This module creates a Sui-native token for testing purposes,
// for example in complete_transfer, where we create a native coin,
// mint some and deposit in the token bridge, then complete transfer
// and ultimately transfer a portion of those native coins to a recipient.
fun init(coin_witness: NATIVE_COIN_WITNESS, ctx: &mut TxContext) {
let (treasury_cap, coin_metadata) = coin::create_currency<NATIVE_COIN_WITNESS>(
coin_witness,
10,
x"00",
x"11",
x"22",
option::none(),
ctx
);
transfer::share_object(coin_metadata);
transfer::share_object(treasury_cap);
}
#[test_only]
public fun test_init(ctx: &mut TxContext) {
init(NATIVE_COIN_WITNESS {}, ctx)
}
}

View File

@ -0,0 +1,32 @@
#[test_only]
module token_bridge::native_coin_witness_v2 {
use std::option::{Self};
use sui::tx_context::{TxContext};
use sui::coin::{Self};
use sui::transfer::{Self};
struct NATIVE_COIN_WITNESS_V2 has drop {}
// This module creates a Sui-native token for testing purposes,
// for example in complete_transfer, where we create a native coin,
// mint some and deposit in the token bridge, then complete transfer
// and ultimately transfer a portion of those native coins to a recipient.
fun init(coin_witness: NATIVE_COIN_WITNESS_V2, ctx: &mut TxContext) {
let (treasury_cap, coin_metadata) = coin::create_currency<NATIVE_COIN_WITNESS_V2>(
coin_witness,
4,
x"33",
x"44",
x"55",
option::none(),
ctx
);
transfer::share_object(coin_metadata);
transfer::share_object(treasury_cap);
}
#[test_only]
public fun test_init(ctx: &mut TxContext) {
init(NATIVE_COIN_WITNESS_V2 {}, ctx)
}
}

View File

@ -0,0 +1,112 @@
#[test_only]
module token_bridge::coin_witness {
use sui::transfer;
use sui::tx_context::{Self, TxContext};
use token_bridge::wrapped;
struct COIN_WITNESS has drop {}
fun init(coin_witness: COIN_WITNESS, ctx: &mut TxContext) {
// Step 1. Paste token attestation VAA below. This example is ethereum beefface token.
let vaa_bytes = x"0100000000010080366065746148420220f25a6275097370e8db40984529a6676b7a5fc9feb11755ec49ca626b858ddfde88d15601f85ab7683c5f161413b0412143241c700aff010000000100000001000200000000000000000000000000000000000000000000000000000000deadbeef000000000150eb23000200000000000000000000000000000000000000000000000000000000beefface00020c424545460000000000000000000000000000000000000000000000000000000042656566206661636520546f6b656e0000000000000000000000000000000000";
let new_wrapped_coin = wrapped::create_wrapped_coin(vaa_bytes, coin_witness, ctx);
transfer::transfer(
new_wrapped_coin,
tx_context::sender(ctx)
);
}
#[test_only]
public fun test_init(ctx: &mut TxContext) {
init(COIN_WITNESS {}, ctx)
}
}
#[test_only]
module token_bridge::coin_witness_test {
use sui::test_scenario::{Self, Scenario, ctx, next_tx, take_from_address, return_shared, take_shared};
use wormhole::state::{State};
use wormhole::myu16::{Self as u16};
use wormhole::external_address::{Self};
use token_bridge::bridge_state::{BridgeState, is_wrapped_asset, is_registered_native_asset, origin_info, get_token_chain_from_origin_info, get_token_address_from_origin_info};
use token_bridge::bridge_state_test::{set_up_wormhole_core_and_token_bridges};
use token_bridge::wrapped::{NewWrappedCoin, register_wrapped_coin};
use token_bridge::register_chain::{submit_vaa};
use token_bridge::coin_witness::{test_init, COIN_WITNESS};
fun scenario(): Scenario { test_scenario::begin(@0x123233) }
fun people(): (address, address, address) { (@0x124323, @0xE05, @0xFACE) }
/// Registration VAA for the etheruem token bridge 0xdeadbeef
const ETHEREUM_TOKEN_REG: vector<u8> = x"0100000000010015d405c74be6d93c3c33ed6b48d8db70dfb31e0981f8098b2a6c7583083e0c3343d4a1abeb3fc1559674fa067b0c0e2e9de2fafeaecdfeae132de2c33c9d27cc0100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000016911ae00000000000000000000000000000000000000000000546f6b656e427269646765010000000200000000000000000000000000000000000000000000000000000000deadbeef";
// call coin init to create wrapped coin and traasfer to sender
#[test]
fun test_create_wrapped() {
let test = scenario();
let (admin, _, _) = people();
next_tx(&mut test, admin); {
test_init(ctx(&mut test))
};
test_scenario::end(test);
}
// call token bridge register wrapped coin
#[test]
fun test_register_wrapped() {
let (admin, _, _) = people();
let scenario = scenario();
let test = test_register_wrapped_(admin, scenario);
test_scenario::end(test);
}
public fun test_register_wrapped_(admin: address, test: Scenario): Scenario {
test = set_up_wormhole_core_and_token_bridges(admin, test);
// create and transfer new wrapped coin to sender
next_tx(&mut test, admin); {
test_init(ctx(&mut test))
};
// register chain
next_tx(&mut test, admin); {
let wormhole_state = take_shared<State>(&test);
let bridge_state = take_shared<BridgeState>(&test);
submit_vaa(&mut wormhole_state, &mut bridge_state, ETHEREUM_TOKEN_REG, ctx(&mut test));
return_shared<State>(wormhole_state);
return_shared<BridgeState>(bridge_state);
};
// register wrapped coin with token bridge, handing it the treasury cap and storing metadata
next_tx(&mut test, admin);{
let bridge_state = take_shared<BridgeState>(&test);
let worm_state = take_shared<State>(&test);
let wrapped_coin = take_from_address<NewWrappedCoin<COIN_WITNESS>>(&test, admin);
register_wrapped_coin<COIN_WITNESS>(
&mut worm_state,
&mut bridge_state,
wrapped_coin,
ctx(&mut test)
);
// assert that wrapped asset is indeed recognized by token bridge
let is_wrapped = is_wrapped_asset<COIN_WITNESS>(&bridge_state);
assert!(is_wrapped, 0);
// assert that wrapped asset is not recognized as a native asset by token bridge
let is_native = is_registered_native_asset<COIN_WITNESS>(&bridge_state);
assert!(!is_native, 0);
// assert origin info is correct
let origin_info = origin_info<COIN_WITNESS>(&bridge_state);
let chain = get_token_chain_from_origin_info(&origin_info);
let address = get_token_address_from_origin_info(&origin_info);
assert!(chain == u16::from_u64(2), 0);
assert!(address == external_address::from_bytes(x"beefface"), 0);
return_shared<BridgeState>(bridge_state);
return_shared<State>(worm_state);
};
return test
}
}

View File

@ -0,0 +1,129 @@
module token_bridge::transfer_tokens {
use sui::sui::SUI;
use sui::coin::{Self, Coin, CoinMetadata};
use wormhole::state::{State as WormholeState};
use wormhole::external_address::{Self, ExternalAddress};
use wormhole::myu16::{Self as u16, U16};
use wormhole::emitter::{Self, EmitterCapability};
use token_bridge::bridge_state::{Self, BridgeState};
use token_bridge::transfer_result::{Self, TransferResult};
use token_bridge::transfer::{Self};
use token_bridge::normalized_amount::{Self};
use token_bridge::transfer_with_payload::{Self};
const E_TOO_MUCH_RELAYER_FEE: u64 = 0;
public entry fun transfer_tokens<CoinType>(
wormhole_state: &mut WormholeState,
bridge_state: &mut BridgeState,
coins: Coin<CoinType>,
coin_metadata: &CoinMetadata<CoinType>,
wormhole_fee_coins: Coin<SUI>,
recipient_chain: u64,
recipient: vector<u8>,
relayer_fee: u64,
nonce: u64,
) {
let result = transfer_tokens_internal<CoinType>(
bridge_state,
coins,
coin_metadata,
relayer_fee,
);
let (token_chain, token_address, normalized_amount, normalized_relayer_fee)
= transfer_result::destroy(result);
let transfer = transfer::create(
normalized_amount,
token_address,
token_chain,
external_address::from_bytes(recipient),
u16::from_u64(recipient_chain),
normalized_relayer_fee,
);
bridge_state::publish_message(
wormhole_state,
bridge_state,
nonce,
transfer::encode(transfer),
wormhole_fee_coins,
);
}
public fun transfer_tokens_with_payload<CoinType>(
emitter_cap: &EmitterCapability,
wormhole_state: &mut WormholeState,
bridge_state: &mut BridgeState,
coins: Coin<CoinType>,
coin_metadata: &CoinMetadata<CoinType>,
wormhole_fee_coins: Coin<SUI>,
recipient_chain: U16,
recipient: ExternalAddress,
relayer_fee: u64,
nonce: u64,
payload: vector<u8>,
): u64 {
let result = transfer_tokens_internal<CoinType>(
bridge_state,
coins,
coin_metadata,
relayer_fee,
);
let (token_chain, token_address, normalized_amount, _)
= transfer_result::destroy(result);
let transfer = transfer_with_payload::create(
normalized_amount,
token_address,
token_chain,
recipient,
recipient_chain,
emitter::get_external_address(emitter_cap),
payload
);
let payload = transfer_with_payload::encode(transfer);
bridge_state::publish_message(
wormhole_state,
bridge_state,
nonce,
payload,
wormhole_fee_coins
)
}
fun transfer_tokens_internal<CoinType>(
bridge_state: &mut BridgeState,
coins: Coin<CoinType>,
coin_metadata: &CoinMetadata<CoinType>,
relayer_fee: u64,
): TransferResult {
let amount = coin::value<CoinType>(&coins);
assert!(relayer_fee <= amount, E_TOO_MUCH_RELAYER_FEE);
if (bridge_state::is_wrapped_asset<CoinType>(bridge_state)) {
// now we burn the wrapped coins to remove them from circulation
bridge_state::burn<CoinType>(bridge_state, coins);
} else {
// deposit native assets. this call to deposit requires the native
// asset to have been attested
bridge_state::deposit<CoinType>(bridge_state, coins);
};
let origin_info = bridge_state::origin_info<CoinType>(bridge_state);
let token_chain = bridge_state::get_token_chain_from_origin_info(&origin_info);
let token_address = bridge_state::get_token_address_from_origin_info(&origin_info);
let decimals = coin::get_decimals(coin_metadata);
let normalized_amount = normalized_amount::normalize(amount, decimals);
let normalized_relayer_fee = normalized_amount::normalize(relayer_fee, decimals);
let transfer_result: TransferResult = transfer_result::create(
token_chain,
token_address,
normalized_amount,
normalized_relayer_fee,
);
transfer_result
}
}

View File

@ -0,0 +1,221 @@
/// Token Bridge VAA utilities
module token_bridge::vaa {
use std::option;
use sui::tx_context::{TxContext};
use wormhole::myvaa::{Self as corevaa, VAA};
use wormhole::state::{State as WormholeState};
use wormhole::external_address::{ExternalAddress};
use token_bridge::bridge_state::{Self as bridge_state, BridgeState};
//friend token_bridge::contract_upgrade;
friend token_bridge::register_chain;
friend token_bridge::wrapped;
friend token_bridge::complete_transfer;
#[test_only]
friend token_bridge::token_bridge_vaa_test;
/// We have no registration for this chain
const E_UNKNOWN_CHAIN: u64 = 0;
/// We have a registration, but it's different from what's given
const E_UNKNOWN_EMITTER: u64 = 1;
/// Aborts if the VAA has already been consumed. Marks the VAA as consumed
/// the first time around.
public(friend) fun replay_protect(bridge_state: &mut BridgeState, vaa: &VAA) {
// this calls set::add which aborts if the element already exists
bridge_state::store_consumed_vaa(bridge_state, corevaa::get_hash(vaa));
}
/// Asserts that the VAA is from a known token bridge.
public fun assert_known_emitter(state: &BridgeState, vm: &VAA) {
let maybe_emitter = bridge_state::get_registered_emitter(state, &corevaa::get_emitter_chain(vm));
assert!(option::is_some<ExternalAddress>(&maybe_emitter), E_UNKNOWN_CHAIN);
let emitter = option::extract(&mut maybe_emitter);
assert!(emitter == corevaa::get_emitter_address(vm), E_UNKNOWN_EMITTER);
}
/// Parses, verifies, and replay protects a token bridge VAA.
/// Aborts if the VAA is not from a known token bridge emitter.
///
/// Has a 'friend' visibility so that it's only callable by the token bridge
/// (otherwise the replay protection could be abused to DoS the bridge)
public(friend) fun parse_verify_and_replay_protect(
wormhole_state: &mut WormholeState,
bridge_state: &mut BridgeState,
vaa: vector<u8>,
ctx: &mut TxContext
): VAA {
let vaa = parse_and_verify(wormhole_state, bridge_state, vaa, ctx);
replay_protect(bridge_state, &vaa);
vaa
}
/// Parses, and verifies a token bridge VAA.
/// Aborts if the VAA is not from a known token bridge emitter.
public fun parse_and_verify(wormhole_state: &mut WormholeState, bridge_state: &BridgeState, vaa: vector<u8>, ctx:&mut TxContext): VAA {
let vaa = corevaa::parse_and_verify(wormhole_state, vaa, ctx);
assert_known_emitter(bridge_state, &vaa);
vaa
}
}
#[test_only]
module token_bridge::token_bridge_vaa_test{
use sui::test_scenario::{Self, Scenario, next_tx, ctx, take_shared, return_shared};
use wormhole::state::{State};
use wormhole::myvaa::{Self as corevaa};
use wormhole::myu16::{Self as u16};
use wormhole::external_address::{Self};
use token_bridge::bridge_state::{Self, BridgeState};
use token_bridge::vaa::{Self};
use token_bridge::bridge_state_test::{set_up_wormhole_core_and_token_bridges};
fun scenario(): Scenario { test_scenario::begin(@0x123233) }
fun people(): (address, address, address) { (@0x124323, @0xE05, @0xFACE) }
/// VAA sent from the ethereum token bridge 0xdeadbeef
const VAA: vector<u8> = x"01000000000100102d399190fa61daccb11c2ea4f7a3db3a9365e5936bcda4cded87c1b9eeb095173514f226256d5579af71d4089eb89496befb998075ba94cd1d4460c5c57b84000000000100000001000200000000000000000000000000000000000000000000000000000000deadbeef0000000002634973000200000000000000000000000000000000000000000000000000000000beefface00020c0000000000000000000000000000000000000000000000000000000042454546000000000000000000000000000000000042656566206661636520546f6b656e";
#[test]
#[expected_failure(abort_code = vaa::E_UNKNOWN_CHAIN)]
fun test_unknown_chain() {
let (admin, _, _) = people();
let test = scenario();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin); {
let state = take_shared<BridgeState>(&test);
let w_state = take_shared<State>(&test);
let vaa = vaa::parse_verify_and_replay_protect(&mut w_state, &mut state, VAA, ctx(&mut test));
corevaa::destroy(vaa);
return_shared<BridgeState>(state);
return_shared<State>(w_state);
};
test_scenario::end(test);
}
#[test]
#[expected_failure(abort_code = vaa::E_UNKNOWN_EMITTER)]
fun test_unknown_emitter() {
let (admin, _, _) = people();
let test = scenario();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin); {
let state = take_shared<BridgeState>(&test);
bridge_state::set_registered_emitter(
&mut state,
u16::from_u64(2),
external_address::from_bytes(x"deadbeed"), // not deadbeef
);
return_shared<BridgeState>(state);
};
next_tx(&mut test, admin); {
let state = take_shared<BridgeState>(&test);
let w_state = take_shared<State>(&test);
let vaa = vaa::parse_verify_and_replay_protect(&mut w_state, &mut state, VAA, ctx(&mut test));
corevaa::destroy(vaa);
return_shared<BridgeState>(state);
return_shared<State>(w_state);
};
test_scenario::end(test);
}
#[test]
fun test_known_emitter() {
let (admin, _, _) = people();
let test = scenario();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin); {
let state = take_shared<BridgeState>(&test);
bridge_state::set_registered_emitter(
&mut state,
u16::from_u64(2),
external_address::from_bytes(x"deadbeef"),
);
return_shared<BridgeState>(state);
};
next_tx(&mut test, admin); {
let state = take_shared<BridgeState>(&test);
let w_state = take_shared<State>(&test);
let vaa = vaa::parse_verify_and_replay_protect(&mut w_state, &mut state, VAA, ctx(&mut test));
corevaa::destroy(vaa);
return_shared<BridgeState>(state);
return_shared<State>(w_state);
};
test_scenario::end(test);
}
#[test]
#[expected_failure(abort_code = 0, location=0000000000000000000000000000000000000002::dynamic_field)]
fun test_replay_protection_works() {
let (admin, _, _) = people();
let test = scenario();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin); {
let state = take_shared<BridgeState>(&test);
bridge_state::set_registered_emitter(
&mut state,
u16::from_u64(2),
external_address::from_bytes(x"deadbeef"),
);
return_shared<BridgeState>(state);
};
next_tx(&mut test, admin); {
let state = take_shared<BridgeState>(&test);
let w_state = take_shared<State>(&test);
// try to use the VAA twice
let vaa = vaa::parse_verify_and_replay_protect(&mut w_state, &mut state, VAA, ctx(&mut test));
corevaa::destroy(vaa);
let vaa = vaa::parse_verify_and_replay_protect(&mut w_state, &mut state, VAA, ctx(&mut test));
corevaa::destroy(vaa);
return_shared<BridgeState>(state);
return_shared<State>(w_state);
};
test_scenario::end(test);
}
#[test]
fun test_can_verify_after_replay_protect() {
let (admin, _, _) = people();
let test = scenario();
test = set_up_wormhole_core_and_token_bridges(admin, test);
next_tx(&mut test, admin); {
let state = take_shared<BridgeState>(&test);
bridge_state::set_registered_emitter(
&mut state,
u16::from_u64(2),
external_address::from_bytes(x"deadbeef"),
);
return_shared<BridgeState>(state);
};
next_tx(&mut test, admin); {
let state = take_shared<BridgeState>(&test);
let w_state = take_shared<State>(&test);
// parse and verify and replay protect VAA the first time, don't replay protect the second time
let vaa = vaa::parse_verify_and_replay_protect(&mut w_state, &mut state, VAA, ctx(&mut test));
corevaa::destroy(vaa);
let vaa = vaa::parse_and_verify(&mut w_state, &mut state, VAA, ctx(&mut test));
corevaa::destroy(vaa);
return_shared<BridgeState>(state);
return_shared<State>(w_state);
};
test_scenario::end(test);
}
}

View File

@ -0,0 +1,132 @@
/// This module uses the one-time witness (OTW)
/// Sui one-time witness pattern reference: https://examples.sui.io/basics/one-time-witness.html
module token_bridge::wrapped {
use std::option::{Self};
use sui::tx_context::{TxContext};
use sui::coin::{TreasuryCap};
use sui::object::{Self, UID};
use sui::coin::{Self};
use sui::url::{Url};
use sui::transfer::{Self};
use token_bridge::bridge_state::{Self, BridgeState};
use token_bridge::asset_meta::{Self, AssetMeta};
use token_bridge::vaa;
use token_bridge::string32::{Self};
use wormhole::state::{Self as state, State as WormholeState};
use wormhole::myvaa as core_vaa;
const E_WRAPPING_NATIVE_COIN: u64 = 0;
const E_WRAPPING_REGISTERED_NATIVE_COIN: u64 = 1;
const E_WRAPPED_COIN_ALREADY_INITIALIZED: u64 = 2;
/// Wrapped assets are created in two steps.
/// 1) The coin is initialised by calling `create_wrapped_coin` in the
/// `init` function of a OTW module.
/// 2) The coin is registered in the token bridge in
/// `register_wrapped_coin`.
///
/// Since Step 1. takes places in an untrusted context, we want to remove
/// all degrees of freedom. To this end, `create_wrapped_coin` just takes a
/// VAA, and returns a `NewWrappedCoin` object. That's the only way to
/// create a `NewWrappedCoin` object. Then this object can be passed to
/// `register_wrapped_coin` in Step 2.
///
/// This setup ensures that we don't have to trust (or verify) that the OTW
/// initialiser did the right thing.
///
/// TODO: it would be nice if we could also enforce that the OTW struct's
/// name matches the token symbol being registered. Currently there's no way
/// to do this in the sui framework.
struct NewWrappedCoin<phantom CoinType> has key, store {
id: UID,
vaa_bytes: vector<u8>,
treasury_cap: TreasuryCap<CoinType>,
}
/// This function will be called from the `init` function of a module that
/// defines a OTW type. Due to the nature of `init` functions, this function
/// must be stateless.
/// This means that it performs no verification of the VAA beyond parsing
/// it. It is the responsbility of `register_wrapped_coin` to perform the
/// validation.
/// This function guarantees that if the VAA is valid, then a new currency
/// `CoinType` will be created such that:
/// 1) the asset metadata matches the VAA
/// 2) the treasury total supply will be 0
///
/// Thanks to the above properties, `register_wrapped_coin` does not need to
/// do any checks other than the VAA in `NewWrappedCoin` is valid.
public fun create_wrapped_coin<CoinType: drop>(
vaa_bytes: vector<u8>,
coin_witness: CoinType,
ctx: &mut TxContext
): NewWrappedCoin<CoinType> {
let payload = core_vaa::parse_and_get_payload(vaa_bytes);
let asset_meta: AssetMeta = asset_meta::parse(payload);
// The amounts in the token bridge payload are truncated to 8 decimals
// in each of the contracts when sending tokens out, so there's no
// precision beyond 10^-8. We could preserve the original number of
// decimals when creating wrapped assets, and "untruncate" the amounts
// on the way out by scaling back appropriately. This is what most other
// chains do, but untruncating from 8 decimals to 18 decimals loses
// log2(10^10) ~ 33 bits of precision, which we cannot afford on Aptos
// (and Solana), as the coin type only has 64bits to begin with.
// Contrast with Ethereum, where amounts are 256 bits.
// So we cap the maximum decimals at 8 when creating a wrapped token.
let max_decimals: u8 = 8;
let parsed_decimals = asset_meta::get_decimals(&asset_meta);
let symbol = asset_meta::get_symbol(&asset_meta);
let name = asset_meta::get_name(&asset_meta);
let decimals = if (max_decimals < parsed_decimals) max_decimals else parsed_decimals;
let (treasury_cap, coin_metadata) = coin::create_currency<CoinType>(
coin_witness,
decimals,
string32::to_bytes(&symbol),
string32::to_bytes(&name),
x"", //empty description
option::none<Url>(), //empty url
ctx
);
transfer::share_object(coin_metadata);
NewWrappedCoin { id: object::new(ctx), vaa_bytes, treasury_cap }
}
public entry fun register_wrapped_coin<CoinType>(
state: &mut WormholeState,
bridge_state: &mut BridgeState,
new_wrapped_coin: NewWrappedCoin<CoinType>,
ctx: &mut TxContext,
) {
let NewWrappedCoin { id, vaa_bytes, treasury_cap } = new_wrapped_coin;
object::delete(id);
let vaa = vaa::parse_verify_and_replay_protect(
state,
bridge_state,
vaa_bytes,
ctx
);
let payload = core_vaa::destroy(vaa);
let metadata = asset_meta::parse(payload);
let origin_chain = asset_meta::get_token_chain(&metadata);
let external_address = asset_meta::get_token_address(&metadata);
let wrapped_asset_info =
bridge_state::create_wrapped_asset_info(
origin_chain,
external_address,
treasury_cap,
ctx
);
assert!(origin_chain != state::get_chain_id(state), E_WRAPPING_NATIVE_COIN);
assert!(!bridge_state::is_registered_native_asset<CoinType>(bridge_state), E_WRAPPING_REGISTERED_NATIVE_COIN);
assert!(!bridge_state::is_wrapped_asset<CoinType>(bridge_state), E_WRAPPED_COIN_ALREADY_INITIALIZED);
bridge_state::register_wrapped_asset<CoinType>(bridge_state, wrapped_asset_info);
}
}

14
sui/wormhole/Makefile Normal file
View File

@ -0,0 +1,14 @@
-include ../../Makefile.help
.PHONY: artifacts
artifacts: build
.PHONY: build
## Build contract
build:
sui move build
.PHONY: test
## Run tests
test:
sui move test

9
sui/wormhole/Move.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "Wormhole"
version = "0.0.1"
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework", rev = "2d709054a08d904b9229a2472af679f210af3827" }
[addresses]
wormhole = "0x0"

25
sui/wormhole/README.md Normal file
View File

@ -0,0 +1,25 @@
# Sui Wormhole Core Bridge Design
## State
The `State` object is created exactly once during the initialisation of the
contract. Normally, run-once functionality is implemented in the special `init`
function of a module (this code runs once, when the module is first deployed),
but this function takes no arguments, while our initialisation code does (to
ease deployment to different environments without recompiling the contract).
To allow configuring the state with arguments, it's initialised in the
`init_and_share_state` function, which also shares the state object. To ensure
this function can only be called once, it consumes a `DeployerCapability` object
which in turn is created and transferred to the deployer in the `init` function.
Since `init_and_share_state` consumes this object, it won't be possible to call
it again.
## Dynamic fields
TODO: up to date notes on where and how we use dynamic fields.
## Epoch Timestamp
Sui currently does not have fine-grained timestamps, so we use
`tx_context::epoch(ctx)` in place of on-chain time in seconds.

View File

@ -0,0 +1,47 @@
module wormhole::cursor {
use std::vector::{Self};
/// A cursor allows consuming a vector incrementally for parsing operations.
/// It has no drop ability, and the only way to deallocate it is by calling the
/// `destroy_empty` method, which will fail if the whole input hasn't been consumed.
///
/// This setup statically guarantees that the parsing methods consume the
/// full input.
struct Cursor<T> {
data: vector<T>,
}
/// Initialises a cursor from a vector.
public fun cursor_init<T>(data: vector<T>): Cursor<T> {
// reverse the array so we have access to the first element easily
vector::reverse(&mut data);
Cursor<T> {
data,
}
}
/// Destroys an empty cursor.
/// Aborts if the cursor is not empty.
public fun destroy_empty<T>(cur: Cursor<T>) {
let Cursor { data } = cur;
vector::destroy_empty(data);
}
/// Consumes the rest of the cursor (thus destroying it) and returns the
/// remaining bytes.
/// NOTE: Only use this function if you intend to consume the rest of the
/// bytes. Since the result is a vector, which can be dropped, it is not
/// possible to statically guarantee that the rest will be used.
public fun rest<T>(cur: Cursor<T>): vector<T> {
let Cursor { data } = cur;
// re-reverse the data so it is in the same order as the original input
vector::reverse(&mut data);
data
}
/// Returns the first element of the cursor and advances it.
public fun poke<T>(cur: &mut Cursor<T>): T {
vector::pop_back(&mut cur.data)
}
}

View File

@ -0,0 +1,138 @@
module wormhole::deserialize {
use std::vector::{Self};
use wormhole::cursor::{Self, Cursor};
use wormhole::myu16::{Self as u16, U16};
use wormhole::myu32::{Self as u32, U32};
use wormhole::myu256::{Self as u256, U256};
public fun deserialize_u8(cur: &mut Cursor<u8>): u8 {
cursor::poke(cur)
}
public fun deserialize_u16(cur: &mut Cursor<u8>): U16 {
let res: u64 = 0;
let i = 0;
while (i < 2) {
let b = cursor::poke(cur);
res = (res << 8) + (b as u64);
i = i + 1;
};
u16::from_u64(res)
}
public fun deserialize_u32(cur: &mut Cursor<u8>): U32 {
let res: u64 = 0;
let i = 0;
while (i < 4) {
let b = cursor::poke(cur);
res = (res << 8) + (b as u64);
i = i + 1;
};
u32::from_u64(res)
}
public fun deserialize_u64(cur: &mut Cursor<u8>): u64 {
let res: u64 = 0;
let i = 0;
while (i < 8) {
let b = cursor::poke(cur);
res = (res << 8) + (b as u64);
i = i + 1;
};
res
}
public fun deserialize_u128(cur: &mut Cursor<u8>): u128 {
let res: u128 = 0;
let i = 0;
while (i < 16) {
let b = cursor::poke(cur);
res = (res << 8) + (b as u128);
i = i + 1;
};
res
}
public fun deserialize_u256(cur: &mut Cursor<u8>): U256 {
let v0 = deserialize_u128(cur);
let v1 = deserialize_u128(cur);
u256::add(u256::shl(u256::from_u128(v0), 128), u256::from_u128(v1))
}
public fun deserialize_vector(cur: &mut Cursor<u8>, len: u64): vector<u8> {
let result = vector::empty();
while (len > 0) {
vector::push_back(&mut result, cursor::poke(cur));
len = len - 1;
};
result
}
}
#[test_only]
module wormhole::deserialize_test {
use wormhole::cursor;
use wormhole::myu16::{Self as u16};
use wormhole::myu32::{Self as u32};
use wormhole::deserialize::{
deserialize_u8,
deserialize_u16,
deserialize_u32,
deserialize_u64,
deserialize_u128,
deserialize_vector,
};
#[test]
fun test_deserialize_u8() {
let cur = cursor::cursor_init(x"99");
let byte = deserialize_u8(&mut cur);
assert!(byte==0x99, 0);
cursor::destroy_empty(cur);
}
#[test]
fun test_deserialize_u16() {
let cur = cursor::cursor_init(x"9987");
let u = deserialize_u16(&mut cur);
assert!(u == u16::from_u64(0x9987), 0);
cursor::destroy_empty(cur);
}
#[test]
fun test_deserialize_u32() {
let cur = cursor::cursor_init(x"99876543");
let u = deserialize_u32(&mut cur);
assert!(u == u32::from_u64(0x99876543), 0);
cursor::destroy_empty(cur);
}
#[test]
fun test_deserialize_u64() {
let cur = cursor::cursor_init(x"1300000025000001");
let u = deserialize_u64(&mut cur);
assert!(u==0x1300000025000001, 0);
cursor::destroy_empty(cur);
}
#[test]
fun test_deserialize_u128() {
let cur = cursor::cursor_init(x"130209AB2500FA0113CD00AE25000001");
let u = deserialize_u128(&mut cur);
assert!(u==0x130209AB2500FA0113CD00AE25000001, 0);
cursor::destroy_empty(cur);
}
#[test]
fun test_deserialize_vector() {
let cur = cursor::cursor_init(b"hello world");
let hello = deserialize_vector(&mut cur, 5);
deserialize_u8(&mut cur);
let world = deserialize_vector(&mut cur, 5);
assert!(hello == b"hello", 0);
assert!(world == b"world", 0);
cursor::destroy_empty(cur);
}
}

View File

@ -0,0 +1,99 @@
module wormhole::emitter {
use sui::object::{Self, UID};
use sui::tx_context::{TxContext};
use wormhole::serialize;
use wormhole::external_address::{Self, ExternalAddress};
friend wormhole::state;
friend wormhole::wormhole;
#[test_only]
friend wormhole::emitter_test;
struct EmitterRegistry has store {
next_id: u64
}
// TODO(csongor): document that this has to be globally unique.
// The friend modifier is very important here.
public(friend) fun init_emitter_registry(): EmitterRegistry {
EmitterRegistry { next_id: 1 }
}
#[test_only]
public fun destroy_emitter_registry(registry: EmitterRegistry) {
let EmitterRegistry { next_id: _ } = registry;
}
public(friend) fun new_emitter(
registry: &mut EmitterRegistry,
ctx: &mut TxContext
): EmitterCapability {
let emitter = registry.next_id;
registry.next_id = emitter + 1;
EmitterCapability {
id: object::new(ctx),
emitter: emitter,
sequence: 0
}
}
struct EmitterCapability has key, store {
id: UID,
/// Unique identifier of the emitter
emitter: u64,
/// Sequence number of the next wormhole message
sequence: u64
}
/// Destroys an emitter capability.
///
/// Note that this operation removes the ability to send messages using the
/// emitter id, and is irreversible.
public fun destroy_emitter_cap(emitter_cap: EmitterCapability) {
let EmitterCapability {id: id, emitter: _, sequence: _ } = emitter_cap;
object::delete(id);
}
public fun get_emitter(emitter_cap: &EmitterCapability): u64 {
emitter_cap.emitter
}
/// Returns the external address of the emitter.
///
/// The 16 byte (u128) emitter id left-padded to u256
public fun get_external_address(emitter_cap: &EmitterCapability): ExternalAddress {
let emitter_bytes = vector<u8>[];
serialize::serialize_u64(&mut emitter_bytes, emitter_cap.emitter);
external_address::from_bytes(emitter_bytes)
}
public(friend) fun use_sequence(emitter_cap: &mut EmitterCapability): u64 {
let sequence = emitter_cap.sequence;
emitter_cap.sequence = sequence + 1;
sequence
}
}
#[test_only]
module wormhole::emitter_test {
use wormhole::emitter;
use sui::tx_context;
#[test]
public fun test_increasing_emitters() {
let ctx = tx_context::dummy();
let registry = emitter::init_emitter_registry();
let emitter1 = emitter::new_emitter(&mut registry, &mut ctx);
let emitter2 = emitter::new_emitter(&mut registry, &mut ctx);
assert!(emitter::get_emitter(&emitter1) == 1, 0);
assert!(emitter::get_emitter(&emitter2) == 2, 0);
emitter::destroy_emitter_cap(emitter1);
emitter::destroy_emitter_cap(emitter2);
emitter::destroy_emitter_registry(registry);
}
}

View File

@ -0,0 +1,166 @@
/// 32 byte, left-padded address representing an arbitrary address, to be used in VAAs to
/// refer to addresses.
module wormhole::external_address {
use std::vector;
use sui::address;
use wormhole::cursor::Cursor;
use wormhole::deserialize;
use wormhole::serialize;
const E_VECTOR_TOO_LONG: u64 = 0;
const E_INVALID_EXTERNAL_ADDRESS: u64 = 1;
struct ExternalAddress has drop, copy, store {
external_address: vector<u8>,
}
public fun get_bytes(e: &ExternalAddress): vector<u8> {
e.external_address
}
public fun pad_left_32(input: &vector<u8>): vector<u8>{
let len = vector::length<u8>(input);
assert!(len <= 32, E_VECTOR_TOO_LONG);
let ret = vector::empty<u8>();
let zeros_remaining = 32 - len;
while (zeros_remaining > 0){
vector::push_back<u8>(&mut ret, 0);
zeros_remaining = zeros_remaining - 1;
};
vector::append<u8>(&mut ret, *input);
ret
}
public fun left_pad(s: &vector<u8>): ExternalAddress {
let padded_vector = pad_left_32(s);
ExternalAddress { external_address: padded_vector}
}
public fun from_bytes(bytes: vector<u8>): ExternalAddress {
left_pad(&bytes)
}
public fun deserialize(cur: &mut Cursor<u8>): ExternalAddress {
let bytes = deserialize::deserialize_vector(cur, 32);
from_bytes(bytes)
}
public fun serialize(buf: &mut vector<u8>, e: ExternalAddress) {
serialize::serialize_vector(buf, e.external_address)
}
/// Convert an `ExternalAddress` to a native Sui address.
///
/// Sui addresses are 20 bytes, while external addresses are represented as
/// 32 bytes, left-padded with 0s. This function thus takes the last 20
/// bytes of an external address, and reverts if the first 12 bytes contain
/// non-0 bytes.
public fun to_address(e: &ExternalAddress): address {
let vec = e.external_address;
// we reverse the vector and drop the last 12 bytes
vector::reverse(&mut vec);
let bytes_to_drop = 12;
while (bytes_to_drop > 0) {
let last_byte = vector::pop_back(&mut vec);
// ensure no junk in the first 12 bytes
assert!(last_byte == 0, E_INVALID_EXTERNAL_ADDRESS);
bytes_to_drop = bytes_to_drop - 1;
};
// reverse back to original order
vector::reverse(&mut vec);
address::from_bytes(vec)
}
}
#[test_only]
module wormhole::external_address_test {
use wormhole::external_address;
use std::vector::{Self};
// test get_bytes and left_pad
#[test]
public fun test_left_pad() {
let v = x"123456789123456789123456789123451234567891234567891234"; // less than 32 bytes
let res = external_address::left_pad(&v);
let bytes = external_address::get_bytes(&res);
let m = x"0000000000";
vector::append(&mut m, v);
assert!(bytes == m, 0);
}
#[test]
public fun test_left_pad_length_32_vector() {
let v = x"1234567891234567891234567891234512345678912345678912345678912345"; //32 bytes
let res = external_address::left_pad(&v);
let bytes = external_address::get_bytes(&res);
assert!(bytes == v, 0);
}
#[test]
#[expected_failure(abort_code = 0, location=wormhole::external_address)]
public fun test_left_pad_vector_too_long() {
let v = x"123456789123456789123456789123451234567891234567891234567891234500"; //33 bytes
let res = external_address::left_pad(&v);
let bytes = external_address::get_bytes(&res);
assert!(bytes == v, 0);
}
#[test]
public fun test_from_bytes() {
let v = x"1234";
let ea = external_address::from_bytes(v);
let bytes = external_address::get_bytes(&ea);
let w = x"000000000000000000000000000000000000000000000000000000000000";
vector::append(&mut w, v);
assert!(bytes == w, 0);
}
#[test]
#[expected_failure(abort_code = 0, location=wormhole::external_address)]
public fun test_from_bytes_over_32_bytes() {
let v = x"00000000000000000000000000000000000000000000000000000000000000001234";
let ea = external_address::from_bytes(v);
let _bytes = external_address::get_bytes(&ea);
}
#[test]
fun test_pad_left_short() {
let v = x"11";
let pad_left_v = external_address::pad_left_32(&v);
assert!(pad_left_v == x"0000000000000000000000000000000000000000000000000000000000000011", 0);
}
#[test]
fun test_pad_left_exact() {
let v = x"5555555555555555555555555555555555555555555555555555555555555555";
let pad_left_v = external_address::pad_left_32(&v);
assert!(pad_left_v == x"5555555555555555555555555555555555555555555555555555555555555555", 0);
}
#[test]
#[expected_failure(abort_code = 0, location=wormhole::external_address)]
fun test_pad_left_long() {
let v = x"665555555555555555555555555555555555555555555555555555555555555555";
external_address::pad_left_32(&v);
}
#[test]
#[expected_failure(abort_code = 1, location=wormhole::external_address)]
public fun test_to_address_too_long() {
// non-0 bytes in first 12 bytes
let v = x"0000010000000000000000000000000000000000000000000000000000001234";
let res = external_address::from_bytes(v);
let _address = external_address::to_address(&res);
}
#[test]
public fun test_to_address() {
let v = x"0000000000000000000000000000000000000000000000000000000000001234";
let res = external_address::from_bytes(v);
let address = external_address::to_address(&res);
assert!(address == @0x1234, 0);
}
}

View File

@ -0,0 +1,82 @@
/// Guardian keys are EVM-style 20 byte addresses
/// That is, they are computed by taking the last 20 bytes of the keccak256
/// hash of their 64 byte secp256k1 public key.
module wormhole::guardian_pubkey {
use sui::ecdsa_k1::{Self as ecdsa};
use std::vector;
use wormhole::keccak256::keccak256;
/// An error occurred while deserializing, for example due to wrong input size.
const E_DESERIALIZE: u64 = 1;
struct Address has store, drop, copy {
bytes: vector<u8>
}
/// Deserializes a raw byte sequence into an address.
/// Aborts if the input is not 20 bytes long.
public fun from_bytes(bytes: vector<u8>): Address {
assert!(std::vector::length(&bytes) == 20, E_DESERIALIZE);
Address { bytes }
}
/// Computes the address from a 64 byte public key.
public fun from_pubkey(pubkey: vector<u8>): Address {
assert!(std::vector::length(&pubkey) == 64, E_DESERIALIZE);
let hash = keccak256(pubkey);
let address = vector::empty<u8>();
let i = 0;
while (i < 20) {
vector::push_back(&mut address, vector::pop_back(&mut hash));
i = i + 1;
};
vector::reverse(&mut address);
Address { bytes: address }
}
/// Recovers the address from a signature and message.
/// This is known as 'ecrecover' in EVM.
public fun from_signature(
message: vector<u8>,
recovery_id: u8,
sig: vector<u8>,
): Address {
// sui's ecrecover function takes a 65 byte array (signature + recovery byte)
vector::push_back(&mut sig, recovery_id);
let pubkey = ecdsa::ecrecover(&sig, &message);
let pubkey = ecdsa::decompress_pubkey(&pubkey);
// decompress_pubkey returns 65 bytes, the first byte is not relevant to
// us, so we remove it
vector::remove(&mut pubkey, 0);
from_pubkey(pubkey)
}
}
#[test_only]
module wormhole::guardian_pubkey_test {
use wormhole::guardian_pubkey;
#[test]
public fun from_pubkey_test() {
// devnet guardian public key
let pubkey = x"d4a4629979f0c9fa0f0bb54edf33f87c8c5a1f42c0350a30d68f7e967023e34e495a8ebf5101036d0fd66e3b0a8c7c61b65fceeaf487ab3cd1b5b7b50beb7970";
let expected_address = guardian_pubkey::from_bytes(x"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe");
let address = guardian_pubkey::from_pubkey(pubkey);
assert!(address == expected_address, 0);
}
#[test]
public fun from_signature() {
let sig = x"38535089d6eec412a00066f84084212316ee3451145a75591dbd4a1c2a2bff442223f81e58821bfa4e8ffb80a881daf7a37500b04dfa5719fff25ed4cec8dda3";
let msg = x"43f3693ccdcb4400e1d1c5c8cec200153bd4b3d167e5b9fe5400508cf8717880";
let addr = guardian_pubkey::from_signature(msg, 0x01, sig);
let expected_addr = guardian_pubkey::from_bytes(x"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe");
assert!(addr == expected_addr, 0);
}
}

View File

@ -0,0 +1,188 @@
module wormhole::guardian_set_upgrade {
use std::vector::{Self};
use sui::tx_context::{TxContext};
use wormhole::deserialize;
use wormhole::cursor::{Self};
use wormhole::myvaa::{Self as vaa};
//use wormhole::myvaa::{Self as vaa};
use wormhole::state::{Self, State};
use wormhole::structs::{
Guardian,
create_guardian,
create_guardian_set
};
use wormhole::myu32::{Self as u32,U32};
use wormhole::myu16::{Self as u16};
const E_WRONG_GUARDIAN_LEN: u64 = 0x0;
const E_NO_GUARDIAN_SET: u64 = 0x1;
const E_INVALID_MODULE: u64 = 0x2;
const E_INVALID_ACTION: u64 = 0x3;
const E_INVALID_TARGET: u64 = 0x4;
const E_NON_INCREMENTAL_GUARDIAN_SETS: u64 = 0x5;
struct GuardianSetUpgrade {
new_index: U32,
guardians: vector<Guardian>,
}
public entry fun submit_vaa(state: &mut State, vaa: vector<u8>, ctx: &mut TxContext) {
let vaa = vaa::parse_and_verify(state, vaa, ctx);
vaa::assert_governance(state, &vaa);
vaa::replay_protect(state, &vaa);
do_upgrade(state, parse_payload(vaa::destroy(vaa)), ctx)
}
fun do_upgrade(state: &mut State, upgrade: GuardianSetUpgrade, ctx: &TxContext) {
let current_index = state::get_current_guardian_set_index(state);
let GuardianSetUpgrade {
new_index,
guardians,
} = upgrade;
assert!(
u32::to_u64(new_index) == u32::to_u64(current_index) + 1,
E_NON_INCREMENTAL_GUARDIAN_SETS
);
state::update_guardian_set_index(state, new_index);
state::store_guardian_set(state, new_index, create_guardian_set(new_index, guardians));
state::expire_guardian_set(state, current_index, ctx);
}
#[test_only]
public fun do_upgrade_test(s: &mut State, new_index: U32, guardians: vector<Guardian>, ctx: &mut TxContext) {
do_upgrade(s, GuardianSetUpgrade { new_index, guardians }, ctx)
}
public fun parse_payload(bytes: vector<u8>): GuardianSetUpgrade {
let cur = cursor::cursor_init(bytes);
let guardians = vector::empty<Guardian>();
let target_module = deserialize::deserialize_vector(&mut cur, 32);
let expected_module = x"00000000000000000000000000000000000000000000000000000000436f7265"; // Core
assert!(target_module == expected_module, E_INVALID_MODULE);
let action = deserialize::deserialize_u8(&mut cur);
assert!(action == 0x02, E_INVALID_ACTION);
let chain = deserialize::deserialize_u16(&mut cur);
assert!(chain == u16::from_u64(0x00), E_INVALID_TARGET);
let new_index = deserialize::deserialize_u32(&mut cur);
let guardian_len = deserialize::deserialize_u8(&mut cur);
while (guardian_len > 0) {
let key = deserialize::deserialize_vector(&mut cur, 20);
vector::push_back(&mut guardians, create_guardian(key));
guardian_len = guardian_len - 1;
};
cursor::destroy_empty(cur);
GuardianSetUpgrade {
new_index: new_index,
guardians: guardians,
}
}
#[test_only]
public fun split(upgrade: GuardianSetUpgrade): (U32, vector<Guardian>) {
let GuardianSetUpgrade { new_index, guardians } = upgrade;
(new_index, guardians)
}
}
#[test_only]
module wormhole::guardian_set_upgrade_test {
use std::vector;
use wormhole::structs::{create_guardian};
use wormhole::guardian_set_upgrade;
use wormhole::myu32::{Self as u32};
use sui::test_scenario::{Self, Scenario, next_tx, take_shared, return_shared, ctx};
use sui::tx_context::{increment_epoch_number};
fun scenario(): Scenario { test_scenario::begin(@0x123233) }
fun people(): (address, address, address) { (@0x124323, @0xE05, @0xFACE) }
#[test]
public fun test_parse_guardian_set_upgrade() {
use wormhole::myu32::{Self as u32};
let b = x"00000000000000000000000000000000000000000000000000000000436f7265020000000000011358cc3ae5c097b213ce3c81979e1b9f9570746aa5ff6cb952589bde862c25ef4392132fb9d4a42157114de8460193bdf3a2fcf81f86a09765f4762fd1107a0086b32d7a0977926a205131d8731d39cbeb8c82b2fd82faed2711d59af0f2499d16e726f6b211b39756c042441be6d8650b69b54ebe715e234354ce5b4d348fb74b958e8966e2ec3dbd4958a7cdeb5f7389fa26941519f0863349c223b73a6ddee774a3bf913953d695260d88bc1aa25a4eee363ef0000ac0076727b35fbea2dac28fee5ccb0fea768eaf45ced136b9d9e24903464ae889f5c8a723fc14f93124b7c738843cbb89e864c862c38cddcccf95d2cc37a4dc036a8d232b48f62cdd4731412f4890da798f6896a3331f64b48c12d1d57fd9cbe7081171aa1be1d36cafe3867910f99c09e347899c19c38192b6e7387ccd768277c17dab1b7a5027c0b3cf178e21ad2e77ae06711549cfbb1f9c7a9d8096e85e1487f35515d02a92753504a8d75471b9f49edb6fbebc898f403e4773e95feb15e80c9a99c8348d";
let (new_index, guardians) = guardian_set_upgrade::split(guardian_set_upgrade::parse_payload(b));
assert!(new_index == u32::from_u64(1), 0);
assert!(vector::length(&guardians) == 19, 0);
let expected = vector[
create_guardian(x"58cc3ae5c097b213ce3c81979e1b9f9570746aa5"),
create_guardian(x"ff6cb952589bde862c25ef4392132fb9d4a42157"),
create_guardian(x"114de8460193bdf3a2fcf81f86a09765f4762fd1"),
create_guardian(x"107a0086b32d7a0977926a205131d8731d39cbeb"),
create_guardian(x"8c82b2fd82faed2711d59af0f2499d16e726f6b2"),
create_guardian(x"11b39756c042441be6d8650b69b54ebe715e2343"),
create_guardian(x"54ce5b4d348fb74b958e8966e2ec3dbd4958a7cd"),
create_guardian(x"eb5f7389fa26941519f0863349c223b73a6ddee7"),
create_guardian(x"74a3bf913953d695260d88bc1aa25a4eee363ef0"),
create_guardian(x"000ac0076727b35fbea2dac28fee5ccb0fea768e"),
create_guardian(x"af45ced136b9d9e24903464ae889f5c8a723fc14"),
create_guardian(x"f93124b7c738843cbb89e864c862c38cddcccf95"),
create_guardian(x"d2cc37a4dc036a8d232b48f62cdd4731412f4890"),
create_guardian(x"da798f6896a3331f64b48c12d1d57fd9cbe70811"),
create_guardian(x"71aa1be1d36cafe3867910f99c09e347899c19c3"),
create_guardian(x"8192b6e7387ccd768277c17dab1b7a5027c0b3cf"),
create_guardian(x"178e21ad2e77ae06711549cfbb1f9c7a9d8096e8"),
create_guardian(x"5e1487f35515d02a92753504a8d75471b9f49edb"),
create_guardian(x"6fbebc898f403e4773e95feb15e80c9a99c8348d"),
];
assert!(expected == guardians, 0);
}
#[test]
public fun test_guardian_set_expiry() {
use wormhole::state::{State, Self as worm_state};
use wormhole::test_state::{init_wormhole_state};
let (admin, _, _) = people();
let test = init_wormhole_state(scenario(), admin);
next_tx(&mut test, admin);{
let state = take_shared<State>(&test);
let first_index = worm_state::get_current_guardian_set_index(&state);
let guardian_set = worm_state::get_guardian_set(&state, first_index);
// make sure guardian set is active
assert!(worm_state::guardian_set_is_active(&state, &guardian_set, ctx(&mut test)), 0);
// do an upgrade
guardian_set_upgrade::do_upgrade_test(
&mut state,
u32::from_u64(1),
vector[create_guardian(x"71aa1be1d36cafe3867910f99c09e347899c19c3")], // new guardian set
ctx(&mut test),
);
// make sure old guardian set is still active
guardian_set = worm_state::get_guardian_set(&state, first_index);
assert!(worm_state::guardian_set_is_active(&state, &guardian_set, ctx(&mut test)), 0);
// fast forward time beyond expiration
// increment by 3 epochs
increment_epoch_number(ctx(&mut test));
increment_epoch_number(ctx(&mut test));
increment_epoch_number(ctx(&mut test));
// make sure old guardian set is no longer active
assert!(!worm_state::guardian_set_is_active(&state, &guardian_set, ctx(&mut test)), 0);
return_shared<State>(state);
};
test_scenario::end(test);
}
}

View File

@ -0,0 +1,16 @@
module wormhole::keccak256 {
use sui::ecdsa_k1::{Self as ecdsa};
spec module {
pragma verify=false;
}
public fun keccak256(bytes: vector<u8>): vector<u8> {
ecdsa::keccak256(&bytes)
}
spec keccak256 {
pragma opaque;
}
}

View File

@ -0,0 +1,40 @@
module wormhole::myu16 {
const MAX_U16: u64 = (1 << 16) - 1;
const E_OVERFLOW: u64 = 0x0;
struct U16 has store, copy, drop {
number: u64
}
fun check_overflow(u: &U16) {
assert!(u.number <= MAX_U16, E_OVERFLOW)
}
public fun from_u64(number: u64): U16 {
let u = U16 { number };
check_overflow(&u);
u
}
public fun to_u64(u: U16): u64 {
u.number
}
public fun split_u8(number: U16): (u8, u8) {
let U16 { number } = number;
let v0: u8 = ((number >> 8) % (0xFF + 1) as u8);
let v1: u8 = (number % (0xFF + 1) as u8);
(v0, v1)
}
#[test]
public fun test_split_u8() {
let u = from_u64(0x1234);
let (v0, v1) = split_u8(u);
assert!(v0 == 0x12, 0);
assert!(v1 == 0x34, 0);
}
}

View File

@ -0,0 +1,888 @@
/// The implementation of large numbers written in Move language.
/// Code derived from original work by Andrew Poelstra <apoelstra@wpsoftware.net>
///
/// Rust Bitcoin Library
/// Written in 2014 by
/// Andrew Poelstra <apoelstra@wpsoftware.net>
///
/// To the extent possible under law, the author(s) have dedicated all
/// copyright and related and neighboring rights to this software to
/// the public domain worldwide. This software is distributed without
/// any warranty.
///
/// Simplified impl by Parity Team - https://github.com/paritytech/parity-common/blob/master/uint/src/uint.rs
///
/// Features:
/// * mul
/// * div
/// * add
/// * sub
/// * shift left
/// * shift right
/// * compare
/// * if math overflows the contract crashes.
///
/// Would be nice to help with the following TODO list:
/// * pow() , sqrt().
/// * math funcs that don't abort on overflows, but just returns reminders.
/// * Export of low_u128 (see original implementation).
/// * Export of low_u64 (see original implementation).
/// * Gas Optimisation:
/// * We can optimize by replacing bytecode, as far as we know Move VM itself support slices, so probably
/// we can try to replace parts works with (`v0`,`v1`,`v2`,`v3` etc) works.
/// * More?
/// * More tests (see current tests and TODOs i left):
/// * u256_arithmetic_test - https://github.com/paritytech/bigint/blob/master/src/uint.rs#L1338
/// * More from - https://github.com/paritytech/bigint/blob/master/src/uint.rs
/// * Division:
/// * Could be improved with div_mod_small (current version probably would took a lot of resources for small numbers).
/// * Also could be improved with Knuth, TAOCP, Volume 2, section 4.3.1, Algorithm D (see link to Parity above).
module wormhole::myu256 {
// Errors.
/// When can't cast `U256` to `u128` (e.g. number too large).
const ECAST_OVERFLOW: u64 = 0;
/// When trying to get or put word into U256 but it's out of index.
const EWORDS_OVERFLOW: u64 = 1;
/// When math overflows.
const EOVERFLOW: u64 = 2;
/// When attempted to divide by zero.
const EDIV_BY_ZERO: u64 = 3;
/// TODO: removed some functionality that the prover was breaking on.
/// In order to keep the functions backwards compatible, we keep the
/// signatures but revert immediately.
/// A better solution would be figuring out a way to skip checking them
/// in the prover, and just restore the original functionality.
const EUNSUPPORTED: u64 = 4;
// Constants.
/// Max `u64` value.
const U64_MAX: u128 = 18446744073709551615;
/// Max `u128` value.
const U128_MAX: u128 = 340282366920938463463374607431768211455;
/// Total words in `U256` (64 * 4 = 256).
const WORDS: u64 = 4;
/// When both `U256` equal.
const EQUAL: u8 = 0;
/// When `a` is less than `b`.
const LESS_THAN: u8 = 1;
/// When `b` is greater than `b`.
const GREATER_THAN: u8 = 2;
// Data structs.
/// The `U256` resource.
/// Contains 4 u64 numbers.
struct U256 has copy, drop, store {
v0: u64,
v1: u64,
v2: u64,
v3: u64,
}
/// Double `U256` used for multiple (to store overflow).
struct DU256 has copy, drop, store {
v0: u64,
v1: u64,
v2: u64,
v3: u64,
v4: u64,
v5: u64,
v6: u64,
v7: u64,
}
// Public functions.
/// Adds two `U256` and returns sum.
public fun add(a: U256, b: U256): U256 {
let ret = zero();
let carry = 0u64;
let i = 0;
while (i < WORDS) {
let a1 = get(&a, i);
let b1 = get(&b, i);
if (carry != 0) {
let (res1, is_overflow1) = overflowing_add(a1, b1);
let (res2, is_overflow2) = overflowing_add(res1, carry);
put(&mut ret, i, res2);
carry = 0;
if (is_overflow1) {
carry = carry + 1;
};
if (is_overflow2) {
carry = carry + 1;
}
} else {
let (res, is_overflow) = overflowing_add(a1, b1);
put(&mut ret, i, res);
carry = 0;
if (is_overflow) {
carry = 1;
};
};
i = i + 1;
};
assert!(carry == 0, EOVERFLOW);
ret
}
/// Convert `U256` to `u128` value if possible (otherwise it aborts).
public fun as_u128(a: U256): u128 {
assert!(a.v2 == 0 && a.v3 == 0, ECAST_OVERFLOW);
((a.v1 as u128) << 64) + (a.v0 as u128)
}
/// Convert `U256` to `u64` value if possible (otherwise it aborts).
public fun as_u64(a: U256): u64 {
assert!(a.v1 == 0 && a.v2 == 0 && a.v3 == 0, ECAST_OVERFLOW);
a.v0
}
/// Compares two `U256` numbers.
public fun compare(a: &U256, b: &U256): u8 {
let i = WORDS;
while (i > 0) {
i = i - 1;
let a1 = get(a, i);
let b1 = get(b, i);
if (a1 != b1) {
if (a1 < b1) {
return LESS_THAN
} else {
return GREATER_THAN
}
}
};
EQUAL
}
/// Returns a `U256` from `u64` value.
public fun from_u64(val: u64): U256 {
from_u128((val as u128))
}
/// Returns a `U256` from `u128` value.
public fun from_u128(val: u128): U256 {
let (a2, a1) = split_u128(val);
U256 {
v0: a1,
v1: a2,
v2: 0,
v3: 0,
}
}
/// Multiples two `U256`.
public fun mul(a: U256, b: U256): U256 {
let ret = DU256 {
v0: 0,
v1: 0,
v2: 0,
v3: 0,
v4: 0,
v5: 0,
v6: 0,
v7: 0,
};
let i = 0;
while (i < WORDS) {
let carry = 0u64;
let b1 = get(&b, i);
let j = 0;
while (j < WORDS) {
let a1 = get(&a, j);
if (a1 != 0 || carry != 0) {
let (hi, low) = split_u128((a1 as u128) * (b1 as u128));
let overflow = {
let existing_low = get_d(&ret, i + j);
let (low, o) = overflowing_add(low, existing_low);
put_d(&mut ret, i + j, low);
if (o) {
1
} else {
0
}
};
carry = {
let existing_hi = get_d(&ret, i + j + 1);
let hi = hi + overflow;
let (hi, o0) = overflowing_add(hi, carry);
let (hi, o1) = overflowing_add(hi, existing_hi);
put_d(&mut ret, i + j + 1, hi);
if (o0 || o1) {
1
} else {
0
}
};
};
j = j + 1;
};
i = i + 1;
};
let (r, overflow) = du256_to_u256(ret);
assert!(!overflow, EOVERFLOW);
r
}
/// Subtracts two `U256`, returns result.
public fun sub(a: U256, b: U256): U256 {
let ret = zero();
let carry = 0u64;
let i = 0;
while (i < WORDS) {
let a1 = get(&a, i);
let b1 = get(&b, i);
if (carry != 0) {
let (res1, is_overflow1) = overflowing_sub(a1, b1);
let (res2, is_overflow2) = overflowing_sub(res1, carry);
put(&mut ret, i, res2);
carry = 0;
if (is_overflow1) {
carry = carry + 1;
};
if (is_overflow2) {
carry = carry + 1;
}
} else {
let (res, is_overflow) = overflowing_sub(a1, b1);
put(&mut ret, i, res);
carry = 0;
if (is_overflow) {
carry = 1;
};
};
i = i + 1;
};
assert!(carry == 0, EOVERFLOW);
ret
}
public fun div(_a: U256, _b: U256): U256 {
abort EUNSUPPORTED
}
/// Shift right `a` by `shift`.
public fun shr(a: U256, shift: u8): U256 {
let ret = zero();
let word_shift = (shift as u64) / 64;
let bit_shift = (shift as u64) % 64;
let i = word_shift;
while (i < WORDS) {
let m = get(&a, i) >> (bit_shift as u8);
put(&mut ret, i - word_shift, m);
i = i + 1;
};
if (bit_shift > 0) {
let j = word_shift + 1;
while (j < WORDS) {
let m = get(&ret, j - word_shift - 1) + (get(&a, j) << (64 - (bit_shift as u8)));
put(&mut ret, j - word_shift - 1, m);
j = j + 1;
};
};
ret
}
/// Shift left `a` by `shift`.
public fun shl(a: U256, shift: u8): U256 {
let ret = zero();
let word_shift = (shift as u64) / 64;
let bit_shift = (shift as u64) % 64;
let i = word_shift;
while (i < WORDS) {
let m = get(&a, i - word_shift) << (bit_shift as u8);
put(&mut ret, i, m);
i = i + 1;
};
if (bit_shift > 0) {
let j = word_shift + 1;
while (j < WORDS) {
let m = get(&ret, j) + (get(&a, j - 1 - word_shift) >> (64 - (bit_shift as u8)));
put(&mut ret, j, m);
j = j + 1;
};
};
ret
}
/// Returns `U256` equals to zero.
public fun zero(): U256 {
U256 {
v0: 0,
v1: 0,
v2: 0,
v3: 0,
}
}
// Private functions.
/// Similar to Rust `overflowing_add`.
/// Returns a tuple of the addition along with a boolean indicating whether an arithmetic overflow would occur.
/// If an overflow would have occurred then the wrapped value is returned.
fun overflowing_add(a: u64, b: u64): (u64, bool) {
let a128 = (a as u128);
let b128 = (b as u128);
let r = a128 + b128;
if (r > U64_MAX) {
// overflow
let overflow = r - U64_MAX - 1;
((overflow as u64), true)
} else {
(((a128 + b128) as u64), false)
}
}
/// Similar to Rust `overflowing_sub`.
/// Returns a tuple of the addition along with a boolean indicating whether an arithmetic overflow would occur.
/// If an overflow would have occurred then the wrapped value is returned.
fun overflowing_sub(a: u64, b: u64): (u64, bool) {
if (a < b) {
let r = b - a;
((U64_MAX as u64) - r + 1, true)
} else {
(a - b, false)
}
}
/// Extracts two `u64` from `a` `u128`.
fun split_u128(a: u128): (u64, u64) {
let a1 = ((a >> 64) as u64);
let a2 = ((a % (0xFFFFFFFFFFFFFFFF + 1)) as u64);
(a1, a2)
}
/// Get word from `a` by index `i`.
public fun get(a: &U256, i: u64): u64 {
if (i == 0) {
a.v0
} else if (i == 1) {
a.v1
} else if (i == 2) {
a.v2
} else if (i == 3) {
a.v3
} else {
abort EWORDS_OVERFLOW
}
}
/// Get word from `DU256` by index.
fun get_d(a: &DU256, i: u64): u64 {
if (i == 0) {
a.v0
} else if (i == 1) {
a.v1
} else if (i == 2) {
a.v2
} else if (i == 3) {
a.v3
} else if (i == 4) {
a.v4
} else if (i == 5) {
a.v5
} else if (i == 6) {
a.v6
} else if (i == 7) {
a.v7
} else {
abort EWORDS_OVERFLOW
}
}
/// Put new word `val` into `U256` by index `i`.
fun put(a: &mut U256, i: u64, val: u64) {
if (i == 0) {
a.v0 = val;
} else if (i == 1) {
a.v1 = val;
} else if (i == 2) {
a.v2 = val;
} else if (i == 3) {
a.v3 = val;
} else {
abort EWORDS_OVERFLOW
}
}
/// Put new word into `DU256` by index `i`.
fun put_d(a: &mut DU256, i: u64, val: u64) {
if (i == 0) {
a.v0 = val;
} else if (i == 1) {
a.v1 = val;
} else if (i == 2) {
a.v2 = val;
} else if (i == 3) {
a.v3 = val;
} else if (i == 4) {
a.v4 = val;
} else if (i == 5) {
a.v5 = val;
} else if (i == 6) {
a.v6 = val;
} else if (i == 7) {
a.v7 = val;
} else {
abort EWORDS_OVERFLOW
}
}
/// Convert `DU256` to `U256`.
fun du256_to_u256(a: DU256): (U256, bool) {
let b = U256 {
v0: a.v0,
v1: a.v1,
v2: a.v2,
v3: a.v3,
};
let overflow = false;
if (a.v4 != 0 || a.v5 != 0 || a.v6 != 0 || a.v7 != 0) {
overflow = true;
};
(b, overflow)
}
// Tests.
#[test]
fun test_get_d() {
let a = DU256 {
v0: 1,
v1: 2,
v2: 3,
v3: 4,
v4: 5,
v5: 6,
v6: 7,
v7: 8,
};
assert!(get_d(&a, 0) == 1, 0);
assert!(get_d(&a, 1) == 2, 1);
assert!(get_d(&a, 2) == 3, 2);
assert!(get_d(&a, 3) == 4, 3);
assert!(get_d(&a, 4) == 5, 4);
assert!(get_d(&a, 5) == 6, 5);
assert!(get_d(&a, 6) == 7, 6);
assert!(get_d(&a, 7) == 8, 7);
}
#[test]
#[expected_failure(abort_code = 1, location=wormhole::myu256)]
fun test_get_d_overflow() {
let a = DU256 {
v0: 1,
v1: 2,
v2: 3,
v3: 4,
v4: 5,
v5: 6,
v6: 7,
v7: 8,
};
get_d(&a, 8);
}
#[test]
fun test_put_d() {
let a = DU256 {
v0: 1,
v1: 2,
v2: 3,
v3: 4,
v4: 5,
v5: 6,
v6: 7,
v7: 8,
};
put_d(&mut a, 0, 10);
put_d(&mut a, 1, 20);
put_d(&mut a, 2, 30);
put_d(&mut a, 3, 40);
put_d(&mut a, 4, 50);
put_d(&mut a, 5, 60);
put_d(&mut a, 6, 70);
put_d(&mut a, 7, 80);
assert!(get_d(&a, 0) == 10, 0);
assert!(get_d(&a, 1) == 20, 1);
assert!(get_d(&a, 2) == 30, 2);
assert!(get_d(&a, 3) == 40, 3);
assert!(get_d(&a, 4) == 50, 4);
assert!(get_d(&a, 5) == 60, 5);
assert!(get_d(&a, 6) == 70, 6);
assert!(get_d(&a, 7) == 80, 7);
}
#[test]
#[expected_failure(abort_code = 1, location=wormhole::myu256)]
fun test_put_d_overflow() {
let a = DU256 {
v0: 1,
v1: 2,
v2: 3,
v3: 4,
v4: 5,
v5: 6,
v6: 7,
v7: 8,
};
put_d(&mut a, 8, 0);
}
#[test]
fun test_du256_to_u256() {
let a = DU256 {
v0: 255,
v1: 100,
v2: 50,
v3: 300,
v4: 0,
v5: 0,
v6: 0,
v7: 0,
};
let (m, overflow) = du256_to_u256(a);
assert!(!overflow, 0);
assert!(m.v0 == a.v0, 1);
assert!(m.v1 == a.v1, 2);
assert!(m.v2 == a.v2, 3);
assert!(m.v3 == a.v3, 4);
a.v4 = 100;
a.v5 = 5;
let (m, overflow) = du256_to_u256(a);
assert!(overflow, 5);
assert!(m.v0 == a.v0, 6);
assert!(m.v1 == a.v1, 7);
assert!(m.v2 == a.v2, 8);
assert!(m.v3 == a.v3, 9);
}
#[test]
fun test_get() {
let a = U256 {
v0: 1,
v1: 2,
v2: 3,
v3: 4,
};
assert!(get(&a, 0) == 1, 0);
assert!(get(&a, 1) == 2, 1);
assert!(get(&a, 2) == 3, 2);
assert!(get(&a, 3) == 4, 3);
}
#[test]
#[expected_failure(abort_code = 1, location=wormhole::myu256)]
fun test_get_aborts() {
let _ = get(&zero(), 4);
}
#[test]
fun test_put() {
let a = zero();
put(&mut a, 0, 255);
assert!(get(&a, 0) == 255, 0);
put(&mut a, 1, (U64_MAX as u64));
assert!(get(&a, 1) == (U64_MAX as u64), 1);
put(&mut a, 2, 100);
assert!(get(&a, 2) == 100, 2);
put(&mut a, 3, 3);
assert!(get(&a, 3) == 3, 3);
put(&mut a, 2, 0);
assert!(get(&a, 2) == 0, 4);
}
#[test]
#[expected_failure(abort_code = 1, location=wormhole::myu256)]
fun test_put_overflow() {
let a = zero();
put(&mut a, 6, 255);
}
#[test]
fun test_from_u128() {
let i = 0;
while (i < 1024) {
let big = from_u128(i);
assert!(as_u128(big) == i, 0);
i = i + 1;
};
}
#[test]
fun test_add() {
let a = from_u128(1000);
let b = from_u128(500);
let s = as_u128(add(a, b));
assert!(s == 1500, 0);
a = from_u128(U64_MAX);
b = from_u128(U64_MAX);
s = as_u128(add(a, b));
assert!(s == (U64_MAX + U64_MAX), 1);
}
#[test]
#[expected_failure(abort_code = 2, location=wormhole::myu256)]
fun test_add_overflow() {
let max = (U64_MAX as u64);
let a = U256 {
v0: max,
v1: max,
v2: max,
v3: max
};
let _ = add(a, from_u128(1));
}
#[test]
fun test_sub() {
let a = from_u128(1000);
let b = from_u128(500);
let s = as_u128(sub(a, b));
assert!(s == 500, 0);
}
#[test]
#[expected_failure(abort_code = 2, location=wormhole::myu256)]
fun test_sub_overflow() {
let a = from_u128(0);
let b = from_u128(1);
let _ = sub(a, b);
}
#[test]
#[expected_failure(abort_code = 0, location=wormhole::myu256)]
fun test_too_big_to_cast_to_u128() {
let a = from_u128(U128_MAX);
let b = from_u128(U128_MAX);
let _ = as_u128(add(a, b));
}
#[test]
fun test_overflowing_add() {
let (n, z) = overflowing_add(10, 10);
assert!(n == 20, 0);
assert!(!z, 1);
(n, z) = overflowing_add((U64_MAX as u64), 1);
assert!(n == 0, 2);
assert!(z, 3);
(n, z) = overflowing_add((U64_MAX as u64), 10);
assert!(n == 9, 4);
assert!(z, 5);
(n, z) = overflowing_add(5, 8);
assert!(n == 13, 6);
assert!(!z, 7);
}
#[test]
fun test_overflowing_sub() {
let (n, z) = overflowing_sub(10, 5);
assert!(n == 5, 0);
assert!(!z, 1);
(n, z) = overflowing_sub(0, 1);
assert!(n == (U64_MAX as u64), 2);
assert!(z, 3);
(n, z) = overflowing_sub(10, 10);
assert!(n == 0, 4);
assert!(!z, 5);
}
#[test]
fun test_split_u128() {
let (a1, a2) = split_u128(100);
assert!(a1 == 0, 0);
assert!(a2 == 100, 1);
(a1, a2) = split_u128(U64_MAX + 1);
assert!(a1 == 1, 2);
assert!(a2 == 0, 3);
}
#[test]
fun test_mul() {
let a = from_u128(285);
let b = from_u128(375);
let c = as_u128(mul(a, b));
assert!(c == 106875, 0);
a = from_u128(0);
b = from_u128(1);
c = as_u128(mul(a, b));
assert!(c == 0, 1);
a = from_u128(U64_MAX);
b = from_u128(2);
c = as_u128(mul(a, b));
assert!(c == 36893488147419103230, 2);
}
#[test]
#[expected_failure(abort_code = 2, location=wormhole::myu256)]
fun test_mul_overflow() {
let max = (U64_MAX as u64);
let a = U256 {
v0: max,
v1: max,
v2: max,
v3: max,
};
let _ = mul(a, from_u128(2));
}
#[test]
fun test_zero() {
let a = as_u128(zero());
assert!(a == 0, 0);
let a = zero();
assert!(a.v0 == 0, 1);
assert!(a.v1 == 0, 2);
assert!(a.v2 == 0, 3);
assert!(a.v3 == 0, 4);
}
#[test]
fun test_from_u64() {
let a = as_u128(from_u64(100));
assert!(a == 100, 0);
// TODO: more tests.
}
#[test]
fun test_compare() {
let a = from_u128(1000);
let b = from_u128(50);
let cmp = compare(&a, &b);
assert!(cmp == 2, 0);
a = from_u128(100);
b = from_u128(100);
cmp = compare(&a, &b);
assert!(cmp == 0, 1);
a = from_u128(50);
b = from_u128(75);
cmp = compare(&a, &b);
assert!(cmp == 1, 2);
}
#[test]
fun test_shift_left() {
let a = from_u128(100);
let b = shl(a, 2);
assert!(as_u128(b) == 400, 0);
// TODO: more shift left tests.
}
#[test]
fun test_shift_right() {
let a = from_u128(100);
let b = shr(a, 2);
assert!(as_u128(b) == 25, 0);
// TODO: more shift right tests.
}
#[test]
fun test_as_u64() {
let _ = as_u64(from_u64((U64_MAX as u64)));
let _ = as_u64(from_u128(1));
}
#[test]
#[expected_failure(abort_code=0, location=wormhole::myu256)]
fun test_as_u64_overflow() {
let _ = as_u64(from_u128(U128_MAX));
}
}

View File

@ -0,0 +1,44 @@
module wormhole::myu32 {
const MAX_U32: u64 = (1 << 32) - 1;
const E_OVERFLOW: u64 = 0x0;
struct U32 has store, copy, drop {
number: u64
}
fun check_overflow(u: &U32) {
assert!(u.number <= MAX_U32, E_OVERFLOW)
}
public fun from_u64(number: u64): U32 {
let u = U32 { number };
check_overflow(&u);
u
}
public fun to_u64(u: U32): u64 {
u.number
}
public fun split_u8(number: U32): (u8, u8, u8, u8) {
let U32 { number } = number;
let v0: u8 = ((number >> 24) % (0xFF + 1) as u8);
let v1: u8 = ((number >> 16) % (0xFF + 1) as u8);
let v2: u8 = ((number >> 8) % (0xFF + 1) as u8);
let v3: u8 = (number % (0xFF + 1) as u8);
(v0, v1, v2, v3)
}
#[test]
public fun test_split_u8() {
let u = from_u64(0x12345678);
let (v0, v1, v2, v3) = split_u8(u);
assert!(v0 == 0x12, 0);
assert!(v1 == 0x34, 0);
assert!(v2 == 0x56, 0);
assert!(v3 == 0x78, 0);
}
}

View File

@ -0,0 +1,548 @@
module wormhole::myvaa {
use std::vector;
use sui::tx_context::TxContext;
//use 0x1::secp256k1;
use wormhole::myu16::{U16};
use wormhole::myu32::{U32};
use wormhole::deserialize;
use wormhole::cursor;
use wormhole::guardian_pubkey;
use wormhole::structs::{
Guardian,
GuardianSet,
Signature,
create_signature,
get_guardians,
unpack_signature,
get_address,
};
use wormhole::state::{Self, State};
use wormhole::external_address::{Self, ExternalAddress};
use wormhole::keccak256::keccak256;
friend wormhole::guardian_set_upgrade;
//friend wormhole::contract_upgrade;
const E_NO_QUORUM: u64 = 0x0;
const E_TOO_MANY_SIGNATURES: u64 = 0x1;
const E_INVALID_SIGNATURE: u64 = 0x2;
const E_GUARDIAN_SET_EXPIRED: u64 = 0x3;
const E_INVALID_GOVERNANCE_CHAIN: u64 = 0x4;
const E_INVALID_GOVERNANCE_EMITTER: u64 = 0x5;
const E_WRONG_VERSION: u64 = 0x6;
const E_NON_INCREASING_SIGNERS: u64 = 0x7;
const E_OLD_GUARDIAN_SET_GOVERNANCE: u64 = 0x8;
struct VAA {
/// Header
guardian_set_index: U32,
signatures: vector<Signature>,
/// Body
timestamp: U32,
nonce: U32,
emitter_chain: U16,
emitter_address: ExternalAddress,
sequence: u64,
consistency_level: u8,
hash: vector<u8>, // 32 bytes
payload: vector<u8>, // variable bytes
}
//break
#[test_only]
public fun parse_test(bytes: vector<u8>): VAA {
parse(bytes)
}
/// Parses a VAA.
/// Does not do any verification, and is thus private.
/// This ensures the invariant that if an external module receives a `VAA`
/// object, its signatures must have been verified, because the only public
/// function that returns a VAA is `parse_and_verify`
fun parse(bytes: vector<u8>): VAA {
let cur = cursor::cursor_init(bytes);
let version = deserialize::deserialize_u8(&mut cur);
assert!(version == 1, E_WRONG_VERSION);
let guardian_set_index = deserialize::deserialize_u32(&mut cur);
let signatures_len = deserialize::deserialize_u8(&mut cur);
let signatures = vector::empty<Signature>();
while (signatures_len > 0) {
let guardian_index = deserialize::deserialize_u8(&mut cur);
let sig = deserialize::deserialize_vector(&mut cur, 64);
let recovery_id = deserialize::deserialize_u8(&mut cur);
vector::push_back(&mut signatures, create_signature(sig, recovery_id, guardian_index));
signatures_len = signatures_len - 1;
};
let body = cursor::rest(cur);
let hash = keccak256(keccak256(body));
let cur = cursor::cursor_init(body);
let timestamp = deserialize::deserialize_u32(&mut cur);
let nonce = deserialize::deserialize_u32(&mut cur);
let emitter_chain = deserialize::deserialize_u16(&mut cur);
let emitter_address = external_address::deserialize(&mut cur);
let sequence = deserialize::deserialize_u64(&mut cur);
let consistency_level = deserialize::deserialize_u8(&mut cur);
let payload = cursor::rest(cur);
VAA {
guardian_set_index,
signatures,
timestamp,
nonce,
emitter_chain,
emitter_address,
sequence,
consistency_level,
hash,
payload,
}
}
public fun get_guardian_set_index(vaa: &VAA): U32 {
vaa.guardian_set_index
}
public fun get_timestamp(vaa: &VAA): U32 {
vaa.timestamp
}
public fun get_payload(vaa: &VAA): vector<u8> {
vaa.payload
}
public fun get_hash(vaa: &VAA): vector<u8> {
vaa.hash
}
public fun get_emitter_chain(vaa: &VAA): U16 {
vaa.emitter_chain
}
public fun get_emitter_address(vaa: &VAA): ExternalAddress {
vaa.emitter_address
}
public fun get_sequence(vaa: &VAA): u64 {
vaa.sequence
}
public fun get_consistency_level(vaa: &VAA): u8 {
vaa.consistency_level
}
// break
public fun destroy(vaa: VAA): vector<u8> {
let VAA {
guardian_set_index: _,
signatures: _,
timestamp: _,
nonce: _,
emitter_chain: _,
emitter_address: _,
sequence: _,
consistency_level: _,
hash: _,
payload,
} = vaa;
payload
}
/// Verifies the signatures of a VAA.
/// It's private, because there's no point calling it externally, since VAAs
/// external to this module have already been verified (by construction).
fun verify(vaa: &VAA, state: &State, guardian_set: &GuardianSet, ctx: &TxContext) {
assert!(state::guardian_set_is_active(state, guardian_set, ctx), E_GUARDIAN_SET_EXPIRED);
let guardians = get_guardians(guardian_set);
let hash = vaa.hash;
let sigs_len = vector::length<Signature>(&vaa.signatures);
let guardians_len = vector::length<Guardian>(&guardians);
assert!(sigs_len >= quorum(guardians_len), E_NO_QUORUM);
let sig_i = 0;
let last_index = 0;
while (sig_i < sigs_len) {
let (sig, recovery_id, guardian_index) = unpack_signature(vector::borrow(&vaa.signatures, sig_i));
// Ensure that the provided signatures are strictly increasing.
// This check makes sure that no duplicate signers occur. The
// increasing order is guaranteed by the guardians, or can always be
// reordered by the client.
assert!(sig_i == 0 || guardian_index > last_index, E_NON_INCREASING_SIGNERS);
last_index = guardian_index;
let address = guardian_pubkey::from_signature(hash, recovery_id, sig);
let cur_guardian = vector::borrow<Guardian>(&guardians, (guardian_index as u64));
let cur_address = get_address(cur_guardian);
assert!(address == cur_address, E_INVALID_SIGNATURE);
sig_i = sig_i + 1;
};
}
/// Parses and verifies the signatures of a VAA.
/// NOTE: this is the only public function that returns a VAA, and it should
/// be kept that way. This ensures that if an external module receives a
/// `VAA`, it has been verified.
public fun parse_and_verify(state: &mut State, bytes: vector<u8>, ctx: &TxContext): VAA {
let vaa = parse(bytes);
let guardian_set = state::get_guardian_set(state, vaa.guardian_set_index);
verify(&vaa, state, &guardian_set, ctx);
vaa
}
/// Gets a VAA payload without doing verififcation on the VAA. This method is
/// used for convenience in the Coin package, for example, for creating new tokens
/// with asset metadata in a token attestation VAA payload.
public fun parse_and_get_payload(bytes: vector<u8>): vector<u8> {
let vaa = parse(bytes);
let payload = destroy(vaa);
return payload
}
/// Aborts if the VAA is not governance (i.e. sent from the governance
/// emitter on the governance chain)
public fun assert_governance(state: &State, vaa: &VAA) {
let latest_guardian_set_index = state::get_current_guardian_set_index(state);
assert!(vaa.guardian_set_index == latest_guardian_set_index, E_OLD_GUARDIAN_SET_GOVERNANCE);
assert!(vaa.emitter_chain == state::get_governance_chain(state), E_INVALID_GOVERNANCE_CHAIN);
assert!(vaa.emitter_address == state::get_governance_contract(state), E_INVALID_GOVERNANCE_EMITTER);
}
/// Aborts if the VAA has already been consumed. Marks the VAA as consumed
/// the first time around.
/// Only to be used for core bridge messages. Protocols should implement
/// their own replay protection.
public(friend) fun replay_protect(state: &mut State, vaa: &VAA) {
// this calls table::add which aborts if the key already exists
state::set_governance_action_consumed(state, vaa.hash);
}
/// Returns the minimum number of signatures required for a VAA to be valid.
public fun quorum(num_guardians: u64): u64 {
(num_guardians * 2) / 3 + 1
}
}
// tests
// - do_upgrade (upgrade active guardian set to new set)
// TODO: fast forward test, check that previous guardian set gets expired
// TODO: adapt the tests from the aptos contracts test suite
#[test_only]
module wormhole::vaa_test {
use sui::test_scenario::{Self, Scenario, next_tx, ctx, take_shared, return_shared};
use sui::tx_context::{increment_epoch_number};
fun scenario(): Scenario { test_scenario::begin(@0x123233) }
fun people(): (address, address, address) { (@0x124323, @0xE05, @0xFACE) }
use wormhole::guardian_set_upgrade::{Self, do_upgrade_test};
use wormhole::state::{Self, State};
use wormhole::test_state::{init_wormhole_state};
use wormhole::structs::{Self, create_guardian};
use wormhole::myu32::{Self as u32};
use wormhole::myvaa::{Self as vaa};
/// A test VAA signed by the first guardian set (index 0) containing guardian a single
/// guardian beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe
/// It's a governance VAA (contract upgrade), so we can test all sorts of
/// properties
const GOV_VAA: vector<u8> = x"010000000001000da16466429ee8ffb09b90ca90db8326d20cfeeae0542da9dcaaad641a5aca2d6c1fe33a5970ca84fd0ff5e6d29ef9e40404eb1a8892b509f085fc725b9e23a30100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000020b10360000000000000000000000000000000000000000000000000000000000436f7265010016d8f30e4a345ea0fa5df11daac4e1866ee368d253209cf9eda012d915a2db09e6";
/// Identical VAA except it's signed by guardian set 1, and double signed by
/// beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe
/// Used to test that a single guardian can't supply multiple signatures
const GOV_VAA_DOUBLE_SIGNED: vector<u8> = x"010000000102000da16466429ee8ffb09b90ca90db8326d20cfeeae0542da9dcaaad641a5aca2d6c1fe33a5970ca84fd0ff5e6d29ef9e40404eb1a8892b509f085fc725b9e23a301000da16466429ee8ffb09b90ca90db8326d20cfeeae0542da9dcaaad641a5aca2d6c1fe33a5970ca84fd0ff5e6d29ef9e40404eb1a8892b509f085fc725b9e23a30100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000020b10360000000000000000000000000000000000000000000000000000000000436f7265010016d8f30e4a345ea0fa5df11daac4e1866ee368d253209cf9eda012d915a2db09e6";
/// A test VAA signed by the second guardian set (index 1) with the following two guardians:
/// 0: beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe
/// 1: 90F8bf6A479f320ead074411a4B0e7944Ea8c9C1
const GOV_VAA_2: vector<u8> = x"0100000001020052da07c7ba7d58661e22922a1130e75732f454e81086330f9a5337797ee7ee9d703fd55aabc257c4d53d8ab1e471e4eb1f2767bf37cc6d3d6774e2ca3ab429eb00018c9859f14027c2a62563028a2a9bbb30464ce5b86d13728b02fb85b34761d258154bb59bad87908c9b09342efa9045d4420d289bb0144729eb368ec50c45e719010000000100000001000100000000000000000000000000000000000000000000000000000000000000040000000004cdedc90000000000000000000000000000000000000000000000000000000000436f72650100167759324e86f870265b8648ef8d5ef505b2ae99840a616081eb7adc13995204a4";
#[test]
fun test_upgrade_guardian() {
test_upgrade_guardian_(scenario())
}
fun test_upgrade_guardian_(test: Scenario) {
let (admin, _, _) = people();
test = init_wormhole_state(test, admin);
next_tx(&mut test, admin);{
let state = take_shared<State>(&mut test);
let new_guardians = vector[structs::create_guardian(x"71aa1be1d36cafe3867910f99c09e347899c19c4")];
// upgrade guardian set
do_upgrade_test(&mut state, u32::from_u64(1), new_guardians, ctx(&mut test));
assert!(state::get_current_guardian_set_index(&state)==u32::from_u64(1), 0);
return_shared<State>(state);
};
test_scenario::end(test);
}
#[test]
/// Ensures that the GOV_VAA can still be verified after the guardian set
/// upgrade before expiry
public fun test_guardian_set_not_expired() {
let (admin, _, _) = people();
let test = init_wormhole_state(scenario(), admin);
next_tx(&mut test, admin);{
let state = take_shared<State>(&test);
// do an upgrade
guardian_set_upgrade::do_upgrade_test(
&mut state,
u32::from_u64(1),
vector[create_guardian(x"71aa1be1d36cafe3867910f99c09e347899c19c3")],
ctx(&mut test)
);
// fast forward time before expiration
increment_epoch_number(ctx(&mut test));
// we still expect this to verify
vaa::destroy(vaa::parse_and_verify(&mut state, GOV_VAA, ctx(&mut test)));
return_shared<State>(state);
};
test_scenario::end(test);
}
#[test]
#[expected_failure(abort_code = vaa::E_GUARDIAN_SET_EXPIRED)]
/// Ensures that the GOV_VAA can no longer be verified after the guardian set
/// upgrade after expiry
public fun test_guardian_set_expired() {
let (admin, _, _) = people();
let test = init_wormhole_state(scenario(), admin);
next_tx(&mut test, admin);{
let state = take_shared<State>(&test);
// do an upgrade
guardian_set_upgrade::do_upgrade_test(
&mut state,
u32::from_u64(1),
vector[create_guardian(x"71aa1be1d36cafe3867910f99c09e347899c19c3")],
ctx(&mut test)
);
// fast forward time beyond expiration
increment_epoch_number(ctx(&mut test));
increment_epoch_number(ctx(&mut test));
increment_epoch_number(ctx(&mut test));
// we expect this to fail because guardian set has expired
vaa::destroy(vaa::parse_and_verify(&mut state, GOV_VAA, ctx(&mut test)));
return_shared<State>(state);
};
test_scenario::end(test);
}
#[test]
#[expected_failure(abort_code = vaa::E_OLD_GUARDIAN_SET_GOVERNANCE)]
/// Ensures that governance GOV_VAAs can only be verified by the latest guardian
/// set, even if the signer hasn't expired yet
public fun test_governance_guardian_set_latest() {
let (admin, _, _) = people();
let test = init_wormhole_state(scenario(), admin);
next_tx(&mut test, admin);{
let state = take_shared<State>(&test);
// do an upgrade
guardian_set_upgrade::do_upgrade_test(
&mut state,
u32::from_u64(1),
vector[create_guardian(x"71aa1be1d36cafe3867910f99c09e347899c19c3")],
ctx(&mut test)
);
// fast forward time before expiration
increment_epoch_number(ctx(&mut test));
//still expect this to verify
let vaa = vaa::parse_and_verify(&mut state, GOV_VAA, ctx(&mut test));
// expect this to fail
vaa::assert_governance(&mut state, &vaa);
vaa::destroy(vaa);
return_shared<State>(state);
};
test_scenario::end(test);
}
#[test]
#[expected_failure(abort_code = vaa::E_INVALID_GOVERNANCE_EMITTER)]
/// Ensures that governance GOV_VAAs can only be sent from the correct governance emitter
public fun test_invalid_governance_emitter() {
let (admin, _, _) = people();
let test = init_wormhole_state(scenario(), admin);
next_tx(&mut test, admin);{
let state = take_shared<State>(&test);
state::set_governance_contract(&mut state, x"0000000000000000000000000000000000000000000000000000000000000005"); // set emitter contract to wrong contract
// expect this to succeed
let vaa = vaa::parse_and_verify(&mut state, GOV_VAA, ctx(&mut test));
// expect this to fail
vaa::assert_governance(&mut state, &vaa);
vaa::destroy(vaa);
return_shared<State>(state);
};
test_scenario::end(test);
}
#[test]
#[expected_failure(abort_code = vaa::E_INVALID_GOVERNANCE_CHAIN)]
/// Ensures that governance GOV_VAAs can only be sent from the correct governance chain
public fun test_invalid_governance_chain() {
let (admin, _, _) = people();
let test = init_wormhole_state(scenario(), admin);
next_tx(&mut test, admin);{
let state = take_shared<State>(&test);
state::set_governance_chain_id(&mut state, 200); // set governance chain to wrong chain
// expect this to succeed
let vaa = vaa::parse_and_verify(&mut state, GOV_VAA, ctx(&mut test));
// expect this to fail
vaa::assert_governance(&mut state, &vaa);
vaa::destroy(vaa);
return_shared<State>(state);
};
test_scenario::end(test);
}
#[test]
public fun test_quorum() {
let (admin, _, _) = people();
let test = init_wormhole_state(scenario(), admin);
next_tx(&mut test, admin);{
let state = take_shared<State>(&test);
// do an upgrade
guardian_set_upgrade::do_upgrade_test(
&mut state,
u32::from_u64(1),
vector[
create_guardian(x"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe"),
create_guardian(x"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1")
],
ctx(&mut test),
);
// we expect this to succeed because both guardians signed in the correct order
vaa::destroy(vaa::parse_and_verify(&mut state, GOV_VAA_2, ctx(&mut test)));
return_shared<State>(state);
};
test_scenario::end(test);
}
#[test]
#[expected_failure(abort_code = vaa::E_NO_QUORUM)]
public fun test_no_quorum() {
let (admin, _, _) = people();
let test = init_wormhole_state(scenario(), admin);
next_tx(&mut test, admin);{
let state = take_shared<State>(&test);
// do an upgrade
guardian_set_upgrade::do_upgrade_test(
&mut state,
u32::from_u64(1),
vector[
create_guardian(x"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe"),
create_guardian(x"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"),
create_guardian(x"5e1487f35515d02a92753504a8d75471b9f49edb")
],
ctx(&mut test),
);
// we expect this to fail because not enough signatures
vaa::destroy(vaa::parse_and_verify(&mut state, GOV_VAA_2, ctx(&mut test)));
return_shared<State>(state);
};
test_scenario::end(test);
}
#[test]
#[expected_failure(abort_code = vaa::E_NON_INCREASING_SIGNERS)]
public fun test_double_signed() {
let (admin, _, _) = people();
let test = init_wormhole_state(scenario(), admin);
next_tx(&mut test, admin);{
let state = take_shared<State>(&test);
// do an upgrade
guardian_set_upgrade::do_upgrade_test(
&mut state,
u32::from_u64(1),
vector[
create_guardian(x"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe"),
create_guardian(x"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"),
],
ctx(&mut test),
);
// we expect this to fail because
// beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe signed this twice
vaa::destroy(vaa::parse_and_verify(&mut state, GOV_VAA_DOUBLE_SIGNED, ctx(&mut test)));
return_shared<State>(state);
};
test_scenario::end(test);
}
#[test]
#[expected_failure(abort_code = vaa::E_INVALID_SIGNATURE)]
public fun test_out_of_order_signers() {
let (admin, _, _) = people();
let test = init_wormhole_state(scenario(), admin);
next_tx(&mut test, admin);{
let state = take_shared<State>(&test);
// do an upgrade
guardian_set_upgrade::do_upgrade_test(
&mut state,
u32::from_u64(1),
vector[
// guardians are set up in opposite order
create_guardian(x"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"),
create_guardian(x"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe"),
],
ctx(&mut test),
);
// we expect this to fail because signatures are out of order
vaa::destroy(vaa::parse_and_verify(&mut state, GOV_VAA_2, ctx(&mut test)));
return_shared<State>(state);
};
test_scenario::end(test);
}
}

View File

@ -0,0 +1,137 @@
module wormhole::serialize {
use std::vector;
use wormhole::myu16::{Self as u16, U16};
use wormhole::myu32::{Self as u32, U32};
use wormhole::myu256::U256;
// we reuse the native bcs serialiser -- it uses little-endian encoding, and
// we need big-endian, so the results are reversed
use std::bcs;
public fun serialize_u8(buf: &mut vector<u8>, v: u8) {
vector::push_back<u8>(buf, v);
}
public fun serialize_u16(buf: &mut vector<u8>, v: U16) {
let (v0, v1) = u16::split_u8(v);
serialize_u8(buf, v0);
serialize_u8(buf, v1);
}
public fun serialize_u32(buf: &mut vector<u8>, v: U32) {
let (v0, v1, v2, v3) = u32::split_u8(v);
serialize_u8(buf, v0);
serialize_u8(buf, v1);
serialize_u8(buf, v2);
serialize_u8(buf, v3);
}
public fun serialize_u64(buf: &mut vector<u8>, v: u64) {
let v = bcs::to_bytes(&v);
vector::reverse(&mut v);
vector::append(buf, v);
}
public fun serialize_u128(buf: &mut vector<u8>, v: u128) {
let v = bcs::to_bytes(&v);
vector::reverse(&mut v);
vector::append(buf, v);
}
public fun serialize_u256(buf: &mut vector<u8>, v: U256) {
let v = bcs::to_bytes(&v);
vector::reverse(&mut v);
vector::append(buf, v);
}
public fun serialize_vector(buf: &mut vector<u8>, v: vector<u8>){
vector::append(buf, v)
}
}
#[test_only]
module wormhole::test_serialize {
use wormhole::serialize;
use wormhole::deserialize;
use wormhole::cursor;
use wormhole::myu32::{Self as u32};
use wormhole::myu16::{Self as u16};
use wormhole::myu256::{Self as u256};
use 0x1::vector;
#[test]
fun test_serialize_u8(){
let u = 0x12;
let s = vector::empty();
serialize::serialize_u8(&mut s, u);
let cur = cursor::cursor_init(s);
let p = deserialize::deserialize_u8(&mut cur);
cursor::destroy_empty(cur);
assert!(p==u, 0);
}
#[test]
fun test_serialize_u16(){
let u = u16::from_u64((0x1234 as u64));
let s = vector::empty();
serialize::serialize_u16(&mut s, u);
let cur = cursor::cursor_init(s);
let p = deserialize::deserialize_u16(&mut cur);
cursor::destroy_empty(cur);
assert!(p==u, 0);
}
#[test]
fun test_serialize_u32(){
let u = u32::from_u64((0x12345678 as u64));
let s = vector::empty();
serialize::serialize_u32(&mut s, u);
let cur = cursor::cursor_init(s);
let p = deserialize::deserialize_u32(&mut cur);
cursor::destroy_empty(cur);
assert!(p==u, 0);
}
#[test]
fun test_serialize_u64(){
let u = 0x1234567812345678;
let s = vector::empty();
serialize::serialize_u64(&mut s, u);
let cur = cursor::cursor_init(s);
let p = deserialize::deserialize_u64(&mut cur);
cursor::destroy_empty(cur);
assert!(p==u, 0);
}
#[test]
fun test_serialize_u128(){
let u = 0x12345678123456781234567812345678;
let s = vector::empty();
serialize::serialize_u128(&mut s, u);
let cur = cursor::cursor_init(s);
let p = deserialize::deserialize_u128(&mut cur);
cursor::destroy_empty(cur);
assert!(p==u, 0);
}
#[test]
fun test_serialize_u256(){
let u = u256::add(u256::shl(u256::from_u128(0x47386917590997937461700473756125), 128), u256::from_u128(0x9876));
let s = vector::empty();
serialize::serialize_u256(&mut s, u);
let exp = x"4738691759099793746170047375612500000000000000000000000000009876";
assert!(s == exp, 0);
}
#[test]
fun test_serialize_vector(){
let x = vector::empty<u8>();
let y = vector::empty<u8>();
vector::push_back<u8>(&mut x, 0x12);
vector::push_back<u8>(&mut x, 0x34);
vector::push_back<u8>(&mut x, 0x56);
serialize::serialize_vector(&mut y, x);
assert!(y == x"123456", 0);
}
}

View File

@ -0,0 +1,33 @@
/// A set data structure.
module wormhole::set {
use sui::table::{Self, Table};
use sui::tx_context::TxContext;
/// Empty struct. Used as the value type in mappings to encode a set
struct Unit has store, copy, drop {}
/// A set containing elements of type `A` with support for membership
/// checking.
struct Set<phantom A: copy + drop + store> has store {
elems: Table<A, Unit>
}
/// Create a new Set.
public fun new<A: copy + drop + store>(ctx: &mut TxContext): Set<A> {
Set {
elems: table::new(ctx)
}
}
/// Add a new element to the set.
/// Aborts if the element already exists
public fun add<A: copy + drop + store>(set: &mut Set<A>, key: A) {
table::add(&mut set.elems, key, Unit {})
}
/// Returns true iff `set` contains an entry for `key`.
public fun contains<A: copy + drop + store>(set: &Set<A>, key: A): bool {
table::contains(&set.elems, key)
}
}

View File

@ -0,0 +1,267 @@
module wormhole::state {
use std::vector::{Self};
use sui::object::{Self, UID};
use sui::tx_context::{Self, TxContext};
use sui::transfer::{Self};
use sui::vec_map::{Self, VecMap};
use sui::event::{Self};
use wormhole::myu16::{Self as u16, U16};
use wormhole::myu32::{Self as u32, U32};
use wormhole::set::{Self, Set};
use wormhole::structs::{Self, create_guardian, Guardian, GuardianSet};
use wormhole::external_address::{Self, ExternalAddress};
use wormhole::emitter::{Self};
friend wormhole::guardian_set_upgrade;
//friend wormhole::contract_upgrade;
friend wormhole::wormhole;
friend wormhole::myvaa;
#[test_only]
friend wormhole::vaa_test;
struct DeployerCapability has key, store {id: UID}
struct WormholeMessage has store, copy, drop {
sender: u64,
sequence: u64,
nonce: u64,
payload: vector<u8>,
consistency_level: u8
}
struct State has key, store {
id: UID,
/// chain id
chain_id: U16,
/// guardian chain ID
governance_chain_id: U16,
/// Address of governance contract on governance chain
governance_contract: ExternalAddress,
/// Current active guardian set index
guardian_set_index: U32,
/// guardian sets
guardian_sets: VecMap<U32, GuardianSet>,
/// Period for which a guardian set stays active after it has been replaced
guardian_set_expiry: U32,
/// Consumed governance actions
consumed_governance_actions: Set<vector<u8>>,
/// Capability for creating new emitters
emitter_registry: emitter::EmitterRegistry,
/// wormhole message fee
message_fee: u64,
}
/// Called automatically when module is first published. Transfers a deployer cap to sender.
fun init(ctx: &mut TxContext) {
transfer::transfer(DeployerCapability{id: object::new(ctx)}, tx_context::sender(ctx));
}
// creates a shared state object, so that anyone can get a reference to &mut State
// and pass it into various functions
public entry fun init_and_share_state(
deployer: DeployerCapability,
chain_id: u64,
governance_chain_id: u64,
governance_contract: vector<u8>,
initial_guardians: vector<vector<u8>>,
ctx: &mut TxContext
) {
let DeployerCapability{id} = deployer;
object::delete(id);
let state = State {
id: object::new(ctx),
chain_id: u16::from_u64(chain_id),
governance_chain_id: u16::from_u64(governance_chain_id),
governance_contract: external_address::from_bytes(governance_contract),
guardian_set_index: u32::from_u64(0),
guardian_sets: vec_map::empty<U32, GuardianSet>(),
guardian_set_expiry: u32::from_u64(2), // TODO - what is the right #epochs to set this to?
consumed_governance_actions: set::new(ctx),
emitter_registry: emitter::init_emitter_registry(),
message_fee: 0,
};
let guardians = vector::empty<Guardian>();
vector::reverse(&mut initial_guardians);
while (!vector::is_empty(&initial_guardians)) {
vector::push_back(&mut guardians, create_guardian(vector::pop_back(&mut initial_guardians)));
};
// the initial guardian set with index 0
let initial_index = u32::from_u64(0);
store_guardian_set(&mut state, initial_index, structs::create_guardian_set(initial_index, guardians));
// permanently shares state
transfer::share_object<State>(state);
}
#[test_only]
public fun test_init(ctx: &mut TxContext) {
transfer::transfer(DeployerCapability{id: object::new(ctx)}, tx_context::sender(ctx));
}
public(friend) entry fun publish_event(
sender: u64,
sequence: u64,
nonce: u64,
payload: vector<u8>
) {
event::emit(
WormholeMessage {
sender: sender,
sequence: sequence,
nonce: nonce,
payload: payload,
// Sui is an instant finality chain, so we don't need
// confirmations
consistency_level: 0,
}
);
}
// setters
public(friend) fun set_chain_id(state: &mut State, id: u64){
state.chain_id = u16::from_u64(id);
}
#[test_only]
public fun test_set_chain_id(state: &mut State, id: u64) {
set_chain_id(state, id);
}
public(friend) fun set_governance_chain_id(state: &mut State, id: u64){
state.governance_chain_id = u16::from_u64(id);
}
#[test_only]
public fun test_set_governance_chain_id(state: &mut State, id: u64) {
set_governance_chain_id(state, id);
}
public(friend) fun set_governance_action_consumed(state: &mut State, hash: vector<u8>){
set::add<vector<u8>>(&mut state.consumed_governance_actions, hash);
}
public(friend) fun set_governance_contract(state: &mut State, contract: vector<u8>) {
state.governance_contract = external_address::from_bytes(contract);
}
public(friend) fun update_guardian_set_index(state: &mut State, new_index: U32) {
state.guardian_set_index = new_index;
}
public(friend) fun expire_guardian_set(state: &mut State, index: U32, ctx: &TxContext) {
let expiry = state.guardian_set_expiry;
let guardian_set = vec_map::get_mut<U32, GuardianSet>(&mut state.guardian_sets, &index);
structs::expire_guardian_set(guardian_set, expiry, ctx);
}
public(friend) fun store_guardian_set(state: &mut State, index: U32, set: GuardianSet) {
vec_map::insert<U32, GuardianSet>(&mut state.guardian_sets, index, set);
}
// getters
public fun get_current_guardian_set_index(state: &State): U32 {
return state.guardian_set_index
}
public fun get_guardian_set(state: &State, index: U32): GuardianSet {
return *vec_map::get<U32, GuardianSet>(&state.guardian_sets, &index)
}
public fun guardian_set_is_active(state: &State, guardian_set: &GuardianSet, ctx: &TxContext): bool {
let cur_epoch = tx_context::epoch(ctx);
let index = structs::get_guardian_set_index(guardian_set);
let current_index = get_current_guardian_set_index(state);
index == current_index ||
u32::to_u64(structs::get_guardian_set_expiry(guardian_set)) > cur_epoch
}
public fun get_governance_chain(state: &State): U16 {
return state.governance_chain_id
}
public fun get_governance_contract(state: &State): ExternalAddress {
return state.governance_contract
}
public fun get_chain_id(state: &State): U16 {
return state.chain_id
}
public fun get_message_fee(state: &State): u64 {
return state.message_fee
}
public(friend) fun new_emitter(state: &mut State, ctx: &mut TxContext): emitter::EmitterCapability{
emitter::new_emitter(&mut state.emitter_registry, ctx)
}
}
#[test_only]
module wormhole::test_state{
use sui::test_scenario::{Self, Scenario, next_tx, ctx, take_from_address, take_shared, return_shared};
use wormhole::state::{Self, test_init, State, DeployerCapability};
use wormhole::myu16::{Self as u16};
fun scenario(): Scenario { test_scenario::begin(@0x123233) }
fun people(): (address, address, address) { (@0x124323, @0xE05, @0xFACE) }
public fun init_wormhole_state(test: Scenario, admin: address): Scenario {
next_tx(&mut test, admin); {
test_init(ctx(&mut test));
};
next_tx(&mut test, admin); {
let deployer = take_from_address<DeployerCapability>(&test, admin);
state::init_and_share_state(
deployer,
21,
1, // governance chain
x"0000000000000000000000000000000000000000000000000000000000000004", // governance_contract
vector[x"beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe"], // initial_guardian(s)
ctx(&mut test));
};
return test
}
#[test]
fun test_state_setters() {
test_state_setters_(scenario())
}
fun test_state_setters_(test: Scenario) {
let (admin, _, _) = people();
test = init_wormhole_state(test, admin);
// test setters
next_tx(&mut test, admin); {
let state = take_shared<State>(&test);
// test set chain id
state::test_set_chain_id(&mut state, 5);
assert!(state::get_chain_id(&state) == u16::from_u64(5), 0);
// test set governance chain id
state::test_set_governance_chain_id(&mut state, 100);
assert!(state::get_governance_chain(&state) == u16::from_u64(100), 0);
return_shared<State>(state);
};
test_scenario::end(test);
}
}

View File

@ -0,0 +1,69 @@
module wormhole::structs {
use wormhole::myu32::{Self as u32, U32};
use sui::tx_context::{Self, TxContext};
friend wormhole::state;
use wormhole::guardian_pubkey::{Self};
struct Signature has store, copy, drop {
sig: vector<u8>,
recovery_id: u8,
guardian_index: u8,
}
struct Guardian has store, drop, copy {
address: guardian_pubkey::Address
}
struct GuardianSet has store, copy, drop {
index: U32,
guardians: vector<Guardian>,
expiration_time: U32,
}
public fun create_guardian(address: vector<u8>): Guardian {
Guardian {
address: guardian_pubkey::from_bytes(address)
}
}
public fun create_guardian_set(index: U32, guardians: vector<Guardian>): GuardianSet {
GuardianSet {
index: index,
guardians: guardians,
expiration_time: u32::from_u64(0),
}
}
public(friend) fun expire_guardian_set(guardian_set: &mut GuardianSet, delta: U32, ctx: &TxContext) {
guardian_set.expiration_time = u32::from_u64(tx_context::epoch(ctx) + u32::to_u64(delta));
}
public fun unpack_signature(s: &Signature): (vector<u8>, u8, u8) {
(s.sig, s.recovery_id, s.guardian_index)
}
public fun create_signature(
sig: vector<u8>,
recovery_id: u8,
guardian_index: u8
): Signature {
Signature{ sig, recovery_id, guardian_index }
}
public fun get_address(guardian: &Guardian): guardian_pubkey::Address {
guardian.address
}
public fun get_guardian_set_index(guardian_set: &GuardianSet): U32 {
guardian_set.index
}
public fun get_guardians(guardian_set: &GuardianSet): vector<Guardian> {
guardian_set.guardians
}
public fun get_guardian_set_expiry(guardian_set: &GuardianSet): U32 {
guardian_set.expiration_time
}
}

View File

@ -0,0 +1,94 @@
module wormhole::wormhole {
use sui::sui::{SUI};
use sui::coin::{Self, Coin};
use sui::tx_context::{Self, TxContext};
use sui::transfer::{Self};
//use wormhole::structs::{create_guardian, create_guardian_set};
use wormhole::state::{Self, State};
use wormhole::emitter::{Self};
const E_INSUFFICIENT_FEE: u64 = 0;
// -----------------------------------------------------------------------------
// Sending messages
public fun publish_message(
emitter_cap: &mut emitter::EmitterCapability,
state: &State,
nonce: u64,
payload: vector<u8>,
message_fee: Coin<SUI>,
): u64 {
// ensure that provided fee is sufficient to cover message fees
let expected_fee = state::get_message_fee(state);
assert!(expected_fee <= coin::value(&message_fee), E_INSUFFICIENT_FEE);
// deposit the fees into the wormhole account
transfer::transfer(message_fee, @wormhole);
// get sequence number
let sequence = emitter::use_sequence(emitter_cap);
// emit event
state::publish_event(
emitter::get_emitter(emitter_cap),
sequence,
nonce,
payload,
);
return sequence
}
public entry fun publish_message_entry(
emitter_cap: &mut emitter::EmitterCapability,
state: &State,
nonce: u64,
payload: vector<u8>,
message_fee: Coin<SUI>,
) {
publish_message(emitter_cap, state, nonce, payload, message_fee);
}
public entry fun publish_message_free(
emitter_cap: &mut emitter::EmitterCapability,
state: &mut State,
nonce: u64,
payload: vector<u8>,
) {
// ensure that provided fee is sufficient to cover message fees
let expected_fee = state::get_message_fee(state);
assert!(expected_fee == 0, E_INSUFFICIENT_FEE);
// get sender and sequence number
let sequence = emitter::use_sequence(emitter_cap);
// emit event
state::publish_event(
emitter::get_emitter(emitter_cap),
sequence,
nonce,
payload,
);
}
// -----------------------------------------------------------------------------
// Emitter registration
public fun register_emitter(state: &mut State, ctx: &mut TxContext): emitter::EmitterCapability {
state::new_emitter(state, ctx)
}
// -----------------------------------------------------------------------------
// get_new_emitter
//
// Honestly, unsure if this should survive once we get into code review but it
// sure makes writing my test script work quite well
//
// This creates a new emitter object and stores it away into the senders context.
//
// You can then use this to call publish_message_free and generate a vaa
public entry fun get_new_emitter(state: &mut State, ctx: &mut TxContext) {
transfer::transfer(state::new_emitter(state, ctx), tx_context::sender(ctx));
}
}