Forráskód Böngészése

Terra integration added to e2e tests

Yuriy Savchenko 4 éve
szülő
commit
eeb560cb5c

+ 2 - 0
DEVELOP.md

@@ -127,6 +127,8 @@ After a few seconds, the SPL token balance shown below will increase as the VAA
 | `3C3m4tjTy4nSMkkYdqCDSiCWEgpDa6whvprvABdFGBiW` | Account that holds 6qRhs8oA... SPL tokens           |
 | `85kW19uNvETzH43p3AfpyqPaQS5rWouq4x9rGiKUvihf` | Wrapped token for the 0xCfEB86... ERC20 token       |
 | `7EFk3VrWeb29SWJPQs5cUyqcY3fQd33S9gELkGybRzeu` | Account that holds 85kW19u... wrapped tokens [2]    |
+| `9ESkHLgJH4zqbG7fvhpC9u2ZeHMoLJznCHtaRLviEVRh` | Wrapped token for the terra18vd8f... CW20 token     |
+| `EERzaqe8Agm8p1ZkGQFq9zKpP7MDW29FX1pC1vEw9Yfv` | Account that holds 9ESkHLg... wrapped tokens        |
 
 [1]: The account will eventually run out of funds if you run the lockup sending scripts for a long time. Refill it
 using `kubectl exec solana-devnet-0 -c setup cli airdrop solana-devnet:9900` (see [devnet_setup.sh](solana/devnet_setup.sh)).

+ 32 - 2
bridge/e2e/e2e_test.go

@@ -5,9 +5,8 @@ import (
 	"testing"
 	"time"
 
-	"github.com/ethereum/go-ethereum/ethclient"
-
 	"github.com/certusone/wormhole/bridge/pkg/devnet"
+	"github.com/ethereum/go-ethereum/ethclient"
 )
 
 func TestEndToEnd(t *testing.T) {
@@ -47,6 +46,12 @@ func TestEndToEnd(t *testing.T) {
 	}
 	kt := devnet.GetKeyedTransactor(ctx)
 
+	// Terra client
+	tc, err := NewTerraClient()
+	if err != nil {
+		t.Fatalf("creating devnet terra client failed: %v", err)
+	}
+
 	// Generic context for tests.
 	ctx, cancel = context.WithCancel(context.Background())
 	defer cancel()
@@ -107,4 +112,29 @@ func TestEndToEnd(t *testing.T) {
 			9,
 		)
 	})
+
+	t.Run("[Terra] Native -> [SOL] Wrapped", func(t *testing.T) {
+		testTerraLockup(t, ctx, tc, c,
+			// Source CW20 token
+			devnet.TerraTokenAddress,
+			// Destination SPL token account
+			devnet.SolanaExampleWrappedCWTokenOwningAccount,
+			// Amount
+			2*devnet.TerraDefaultPrecision,
+			// Same precision - same amount, no precision gained.
+			0,
+		)
+	})
+	t.Run("[SOL] Native -> [Terra] Wrapped", func(t *testing.T) {
+		testSolanaToTerraLockup(t, ctx, tc, c,
+			// Source SPL account
+			devnet.SolanaExampleTokenOwningAccount,
+			// Source SPL token
+			devnet.SolanaExampleToken,
+			// Amount of SPL token value to transfer.
+			50*devnet.SolanaDefaultPrecision,
+			// Same precision - same amount, no precision gained.
+			0,
+		)
+	})
 }

+ 52 - 0
bridge/e2e/solana.go

@@ -12,6 +12,7 @@ import (
 
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/ethclient"
+	"github.com/mr-tron/base58"
 	"github.com/tendermint/tendermint/libs/rand"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/client-go/kubernetes"
@@ -116,3 +117,54 @@ func testSolanaLockup(t *testing.T, ctx context.Context, ec *ethclient.Client, c
 	// Source account decreases by full amount.
 	waitSPLBalance(t, ctx, c, sourceAcct, beforeSPL, -int64(amount))
 }
+
+func testSolanaToTerraLockup(t *testing.T, ctx context.Context, tc *TerraClient, c *kubernetes.Clientset,
+	sourceAcct string, tokenAddr string, amount int, precisionGain int) {
+
+	tokenSlice, err := base58.Decode(tokenAddr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	terraToken, err := getAssetAddress(ctx, devnet.TerraBridgeAddress, vaa.ChainIDSolana, tokenSlice)
+
+	// Get balance if deployed
+	beforeCw20, err := getTerraBalance(ctx, terraToken)
+	if err != nil {
+		t.Log(err) // account may not yet exist, defaults to 0
+	}
+	t.Logf("CW20 balance: %v", beforeCw20)
+
+	// Store balance of source SPL token
+	beforeSPL, err := getSPLBalance(ctx, c, sourceAcct)
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Logf("SPL balance: %d", beforeSPL)
+
+	_, err = executeCommandInPod(ctx, c, "solana-devnet-0", "setup",
+		[]string{"cli", "lock",
+			// Address of the Wormhole bridge.
+			devnet.SolanaBridgeContract,
+			// Account which holds the SPL tokens to be sent.
+			sourceAcct,
+			// The SPL token.
+			tokenAddr,
+			// Token amount.
+			strconv.Itoa(amount),
+			// Destination chain ID.
+			strconv.Itoa(vaa.ChainIDTerra),
+			// Random nonce.
+			strconv.Itoa(int(rand.Uint16())),
+			// Destination account on Terra
+			devnet.TerraMainTestAddressHex,
+		})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Source account decreases by full amount.
+	waitSPLBalance(t, ctx, c, sourceAcct, beforeSPL, -int64(amount))
+
+	// Destination account increases by the full amount.
+	waitTerraUnknownBalance(t, ctx, devnet.TerraBridgeAddress, vaa.ChainIDSolana, tokenSlice, beforeCw20, int64(float64(amount)*math.Pow10(precisionGain)))
+}

+ 324 - 0
bridge/e2e/terra.go

@@ -0,0 +1,324 @@
+package e2e
+
+import (
+	"context"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"math"
+	"math/big"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/certusone/wormhole/bridge/pkg/devnet"
+	"github.com/certusone/wormhole/bridge/pkg/vaa"
+	"github.com/tendermint/tendermint/libs/rand"
+	"github.com/terra-project/terra.go/client"
+	"github.com/terra-project/terra.go/key"
+	"github.com/terra-project/terra.go/msg"
+	"github.com/terra-project/terra.go/tx"
+	"github.com/tidwall/gjson"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/client-go/kubernetes"
+)
+
+type lockAssetsMsg struct {
+	Params lockAssetsParams `json:"lock_assets"`
+}
+
+type increaseAllowanceMsg struct {
+	Params increaseAllowanceParams `json:"increase_allowance"`
+}
+
+type lockAssetsParams struct {
+	Asset       string `json:"asset"`
+	Amount      string `json:"amount"`
+	Recipient   []byte `json:"recipient"`
+	TargetChain uint8  `json:"target_chain"`
+	Nonce       uint32 `json:"nonce"`
+}
+
+type increaseAllowanceParams struct {
+	Spender string `json:"spender"`
+	Amount  string `json:"amount"`
+}
+
+// TerraClient encapsulates Terra LCD client and fee payer signing address
+type TerraClient struct {
+	lcdClient client.LCDClient
+	address   msg.AccAddress
+}
+
+func (tc TerraClient) lockAssets(t *testing.T, ctx context.Context, token string, amount *big.Int, recipient [32]byte, targetChain uint8, nonce uint32) (*client.TxResponse, error) {
+	bridgeContract, err := msg.AccAddressFromBech32(devnet.TerraBridgeAddress)
+	if err != nil {
+		return nil, err
+	}
+
+	tokenContract, err := msg.AccAddressFromBech32(token)
+	if err != nil {
+		return nil, err
+	}
+
+	// Create tx
+	increaseAllowanceCall, err := json.Marshal(increaseAllowanceMsg{
+		Params: increaseAllowanceParams{
+			Spender: devnet.TerraBridgeAddress,
+			Amount:  amount.String(),
+		}})
+
+	if err != nil {
+		return nil, err
+	}
+
+	lockAssetsCall, err := json.Marshal(lockAssetsMsg{
+		Params: lockAssetsParams{
+			Asset:       token,
+			Amount:      amount.String(),
+			Recipient:   recipient[:],
+			TargetChain: targetChain,
+			Nonce:       nonce,
+		}})
+
+	if err != nil {
+		return nil, err
+	}
+
+	t.Logf("increaseAllowanceCall\n %s", increaseAllowanceCall)
+	t.Logf("lockAssetsCall\n %s", lockAssetsCall)
+
+	executeIncreaseAllowance := msg.NewExecuteContract(tc.address, tokenContract, increaseAllowanceCall, msg.NewCoins())
+	executeLockAssets := msg.NewExecuteContract(tc.address, bridgeContract, lockAssetsCall, msg.NewCoins())
+
+	transaction, err := tc.lcdClient.CreateAndSignTx(ctx, client.CreateTxOptions{
+		Msgs: []msg.Msg{
+			executeIncreaseAllowance,
+			executeLockAssets,
+		},
+		Fee: tx.StdFee{
+			Gas:    msg.NewInt(0),
+			Amount: msg.NewCoins(),
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	// Broadcast
+	return tc.lcdClient.Broadcast(ctx, transaction)
+}
+
+// NewTerraClient creates new TerraClient instance to work
+func NewTerraClient() (*TerraClient, error) {
+	// Derive Raw Private Key
+	privKey, err := key.DerivePrivKey(devnet.TerraFeePayerKey, key.CreateHDPath(0, 0))
+	if err != nil {
+		return nil, err
+	}
+
+	// Generate StdPrivKey
+	tmKey, err := key.StdPrivKeyGen(privKey)
+	if err != nil {
+		return nil, err
+	}
+
+	// Generate Address from Public Key
+	address := msg.AccAddress(tmKey.PubKey().Address())
+
+	// Terra client
+	lcdClient := client.NewLCDClient(
+		devnet.TerraLCDURL,
+		devnet.TerraChainID,
+		msg.NewDecCoinFromDec("uusd", msg.NewDecFromIntWithPrec(msg.NewInt(15), 2)), // 0.15uusd
+		msg.NewDecFromIntWithPrec(msg.NewInt(15), 1), tmKey, time.Second*15,
+	)
+
+	return &TerraClient{
+		lcdClient: *lcdClient,
+		address:   address,
+	}, nil
+}
+
+func getTerraBalance(ctx context.Context, token string) (*big.Int, error) {
+	json, err := terraQuery(ctx, token, fmt.Sprintf("{\"balance\":{\"address\":\"%s\"}}", devnet.TerraMainTestAddress))
+	if err != nil {
+		return nil, err
+	}
+	balance := gjson.Get(json, "result.balance").String()
+	parsed, success := new(big.Int).SetString(balance, 10)
+
+	if !success {
+		return nil, fmt.Errorf("cannot parse balance: %s", balance)
+	}
+
+	return parsed, nil
+}
+
+func getAssetAddress(ctx context.Context, contract string, chain uint8, asset []byte) (string, error) {
+	json, err := terraQuery(ctx, contract, fmt.Sprintf("{\"wrapped_registry\":{\"chain\":%d,\"address\":\"%s\"}}",
+		chain,
+		base64.StdEncoding.EncodeToString(asset)))
+	if err != nil {
+		return "", err
+	}
+	return gjson.Get(json, "result.address").String(), nil
+}
+
+func terraQuery(ctx context.Context, contract string, query string) (string, error) {
+
+	requestURL := fmt.Sprintf("%s/wasm/contracts/%s/store?query_msg=%s", devnet.TerraLCDURL, contract, query)
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
+	if err != nil {
+		return "", fmt.Errorf("http request error: %w", err)
+	}
+
+	client := &http.Client{
+		Timeout: time.Second * 15,
+	}
+	resp, err := client.Do(req)
+	if err != nil {
+		return "", fmt.Errorf("http execution error: %w", err)
+	}
+
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return "", fmt.Errorf("http read error: %w", err)
+	}
+
+	return string(body), nil
+}
+
+// waitTerraAsset waits for asset contract to be deployed on terra
+func waitTerraAsset(t *testing.T, ctx context.Context, contract string, chain uint8, asset []byte) (string, error) {
+	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
+	defer cancel()
+
+	assetAddress := ""
+
+	err := wait.PollUntil(1*time.Second, func() (bool, error) {
+
+		address, err := getAssetAddress(ctx, contract, chain, asset)
+		if err != nil {
+			t.Log(err)
+			return true, nil
+		}
+
+		assetAddress = address
+		return false, nil
+	}, ctx.Done())
+
+	if err != nil {
+		t.Error(err)
+	}
+	return assetAddress, err
+}
+
+// waitTerraBalance waits for target account before to increase.
+func waitTerraBalance(t *testing.T, ctx context.Context, token string, before *big.Int, target int64) {
+	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
+	defer cancel()
+
+	err := wait.PollUntil(1*time.Second, func() (bool, error) {
+
+		after, err := getTerraBalance(ctx, token)
+		if err != nil {
+			return false, err
+		}
+
+		d := new(big.Int).Sub(after, before)
+		t.Logf("CW20 balance after: %d -> %d, delta %d", before, after, d)
+
+		if after.Cmp(before) != 0 {
+			if d.Cmp(new(big.Int).SetInt64(target)) != 0 {
+				t.Errorf("expected CW20 delta of %v, got: %v", target, d)
+			}
+			return true, nil
+		}
+		return false, nil
+	}, ctx.Done())
+
+	if err != nil {
+		t.Error(err)
+	}
+}
+
+func waitTerraUnknownBalance(t *testing.T, ctx context.Context, contract string, chain uint8, asset []byte, before *big.Int, target int64) {
+
+	token, err := waitTerraAsset(t, ctx, contract, chain, asset)
+	if err != nil {
+		return
+	}
+
+	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
+	defer cancel()
+
+	err = wait.PollUntil(1*time.Second, func() (bool, error) {
+
+		after, err := getTerraBalance(ctx, token)
+		if err != nil {
+			return false, err
+		}
+
+		d := new(big.Int).Sub(after, before)
+		t.Logf("CW20 balance after: %d -> %d, delta %d", before, after, d)
+
+		if after.Cmp(before) != 0 {
+			if d.Cmp(new(big.Int).SetInt64(target)) != 0 {
+				t.Errorf("expected CW20 delta of %v, got: %v", target, d)
+			}
+			return true, nil
+		}
+		return false, nil
+	}, ctx.Done())
+
+	if err != nil {
+		t.Error(err)
+	}
+}
+
+func testTerraLockup(t *testing.T, ctx context.Context, tc *TerraClient,
+	c *kubernetes.Clientset, token string, destination string, amount int64, precisionLoss int) {
+
+	// Store balance of source CW20 token
+	beforeCw20, err := getTerraBalance(ctx, token)
+	if err != nil {
+		t.Log(err) // account may not yet exist, defaults to 0
+	}
+	t.Logf("CW20 balance: %v", beforeCw20)
+
+	// Store balance of destination SPL token
+	beforeSPL, err := getSPLBalance(ctx, c, destination)
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Logf("SPL balance: %d", beforeSPL)
+
+	// Send lockup
+	tx, err := tc.lockAssets(
+		t, ctx,
+		// asset address
+		token,
+		// token amount
+		new(big.Int).SetInt64(amount),
+		// recipient address on target chain
+		devnet.MustBase58ToEthAddress(destination),
+		// target chain
+		vaa.ChainIDSolana,
+		// random nonce
+		rand.Uint32(),
+	)
+	if err != nil {
+		t.Error(err)
+	}
+
+	t.Logf("sent lockup tx: %s", tx.TxHash)
+
+	// Destination account increases by full amount.
+	waitSPLBalance(t, ctx, c, destination, beforeSPL, int64(float64(amount)/math.Pow10(precisionLoss)))
+
+	// Source account decreases by the full amount.
+	waitTerraBalance(t, ctx, token, beforeCw20, -int64(amount))
+}

+ 2 - 0
bridge/go.sum

@@ -1187,6 +1187,7 @@ github.com/tendermint/tendermint v0.33.8 h1:Xxu4QhpqcomSE0iQDw1MqLgfsa8fqtPtWFJK
 github.com/tendermint/tendermint v0.33.8/go.mod h1:0yUs9eIuuDq07nQql9BmI30FtYGcEC60Tu5JzB5IezM=
 github.com/tendermint/tm-db v0.5.1 h1:H9HDq8UEA7Eeg13kdYckkgwwkQLBnJGgX4PgLJRhieY=
 github.com/tendermint/tm-db v0.5.1/go.mod h1:g92zWjHpCYlEvQXvy9M168Su8V1IBEeawpXVVBaK4f4=
+github.com/terra-project/terra.go v1.0.0 h1:TR2b3x8yrljXhrs9a3KORRCQ6BGr+bCTp0ZwTrG/i3c=
 github.com/terra-project/terra.go v1.0.1-0.20201113170042-b3bffdc6fd06 h1:TAhaL+7VYJe44qBEKqjlj3wD0CRjJN1JZfz8p+L6FGY=
 github.com/terra-project/terra.go v1.0.1-0.20201113170042-b3bffdc6fd06/go.mod h1:elzj1F6B9Sel3c4QFNeR3yR4E9tu+c1xBP+ZZYPlSq8=
 github.com/tidwall/gjson v1.6.3 h1:aHoiiem0dr7GHkW001T1SMTJ7X5PvyekH5WX0whWGnI=
@@ -1634,6 +1635,7 @@ k8s.io/api v0.19.4 h1:I+1I4cgJYuCDgiLNjKx7SLmIbwgj9w7N7Zr5vSIdwpo=
 k8s.io/api v0.19.4/go.mod h1:SbtJ2aHCItirzdJ36YslycFNzWADYH3tgOhvBEFtZAk=
 k8s.io/apimachinery v0.19.4 h1:+ZoddM7nbzrDCp0T3SWnyxqf8cbWPT2fkZImoyvHUG0=
 k8s.io/apimachinery v0.19.4/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA=
+k8s.io/apimachinery v0.20.1 h1:LAhz8pKbgR8tUwn7boK+b2HZdt7MiTu2mkYtFMUjTRQ=
 k8s.io/client-go v0.19.4 h1:85D3mDNoLF+xqpyE9Dh/OtrJDyJrSRKkHmDXIbEzer8=
 k8s.io/client-go v0.19.4/go.mod h1:ZrEy7+wj9PjH5VMBCuu/BDlvtUAku0oVFk4MmnW9mWA=
 k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=

+ 23 - 0
bridge/pkg/devnet/constants.go

@@ -52,12 +52,35 @@ const (
 	SolanaExampleWrappedERCToken              = "85kW19uNvETzH43p3AfpyqPaQS5rWouq4x9rGiKUvihf"
 	SolanaExampleWrappedERCTokenOwningAccount = "7EFk3VrWeb29SWJPQs5cUyqcY3fQd33S9gELkGybRzeu"
 
+	// Wrapped CW20 token
+	SolanaExampleWrappedCWToken              = "9ESkHLgJH4zqbG7fvhpC9u2ZeHMoLJznCHtaRLviEVRh"
+	SolanaExampleWrappedCWTokenOwningAccount = "EERzaqe8Agm8p1ZkGQFq9zKpP7MDW29FX1pC1vEw9Yfv"
+
 	// Lamports per SOL.
 	SolanaDefaultPrecision = 1e9
 
 	// ERC20 default precision.
 	ERC20DefaultPrecision = 1e18
 
+	// CW20 default precision.
+	TerraDefaultPrecision = 1e8
+
+	// Terra LCD url
+	TerraLCDURL = "http://localhost:1317"
+
+	// Terra test chain ID
+	TerraChainID = "localterra"
+
+	// Terra main test address to send/receive tokens
+	TerraMainTestAddress    = "terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v"
+	TerraMainTestAddressHex = "00000000000000000000000035743074956c710800e83198011ccbd4ddf1556d"
+
+	// Terra token address
+	TerraTokenAddress = "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5"
+
+	// Terra bridge contract address
+	TerraBridgeAddress = "terra174kgn5rtw4kf6f938wm7kwh70h2v4vcfd26jlc"
+
 	// Terra devnet fee payer mnemonic
 	TerraFeePayerKey = "notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius"
 )

+ 7 - 1
solana/devnet_setup.sh

@@ -43,12 +43,18 @@ cli mint "$token" 10000000000 "$account"
 
 # Create wrapped asset for the token we mint in send-lockups.js (2 = Ethereum, 9 decimals)
 wrapped_token=$(cli create-wrapped "$bridge_address" 2 9 000000000000000000000000CfEB869F69431e42cdB54A4F4f105C19C080A601 | grep 'Wrapped Mint address' | awk '{ print $4 }')
-echo "Created wrapped token $token"
+echo "Created wrapped token $wrapped_token"
 
 # Create token account to receive wrapped assets from send-lockups.js
 wrapped_account=$(cli create-account --seed=934893 "$wrapped_token" | grep 'Creating account' | awk '{ print $3 }')
 echo "Created wrapped token account $wrapped_account"
 
+# Create wrapped asset and token account for Terra tokens (3 for Terra, 8 for precision)
+wrapped_terra_token=$(cli create-wrapped "$bridge_address" 3 8 0000000000000000000000003b1a7485c6162c5883ee45fb2d7477a87d8a4ce5 | grep 'Wrapped Mint address' | awk '{ print $4 }')
+echo "Created wrapped token for Terra $wrapped_terra_token"
+wrapped_terra_account=$(cli create-account --seed=736251 "$wrapped_terra_token" | grep 'Creating account' | awk '{ print $3 }')
+echo "Created wrapped token account for Terra $wrapped_terra_account"
+
 # Let k8s startup probe succeed
 nc -l -p 2000
 

+ 30 - 9
terra/contracts/wormhole/src/contract.rs

@@ -1,3 +1,4 @@
+use crate::msg::WrappedRegistryResponse;
 use cosmwasm_std::{
     log, to_binary, Api, Binary, CanonicalAddr, CosmosMsg, Env, Extern, HandleResponse, HumanAddr,
     InitResponse, Querier, QueryRequest, StdResult, Storage, Uint128, WasmMsg, WasmQuery,
@@ -337,14 +338,8 @@ fn vaa_transfer<S: Storage, A: Api, Q: Querier>(
     }
 
     if token_chain != CHAIN_ID {
-        let mut asset_id: Vec<u8> = vec![];
-        asset_id.push(token_chain);
         let asset_address = data.get_bytes32(71);
-        asset_id.extend_from_slice(asset_address);
-
-        let mut hasher = Keccak256::new();
-        hasher.update(asset_id);
-        let asset_id = hasher.finalize();
+        let asset_id = build_asset_id(token_chain, asset_address);
 
         let mut messages: Vec<CosmosMsg> = vec![];
 
@@ -539,11 +534,14 @@ pub fn query<S: Storage, A: Api, Q: Querier>(
     msg: QueryMsg,
 ) -> StdResult<Binary> {
     match msg {
-        QueryMsg::GuardianSetInfo {} => to_binary(&query_query_guardian_set_info(deps)?),
+        QueryMsg::GuardianSetInfo {} => to_binary(&query_guardian_set_info(deps)?),
+        QueryMsg::WrappedRegistry { chain, address } => {
+            to_binary(&query_wrapped_registry(deps, chain, address.as_slice())?)
+        }
     }
 }
 
-pub fn query_query_guardian_set_info<S: Storage, A: Api, Q: Querier>(
+pub fn query_guardian_set_info<S: Storage, A: Api, Q: Querier>(
     deps: &Extern<S, A, Q>,
 ) -> StdResult<GuardianSetInfoResponse> {
     let state = config_read(&deps.storage).load()?;
@@ -555,6 +553,19 @@ pub fn query_query_guardian_set_info<S: Storage, A: Api, Q: Querier>(
     Ok(res)
 }
 
+pub fn query_wrapped_registry<S: Storage, A: Api, Q: Querier>(
+    deps: &Extern<S, A, Q>,
+    chain: u8,
+    address: &[u8],
+) -> StdResult<WrappedRegistryResponse> {
+    let asset_id = build_asset_id(chain, address);
+    // Check if this asset is already deployed
+    match wrapped_asset_read(&deps.storage).load(&asset_id) {
+        Ok(address) => Ok(WrappedRegistryResponse { address }),
+        Err(_) => ContractError::AssetNotFound.std_err(),
+    }
+}
+
 fn keys_equal(a: &VerifyKey, b: &GuardianAddress) -> bool {
     let mut hasher = Keccak256::new();
 
@@ -580,6 +591,16 @@ fn keys_equal(a: &VerifyKey, b: &GuardianAddress) -> bool {
     true
 }
 
+fn build_asset_id(chain: u8, address: &[u8]) -> Vec<u8> {
+    let mut asset_id: Vec<u8> = vec![];
+    asset_id.push(chain);
+    asset_id.extend_from_slice(address);
+
+    let mut hasher = Keccak256::new();
+    hasher.update(asset_id);
+    hasher.finalize().to_vec()
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

+ 4 - 0
terra/contracts/wormhole/src/error.rs

@@ -86,6 +86,10 @@ pub enum ContractError {
     /// More signatures than active guardians found
     #[error("TooManySignatures")]
     TooManySignatures,
+
+    /// Wrapped asset not found in the registry
+    #[error("AssetNotFound")]
+    AssetNotFound,
 }
 
 impl ContractError {

+ 8 - 0
terra/contracts/wormhole/src/msg.rs

@@ -36,10 +36,18 @@ pub enum HandleMsg {
 #[serde(rename_all = "snake_case")]
 pub enum QueryMsg {
     GuardianSetInfo {},
+    WrappedRegistry { chain: u8, address: Binary },
 }
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
 pub struct GuardianSetInfoResponse {
     pub guardian_set_index: u32,         // Current guardian set index
     pub addresses: Vec<GuardianAddress>, // List of querdian addresses
 }
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct WrappedRegistryResponse {
+    pub address: HumanAddr,
+}