wormhole/bridge/e2e/terra.go

387 lines
10 KiB
Go

package e2e
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"math"
"math/big"
"net/http"
"net/url"
"testing"
"time"
"github.com/certusone/wormhole/bridge/pkg/devnet"
"github.com/certusone/wormhole/bridge/pkg/ethereum"
"github.com/certusone/wormhole/bridge/pkg/ethereum/erc20"
"github.com/certusone/wormhole/bridge/pkg/vaa"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"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, url.QueryEscape(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, 90*time.Second)
defer cancel()
assetAddress := ""
err := wait.PollUntil(3*time.Second, func() (bool, error) {
address, err := getAssetAddress(ctx, contract, chain, asset)
if err != nil {
t.Log(err)
return false, nil
}
// Check the case if request was successful, but asset address is not yet in the registry
if address == "" {
return false, nil
}
t.Logf("Returning asset: %s", address)
assetAddress = address
return true, 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, 60*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, 90*time.Second)
defer cancel()
err = wait.PollUntil(3*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))
}
func testTerraToEthLockup(t *testing.T, ctx context.Context, tc *TerraClient,
ec *ethclient.Client, tokenAddr string, destination common.Address, amount int64, precisionGain int) {
token, err := erc20.NewErc20(destination, ec)
if err != nil {
panic(err)
}
// Store balance of source CW20 token
beforeCw20, err := getTerraBalance(ctx, tokenAddr)
if err != nil {
t.Log(err) // account may not yet exist, defaults to 0
beforeCw20 = new(big.Int)
}
t.Logf("CW20 balance: %v", beforeCw20)
/// Store balance of wrapped destination token
beforeErc20, err := token.BalanceOf(nil, devnet.GanacheClientDefaultAccountAddress)
if err != nil {
t.Log(err) // account may not yet exist, defaults to 0
beforeErc20 = new(big.Int)
}
t.Logf("ERC20 balance: %v", beforeErc20)
// Send lockup
tx, err := tc.lockAssets(
t, ctx,
// asset address
tokenAddr,
// token amount
new(big.Int).SetInt64(amount),
// recipient address on target chain
ethereum.PadAddress(devnet.GanacheClientDefaultAccountAddress),
// target chain
vaa.ChainIDEthereum,
// random nonce
rand.Uint32(),
)
if err != nil {
t.Error(err)
}
t.Logf("sent lockup tx: %s", tx.TxHash)
// Destination account increases by full amount.
waitEthBalance(t, ctx, token, beforeErc20, int64(float64(amount)*math.Pow10(precisionGain)))
// Source account decreases by the full amount.
waitTerraBalance(t, ctx, tokenAddr, beforeCw20, -int64(amount))
}