Terra integration added to e2e tests

This commit is contained in:
Yuriy Savchenko 2020-12-31 14:47:09 +02:00 committed by Leopold Schabel
parent eb9c4f2c52
commit eeb560cb5c
10 changed files with 484 additions and 12 deletions

View File

@ -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)).

View File

@ -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,
)
})
}

View File

@ -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
bridge/e2e/terra.go Normal file
View File

@ -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))
}

View File

@ -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=

View File

@ -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"
)

View File

@ -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

View File

@ -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::*;

View File

@ -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 {

View File

@ -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,
}