package main_test import ( "context" "crypto/sha256" "encoding/json" "fmt" "math/big" "path/filepath" "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" gov1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" paramsutils "github.com/cosmos/cosmos-sdk/x/params/client/utils" transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/strangelove-ventures/interchaintest/v7" "github.com/strangelove-ventures/interchaintest/v7/chain/cosmos" "github.com/strangelove-ventures/interchaintest/v7/ibc" "github.com/strangelove-ventures/interchaintest/v7/testreporter" "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/client/erc20" "github.com/kava-labs/kava/tests/e2e/runner" "github.com/kava-labs/kava/tests/e2e/testutil" kavainterchain "github.com/kava-labs/kava/tests/interchain" "github.com/kava-labs/kava/tests/util" evmutiltypes "github.com/kava-labs/kava/x/evmutil/types" ) // This test does the following: // - set up a network with kava & cosmoshub w/ IBC channels between them // - deploy an ERC20 to Kava EVM // - configure Kava to support conversion of ERC20 to Cosmos Coin // - IBC the sdk.Coin representation of the ERC20 to cosmoshub func TestInterchainErc20(t *testing.T) { app.SetSDKConfig() ctx := context.Background() // Configure & run chains with interchaintest cf := interchaintest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*interchaintest.ChainSpec{ {Name: "kava", ChainConfig: kavainterchain.DefaultKavaChainConfig(kavainterchain.KavaTestChainId)}, {Name: "gaia", Version: "v15.2.0", ChainConfig: ibc.ChainConfig{GasPrices: "0.0uatom"}}, }) chains, err := cf.Chains(t.Name()) require.NoError(t, err) ictKava := chains[0].(*cosmos.CosmosChain) gaia := chains[1].(*cosmos.CosmosChain) client, network := interchaintest.DockerSetup(t) r := interchaintest.NewBuiltinRelayerFactory(ibc.CosmosRly, zaptest.NewLogger(t)). Build(t, client, network) // configure interchain const kavaGaiaIbcPath = "kava-gaia-ibc" ic := interchaintest.NewInterchain().AddChain(ictKava). AddChain(gaia). AddRelayer(r, "relayer"). AddLink(interchaintest.InterchainLink{ Chain1: ictKava, Chain2: gaia, Relayer: r, Path: kavaGaiaIbcPath, }) // Log location f, err := interchaintest.CreateLogFile(fmt.Sprintf("%d.json", time.Now().Unix())) require.NoError(t, err) // Reporter/logs rep := testreporter.NewReporter(f) eRep := rep.RelayerExecReporter(t) // Build interchain err = ic.Build(ctx, eRep, interchaintest.InterchainBuildOptions{ TestName: t.Name(), Client: client, NetworkID: network, SkipPathCreation: false}, ) require.NoError(t, err) // Create and Fund User Wallets fundAmount := math.NewInt(1e12) users := interchaintest.GetAndFundTestUsers(t, ctx, t.Name(), fundAmount, ictKava, gaia) kavaUser := users[0] gaiaUser := users[1] // wait for new block to ensure initial funding complete height, err := ictKava.Height(ctx) require.NoError(t, err) h := height for h <= height { h, err = ictKava.Height(ctx) require.NoError(t, err) } gaiaChannelInfo, err := r.GetChannels(ctx, eRep, gaia.Config().ChainID) require.NoError(t, err) gaiaToKavaChannelID := gaiaChannelInfo[0].ChannelID kavaToGaiaChannelID := gaiaChannelInfo[0].Counterparty.ChannelID // for simplified management of the chain, use kava's e2e framework for account management // this skirts problems in interchaintest with needing coin type 60 // there are exceptions in the relayer & ibc channel management that complicate setting the chain // default coin type to 60 in the chain config. // we need to fund an account and then all of kava's e2e testutil chain management will work. rpcUrl, err := ictKava.FullNodes[0].GetHostAddress(ctx, "26657/tcp") require.NoError(t, err, "failed to find rpc URL") grpcUrl, err := ictKava.FullNodes[0].GetHostAddress(ctx, "9090/tcp") require.NoError(t, err, "failed to find grpc URL") evmUrl, err := ictKava.FullNodes[0].GetHostAddress(ctx, "8545/tcp") require.NoError(t, err, "failed to find evm URL") evmClient, err := ethclient.Dial(evmUrl) require.NoError(t, err, "failed to connect to evm") // create a funded evm account to initialize the testutil.Chain // use testutil.Chain for account management because the accounts play nicely with EVM & SDK sides deployerMnemonic, err := util.RandomMnemonic() require.NoError(t, err) evmDeployer, err := util.NewEvmSignerFromMnemonic(evmClient, big.NewInt(kavainterchain.KavaEvmTestChainId), deployerMnemonic) require.NoError(t, err) deployerKavaAddr := util.EvmToSdkAddress(evmDeployer.Address()) err = ictKava.SendFunds(ctx, kavaUser.KeyName(), ibc.WalletAmount{ Address: deployerKavaAddr.String(), Denom: "ukava", Amount: math.NewInt(1e10), }) require.NoError(t, err) // initialize testutil.Chain for account & tx management on both sdk & evm kava, err := testutil.NewChain(t, &runner.ChainDetails{ RpcUrl: rpcUrl, GrpcUrl: grpcUrl, EvmRpcUrl: evmUrl, ChainId: kavainterchain.KavaTestChainId, StakingDenom: "ukava", }, deployerMnemonic) require.NoError(t, err) deployer := kava.GetAccount("whale") // deploy ERC20 contract usdtAddr, deployTx, usdt, err := erc20.DeployErc20( deployer.EvmAuth, kava.EvmClient, "Test Tether USD", "USDT", 6, ) require.NoError(t, err) require.NotNil(t, usdtAddr) require.NotNil(t, usdt) _, err = util.WaitForEvmTxReceipt(kava.EvmClient, deployTx.Hash(), 10*time.Second) require.NoError(t, err) //////////////////////////////////////////// // enable conversion from erc20 -> sdk.Coin // (assumes there are none pre-configured!) //////////////////////////////////////////// // 1. Submit Proposal sdkDenom := "tether/usdt" rawCps, err := json.Marshal(evmutiltypes.NewConversionPairs( evmutiltypes.NewConversionPair( evmutiltypes.NewInternalEVMAddress(usdtAddr), sdkDenom, ), )) require.NoError(t, err) paramChange := paramsutils.ParamChangeProposalJSON{ Title: "Enable erc20 conversion to sdk.Coin", Description: ".", Changes: paramsutils.ParamChangesJSON{ paramsutils.ParamChangeJSON{ Subspace: "evmutil", Key: "EnabledConversionPairs", Value: rawCps, }, }, Deposit: "10000000ukava", } _, err = legacyParamChangeProposal(ictKava.FullNodes[0], ctx, kavaUser.KeyName(), ¶mChange) require.NoError(t, err, "error submitting param change proposal tx") // TODO: query proposal id. assuming it is 1 here. propId := int64(1) // 2. Vote on Proposal err = ictKava.VoteOnProposalAllValidators(ctx, propId, cosmos.ProposalVoteYes) require.NoError(t, err, "failed to submit votes") height, _ = ictKava.Height(ctx) _, err = cosmos.PollForProposalStatus(ctx, ictKava, height, height+10, propId, gov1beta1.StatusPassed) require.NoError(t, err, "proposal status did not change to passed in expected number of blocks") // fund a user & mint them some usdt user := kava.NewFundedAccount("tether-user", sdk.NewCoins(sdk.NewCoin("ukava", math.NewInt(1e7)))) erc20FundAmt := big.NewInt(100e6) mintTx, err := usdt.Mint(deployer.EvmAuth, user.EvmAddress, erc20FundAmt) require.NoError(t, err) _, err = util.WaitForEvmTxReceipt(kava.EvmClient, mintTx.Hash(), 10*time.Second) require.NoError(t, err) // verify they have erc20 balance! bal, err := usdt.BalanceOf(nil, user.EvmAddress) require.NoError(t, err) require.Equal(t, erc20FundAmt, bal) // convert the erc20 to sdk.Coin! amountToConvert := math.NewInt(50e6) msg := evmutiltypes.NewMsgConvertERC20ToCoin( evmutiltypes.NewInternalEVMAddress(user.EvmAddress), user.SdkAddress, evmutiltypes.NewInternalEVMAddress(usdtAddr), amountToConvert, ) convertTx := util.KavaMsgRequest{ Msgs: []sdk.Msg{&msg}, GasLimit: 4e5, FeeAmount: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(400))), Data: "converting sdk coin to erc20", } res := user.SignAndBroadcastKavaTx(convertTx) require.NoError(t, res.Err) // check balance of cosmos coin! sdkBalance := kava.QuerySdkForBalances(user.SdkAddress) require.Equal(t, amountToConvert, sdkBalance.AmountOf(sdkDenom)) // IBC the newly minted sdk.Coin to gaia dstAddress := gaiaUser.FormattedAddress() transfer := ibc.WalletAmount{ Address: dstAddress, Denom: ictKava.Config().Denom, Amount: amountToConvert, } _, err = ictKava.SendIBCTransfer(ctx, kavaToGaiaChannelID, kavaUser.KeyName(), transfer, ibc.TransferOptions{}) require.NoError(t, err) // manually flush packets so we don't need to wait for the relayer require.NoError(t, r.Flush(ctx, eRep, kavaGaiaIbcPath, kavaToGaiaChannelID)) // determine IBC denom & check gaia balance srcDenomTrace := transfertypes.ParseDenomTrace(transfertypes.GetPrefixedDenom("transfer", gaiaToKavaChannelID, ictKava.Config().Denom)) erc20OnGaiaDenom := srcDenomTrace.IBCDenom() gaiaBal, err := gaia.GetBalance(ctx, dstAddress, erc20OnGaiaDenom) require.NoError(t, err) require.Equal(t, amountToConvert, gaiaBal) } // copied from https://github.com/strangelove-ventures/interchaintest/blob/7272afc780da6e2c99b2d2b3d084d5a3b1c6895f/chain/cosmos/chain_node.go#L1292 // but changed "submit-proposal" to "submit-legacy-proposal" func legacyParamChangeProposal(tn *cosmos.ChainNode, ctx context.Context, keyName string, prop *paramsutils.ParamChangeProposalJSON) (string, error) { content, err := json.Marshal(prop) if err != nil { return "", err } hash := sha256.Sum256(content) proposalFilename := fmt.Sprintf("%x.json", hash) err = tn.WriteFile(ctx, content, proposalFilename) if err != nil { return "", fmt.Errorf("writing param change proposal: %w", err) } proposalPath := filepath.Join(tn.HomeDir(), proposalFilename) command := []string{ "gov", "submit-legacy-proposal", "param-change", proposalPath, } return tn.ExecTx(ctx, keyName, command...) }