diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f98f8bec..a1c99bffa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,10 +17,6 @@ and code reviews are our most important tools to accomplish that. - Releases are first tested on a testnet. This involves coordination with the mainnet DAO running the nodes. -- We aim for close to 100% test coverage on all critical paths. Most of Wormhole's complexity is in the - external interfaces, therefore, we primarily rely on software-in-the-loop E2E testing that exercises - the entire path. Where applicable, we also use faster unit tests for invariant checking. - - Commits should be small and have a meaningful commit message. One commit should, roughly, be "one idea" and be as atomic as possible. A feature can consist of many such commits. @@ -50,7 +46,7 @@ The answer is... maybe? The following things are needed in order to fully suppor - The smart contract needs to be built and audited. In some cases, existing contracts can be used, like with EVM-compatible chains. -- Support for observing the chain needs to be added to guardiand, along with E2E tests. +- Support for observing the chain needs to be added to guardiand. - Web wallet integration needs to be built to actually interact with Wormhole. diff --git a/DEVELOP.md b/DEVELOP.md index c501462cd..d4a57e473 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -61,11 +61,6 @@ Generate test Solana -> Ethereum transfers: scripts/send-solana-lockups.sh -Run end-to-end tests: - - cd bridge - go test github.com/certusone/wormhole/bridge/e2e - Adjust number of nodes in running cluster: (this is only useful if you want to test scenarios where the number of nodes diverges from the guardian set - otherwise, `tilt down --delete-namespaces` and restart the cluster) diff --git a/bridge/e2e/e2e_test.go b/bridge/e2e/e2e_test.go deleted file mode 100644 index 30c1b375c..000000000 --- a/bridge/e2e/e2e_test.go +++ /dev/null @@ -1,262 +0,0 @@ -package e2e - -import ( - "context" - "testing" - "time" - - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/mr-tron/base58" - "k8s.io/client-go/kubernetes" - - "github.com/certusone/wormhole/bridge/pkg/devnet" - "github.com/certusone/wormhole/bridge/pkg/ethereum" - "github.com/certusone/wormhole/bridge/pkg/vaa" - "github.com/ethereum/go-ethereum/ethclient" -) - -func setup(t *testing.T) (*kubernetes.Clientset, *ethclient.Client, *bind.TransactOpts, *TerraClient) { - // List of pods we need in a ready state before we can run tests. - want := []string{ - // Our test guardian set. - "guardian-0", - //"guardian-1", - //"guardian-2", - //"guardian-3", - //"guardian-4", - //"guardian-5", - - // Connected chains - "solana-devnet-0", - - "terra-terrad-0", - "terra-lcd-0", - - "eth-devnet-0", - } - - c := getk8sClient() - - // Wait for all pods to be ready. This blocks until the bridge is ready to receive lockups. - ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) - defer cancel() - waitForPods(ctx, c, want) - if ctx.Err() != nil { - t.Fatal(ctx.Err()) - } - - // Ethereum client. - ec, err := ethclient.Dial(devnet.GanacheRPCURL) - if err != nil { - t.Fatalf("dialing devnet eth rpc failed: %v", err) - } - kt := devnet.GetKeyedTransactor(context.Background()) - - // Terra client - tc, err := NewTerraClient() - if err != nil { - t.Fatalf("creating devnet terra client failed: %v", err) - } - - return c, ec, kt, tc -} - -// Careful about parallel tests - accounts on some chains like Ethereum cannot be -// used concurrently as they have monotonically increasing nonces that would conflict. -// Either use different Ethereum account, or do not run Ethereum tests in parallel. - -func TestEndToEnd_SOL_ETH(t *testing.T) { - c, ec, kt, _ := setup(t) - - t.Run("[SOL] Native -> [ETH] Wrapped", func(t *testing.T) { - testSolanaLockup(t, context.Background(), ec, c, - // Source SPL account - devnet.SolanaExampleTokenOwningAccount, - // Source SPL token - devnet.SolanaExampleToken, - // Our wrapped destination token on Ethereum - devnet.GanacheExampleERC20WrappedSOL, - // Amount of SPL token value to transfer. - 50*devnet.SolanaDefaultPrecision, - // Same precision - same amount, no precision gained. - 0, - ) - }) - - t.Run("[ETH] Wrapped -> [SOL] Native", func(t *testing.T) { - testEthereumLockup(t, context.Background(), ec, kt, c, - // Source ERC20 token - devnet.GanacheExampleERC20WrappedSOL, - // Destination SPL token account - devnet.SolanaExampleTokenOwningAccount, - // Amount (the reverse of what the previous test did, with the same precision because - // the wrapped ERC20 is set to the original asset's 10**9 precision). - 50*devnet.SolanaDefaultPrecision, - // No precision loss - 0, - ) - }) - - t.Run("[ETH] Native -> [SOL] Wrapped", func(t *testing.T) { - testEthereumLockup(t, context.Background(), ec, kt, c, - // Source ERC20 token - devnet.GanacheExampleERC20Token, - // Destination SPL token account - devnet.SolanaExampleWrappedERCTokenOwningAccount, - // Amount - 0.000000012*devnet.ERC20DefaultPrecision, - // We lose 9 digits of precision on this path, as the default ERC20 token has 10**18 precision. - 9, - ) - }) - - t.Run("[SOL] Wrapped -> [ETH] Native", func(t *testing.T) { - testSolanaLockup(t, context.Background(), ec, c, - // Source SPL account - devnet.SolanaExampleWrappedERCTokenOwningAccount, - // Source SPL token - devnet.SolanaExampleWrappedERCToken, - // Our wrapped destination token on Ethereum - devnet.GanacheExampleERC20Token, - // Amount of SPL token value to transfer. - 0.000000012*devnet.SolanaDefaultPrecision, - // We gain 9 digits of precision on Eth. - 9, - ) - }) -} - -func TestEndToEnd_SOL_Terra(t *testing.T) { - c, _, _, tc := setup(t) - - t.Run("[Terra] Native -> [SOL] Wrapped", func(t *testing.T) { - testTerraLockup(t, context.Background(), 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] Wrapped -> [Terra] Native", func(t *testing.T) { - testSolanaToTerraLockup(t, context.Background(), c, - // Source SPL account - devnet.SolanaExampleWrappedCWTokenOwningAccount, - // Source SPL token - devnet.SolanaExampleWrappedCWToken, - // Wrapped - false, - // Amount of SPL token value to transfer. - 2*devnet.TerraDefaultPrecision, - // Same precision - same amount, no precision gained. - 0, - ) - }) - - t.Run("[SOL] Native -> [Terra] Wrapped", func(t *testing.T) { - testSolanaToTerraLockup(t, context.Background(), c, - // Source SPL account - devnet.SolanaExampleTokenOwningAccount, - // Source SPL token - devnet.SolanaExampleToken, - // Native - true, - // Amount of SPL token value to transfer. - 50*devnet.SolanaDefaultPrecision, - // Same precision - same amount, no precision gained. - 0, - ) - }) - - t.Run("[Terra] Wrapped -> [SOL] Native", func(t *testing.T) { - - tokenSlice, err := base58.Decode(devnet.SolanaExampleToken) - if err != nil { - t.Fatal(err) - } - wrappedAsset, err := waitTerraAsset(t, context.Background(), devnet.TerraBridgeAddress, vaa.ChainIDSolana, tokenSlice) - - if err != nil { - t.Fatal(err) - } - - testTerraLockup(t, context.Background(), tc, c, - // Source wrapped token - wrappedAsset, - // Destination SPL token account - devnet.SolanaExampleTokenOwningAccount, - // Amount of Terra token value to transfer. - 50*devnet.SolanaDefaultPrecision, - // Same precision - 0, - ) - }) -} - -func TestEndToEnd_ETH_Terra(t *testing.T) { - _, ec, kt, tc := setup(t) - - t.Run("[Terra] Native -> [ETH] Wrapped", func(t *testing.T) { - testTerraToEthLockup(t, context.Background(), tc, ec, - // Source CW20 token - devnet.TerraTokenAddress, - // Destination ETH token - devnet.GanacheExampleERC20WrappedTerra, - // Amount - 2*devnet.TerraDefaultPrecision, - // Same precision - same amount, no precision gained. - 0, - ) - }) - - t.Run("[ETH] Wrapped -> [Terra] Native", func(t *testing.T) { - testEthereumToTerraLockup(t, context.Background(), ec, kt, - // Source Ethereum token - devnet.GanacheExampleERC20WrappedTerra, - // Wrapped - false, - // Amount of Ethereum token value to transfer. - 2*devnet.TerraDefaultPrecision, - // Same precision - 0, - ) - }) - - t.Run("[ETH] Native -> [Terra] Wrapped", func(t *testing.T) { - testEthereumToTerraLockup(t, context.Background(), ec, kt, - // Source Ethereum token - devnet.GanacheExampleERC20Token, - // Native - true, - // Amount of Ethereum token value to transfer. - 0.000000012*devnet.ERC20DefaultPrecision, - // We lose 9 digits of precision on this path, as the default ERC20 token has 10**18 precision. - 9, - ) - }) - - t.Run("[Terra] Wrapped -> [ETH] Native", func(t *testing.T) { - - paddedTokenAddress := ethereum.PadAddress(devnet.GanacheExampleERC20Token) - wrappedAsset, err := waitTerraAsset(t, context.Background(), devnet.TerraBridgeAddress, vaa.ChainIDEthereum, paddedTokenAddress[:]) - - if err != nil { - t.Fatal(err) - } - - testTerraToEthLockup(t, context.Background(), tc, ec, - // Source wrapped token - wrappedAsset, - // Destination ETH token - devnet.GanacheExampleERC20Token, - // Amount of Terra token value to transfer. - 0.000000012*1e9, // 10**9 because default ETH precision is 18 and we lost 9 digits on wrapping - // We gain 9 digits of precision on Eth. - 9, - ) - }) -} diff --git a/bridge/e2e/eth.go b/bridge/e2e/eth.go deleted file mode 100644 index d0bd4c8ae..000000000 --- a/bridge/e2e/eth.go +++ /dev/null @@ -1,190 +0,0 @@ -package e2e - -import ( - "context" - "encoding/hex" - "math" - "math/big" - "testing" - "time" - - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/tendermint/tendermint/libs/rand" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/kubernetes" - - "github.com/certusone/wormhole/bridge/pkg/devnet" - "github.com/certusone/wormhole/bridge/pkg/ethereum" - "github.com/certusone/wormhole/bridge/pkg/ethereum/abi" - "github.com/certusone/wormhole/bridge/pkg/ethereum/erc20" - "github.com/certusone/wormhole/bridge/pkg/vaa" -) - -// waitEthBalance waits for target account before to increase. -func waitEthBalance(t *testing.T, ctx context.Context, token *erc20.Erc20, 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 := token.BalanceOf(nil, devnet.GanacheClientDefaultAccountAddress) - if err != nil { - t.Log(err) - return false, nil - } - - d := new(big.Int).Sub(after, before) - t.Logf("ERC20 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 ERC20 delta of %v, got: %v", target, d) - } - return true, nil - } - return false, nil - }, ctx.Done()) - - if err != nil { - t.Error(err) - } -} - -func testEthereumLockup(t *testing.T, ctx context.Context, ec *ethclient.Client, kt *bind.TransactOpts, - c *kubernetes.Clientset, tokenAddr common.Address, destination string, amount int64, precisionLoss int) { - - // Bridge client - ethBridge, err := abi.NewAbi(devnet.GanacheBridgeContractAddress, ec) - if err != nil { - panic(err) - } - - // Source token client - token, err := erc20.NewErc20(tokenAddr, ec) - if err != nil { - panic(err) - } - - // Store balance of source ERC20 token - beforeErc20, err := token.BalanceOf(nil, devnet.GanacheClientDefaultAccountAddress) - if err != nil { - t.Log(err) // account may not yet exist, defaults to 0 - } - t.Logf("ERC20 balance: %v", beforeErc20) - - // 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 := ethBridge.LockAssets(kt, - // asset address - tokenAddr, - // token amount - new(big.Int).SetInt64(amount), - // recipient address on target chain - devnet.MustBase58ToEthAddress(destination), - // target chain - vaa.ChainIDSolana, - // random nonce - rand.Uint32(), - // refund dust? - false, - ) - if err != nil { - t.Fatal(err) - } - - t.Logf("sent lockup tx: %v", tx.Hash().Hex()) - - // 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. - waitEthBalance(t, ctx, token, beforeErc20, -int64(amount)) -} - -func testEthereumToTerraLockup(t *testing.T, ctx context.Context, ec *ethclient.Client, kt *bind.TransactOpts, - tokenAddr common.Address, isNative bool, amount int64, precisionLoss int) { - - // Bridge client - ethBridge, err := abi.NewAbi(devnet.GanacheBridgeContractAddress, ec) - if err != nil { - panic(err) - } - - // Source token client - token, err := erc20.NewErc20(tokenAddr, ec) - if err != nil { - panic(err) - } - - // Store balance of source ERC20 token - beforeErc20, err := token.BalanceOf(nil, devnet.GanacheClientDefaultAccountAddress) - if err != nil { - beforeErc20 = new(big.Int) - t.Log(err) // account may not yet exist, defaults to 0 - } - t.Logf("ERC20 balance: %v", beforeErc20) - - // Store balance of destination CW20 token - paddedTokenAddress := ethereum.PadAddress(tokenAddr) - var terraToken string - if isNative { - terraToken, err = getAssetAddress(ctx, devnet.TerraBridgeAddress, vaa.ChainIDEthereum, paddedTokenAddress[:]) - if err != nil { - t.Log(err) - } - } else { - terraToken = devnet.TerraTokenAddress - } - - // Get balance if deployed - beforeCw20, err := getTerraBalance(ctx, terraToken) - if err != nil { - beforeCw20 = new(big.Int) - t.Log(err) // account may not yet exist, defaults to 0 - } - t.Logf("CW20 balance: %v", beforeCw20) - - // Send lockup - dstAddress, err := hex.DecodeString(devnet.TerraMainTestAddressHex) - if err != nil { - t.Fatal(err) - } - var dstAddressBytes [32]byte - copy(dstAddressBytes[:], dstAddress) - tx, err := ethBridge.LockAssets(kt, - // asset address - tokenAddr, - // token amount - new(big.Int).SetInt64(amount), - // recipient address on target chain - dstAddressBytes, - // target chain - vaa.ChainIDTerra, - // random nonce - rand.Uint32(), - // refund dust? - false, - ) - if err != nil { - t.Fatal(err) - } - - t.Logf("sent lockup tx: %v", tx.Hash().Hex()) - - // Destination account increases by the full amount. - if isNative { - waitTerraUnknownBalance(t, ctx, devnet.TerraBridgeAddress, vaa.ChainIDEthereum, paddedTokenAddress[:], beforeCw20, int64(float64(amount)/math.Pow10(precisionLoss))) - } else { - waitTerraBalance(t, ctx, devnet.TerraTokenAddress, beforeCw20, int64(float64(amount)/math.Pow10(precisionLoss))) - } - - // Source account decreases by the full amount. - waitEthBalance(t, ctx, token, beforeErc20, -int64(amount)) -} diff --git a/bridge/e2e/k8s.go b/bridge/e2e/k8s.go deleted file mode 100644 index 2a9390be2..000000000 --- a/bridge/e2e/k8s.go +++ /dev/null @@ -1,149 +0,0 @@ -package e2e - -import ( - "bytes" - "context" - "fmt" - "log" - "strings" - - "github.com/golang/glog" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/fields" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/cache" - "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/tools/remotecommand" -) - -const ( - TiltDefaultNamespace = "wormhole" // hardcoded in Tiltfile -) - -func getk8sClient() *kubernetes.Clientset { - config, err := getk8sConfig() - - c, err := kubernetes.NewForConfig(config) - if err != nil { - glog.Errorln(err) - } - return c -} - -func getk8sConfig() (*rest.Config, error) { - // Load local default kubeconfig. - rules := clientcmd.NewDefaultClientConfigLoadingRules() - rules.DefaultClientConfig = &clientcmd.DefaultClientConfig - return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( - clientcmd.ClientConfigLoader(rules), nil).ClientConfig() -} - -func hasPodReadyCondition(conditions []corev1.PodCondition) bool { - for _, condition := range conditions { - if condition.Type == corev1.PodReady && condition.Status == corev1.ConditionTrue { - return true - } - } - return false -} - -func waitForPods(ctx context.Context, c *kubernetes.Clientset, want []string) { - found := make(map[string]bool) - ctx, cancel := context.WithCancel(ctx) - - watchlist := cache.NewListWatchFromClient( - c.CoreV1().RESTClient(), - "pods", - TiltDefaultNamespace, - fields.Everything(), - ) - - handle := func(pod *corev1.Pod) { - ready := hasPodReadyCondition(pod.Status.Conditions) - log.Printf("pod added/changed: %s is %s, ready: %v", pod.Name, pod.Status.Phase, ready) - - if ready { - found[pod.Name] = true - } - - missing := 0 - for _, v := range want { - if found[v] == false { - missing += 1 - } - } - - if missing == 0 { - cancel() - } - } - - _, controller := cache.NewInformer( - watchlist, - &corev1.Pod{}, - 0, - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { handle(obj.(*corev1.Pod)) }, - UpdateFunc: func(oldObj, newObj interface{}) { handle(newObj.(*corev1.Pod)) }, - }, - ) - - controller.Run(ctx.Done()) -} - -func executeCommandInPod(ctx context.Context, c *kubernetes.Clientset, podName string, container string, cmd []string) ([]byte, error) { - p, err := c.CoreV1().Pods(TiltDefaultNamespace).Get(ctx, podName, metav1.GetOptions{}) - if err != nil { - return nil, fmt.Errorf("failed to get pod %s: %w", p, err) - } - - req := c.CoreV1().RESTClient().Post(). - Resource("pods"). - Name(podName). - Namespace(TiltDefaultNamespace). - SubResource("exec") - - req = req.VersionedParams(&corev1.PodExecOptions{ - Stdin: false, - Stdout: true, - Stderr: true, - TTY: false, - Container: container, - Command: cmd, - }, scheme.ParameterCodec) - - config, err := getk8sConfig() - if err != nil { - return nil, fmt.Errorf("failed to get config: %w", err) - } - - exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) - if err != nil { - return nil, fmt.Errorf("failed to init executor: %w", err) - } - - var ( - execOut bytes.Buffer - execErr bytes.Buffer - ) - - err = exec.Stream(remotecommand.StreamOptions{ - Stdout: &execOut, - Stderr: &execErr, - Tty: false, - }) - - log.Printf("command: %s", strings.Join(cmd, " ")) - if execErr.Len() > 0 { - log.Printf("stderr: %s", execErr.String()) - } - - if err != nil { - return nil, fmt.Errorf("failed to execute remote command: %w", err) - } - - return execOut.Bytes(), nil -} diff --git a/bridge/e2e/solana.go b/bridge/e2e/solana.go deleted file mode 100644 index 766891148..000000000 --- a/bridge/e2e/solana.go +++ /dev/null @@ -1,184 +0,0 @@ -package e2e - -import ( - "context" - "fmt" - "math" - "math/big" - "regexp" - "strconv" - "testing" - "time" - - "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" - - "github.com/certusone/wormhole/bridge/pkg/devnet" - "github.com/certusone/wormhole/bridge/pkg/ethereum/erc20" - "github.com/certusone/wormhole/bridge/pkg/vaa" -) - -func getSPLBalance(ctx context.Context, c *kubernetes.Clientset, hexAddr string) (*big.Int, error) { - b, err := executeCommandInPod(ctx, c, "solana-devnet-0", "setup", - []string{"cli", "balance", hexAddr}) - if err != nil { - return nil, fmt.Errorf("error running 'cli balance': %w", err) - } - - re := regexp.MustCompile("(?m)^amount: (.*)$") - m := re.FindStringSubmatch(string(b)) - if len(m) == 0 { - return nil, fmt.Errorf("invalid 'cli balance' output: %s", string(b)) - } - - n, ok := new(big.Int).SetString(m[1], 10) - if !ok { - return nil, fmt.Errorf("invalid int: %s", m[1]) - } - - return n, nil -} - -func waitSPLBalance(t *testing.T, ctx context.Context, c *kubernetes.Clientset, hexAddr string, before *big.Int, target int64) { - // Wait for target account balance to increase. - ctx, cancel := context.WithTimeout(ctx, 30*time.Second) - defer cancel() - err := wait.PollUntil(1*time.Second, func() (bool, error) { - after, err := getSPLBalance(ctx, c, hexAddr) - if err != nil { - t.Fatal(err) - } - - d := new(big.Int).Sub(after, before) - t.Logf("SPL 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 SPL delta of %v, got: %v", target, d) - } - return true, nil - } - return false, nil - }, ctx.Done()) - if err != nil { - t.Error(err) - } -} - -func testSolanaLockup(t *testing.T, ctx context.Context, ec *ethclient.Client, c *kubernetes.Clientset, - sourceAcct string, tokenAddr string, destination common.Address, amount int, precisionGain int) { - token, err := erc20.NewErc20(destination, ec) - if err != nil { - panic(err) - } - - // Store balance of wrapped destination token - beforeErc20, err := token.BalanceOf(nil, devnet.GanacheClientDefaultAccountAddress) - if err != nil { - beforeErc20 = new(big.Int) - t.Log(err) // account may not yet exist, defaults to 0 - } - t.Logf("ERC20 balance: %v", beforeErc20) - - // 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.ChainIDEthereum), - // Random nonce. - strconv.Itoa(int(rand.Uint16())), - // Destination account on Ethereum - devnet.GanacheClientDefaultAccountAddress.Hex()[2:], - }) - if err != nil { - t.Fatal(err) - } - - // Destination account increases by the full amount. - waitEthBalance(t, ctx, token, beforeErc20, int64(float64(amount)*math.Pow10(precisionGain))) - - // Source account decreases by full amount. - waitSPLBalance(t, ctx, c, sourceAcct, beforeSPL, -int64(amount)) -} - -func testSolanaToTerraLockup(t *testing.T, ctx context.Context, c *kubernetes.Clientset, - sourceAcct string, tokenAddr string, isNative bool, amount int, precisionGain int) { - - tokenSlice, err := base58.Decode(tokenAddr) - if err != nil { - t.Fatal(err) - } - var terraToken string - if isNative { - terraToken, err = getAssetAddress(ctx, devnet.TerraBridgeAddress, vaa.ChainIDSolana, tokenSlice) - if err != nil { - t.Log(err) - } - } else { - terraToken = devnet.TerraTokenAddress - } - - // Get balance if deployed - beforeCw20, err := getTerraBalance(ctx, terraToken) - if err != nil { - beforeCw20 = new(big.Int) - 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. - if isNative { - waitTerraUnknownBalance(t, ctx, devnet.TerraBridgeAddress, vaa.ChainIDSolana, tokenSlice, beforeCw20, int64(float64(amount)*math.Pow10(precisionGain))) - } else { - waitTerraBalance(t, ctx, devnet.TerraTokenAddress, beforeCw20, int64(float64(amount)*math.Pow10(precisionGain))) - } -} diff --git a/bridge/e2e/terra.go b/bridge/e2e/terra.go deleted file mode 100644 index ff5af430a..000000000 --- a/bridge/e2e/terra.go +++ /dev/null @@ -1,391 +0,0 @@ -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 -} - -const ( - feeAmount = 10000 - feeDenomination = "uluna" -) - -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(msg.NewInt64Coin(feeDenomination, feeAmount))) - - 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)) -}