diff --git a/tests/e2e/e2e_convert_cosmos_coins_test.go b/tests/e2e/e2e_convert_cosmos_coins_test.go index c8271998..1c9d24a4 100644 --- a/tests/e2e/e2e_convert_cosmos_coins_test.go +++ b/tests/e2e/e2e_convert_cosmos_coins_test.go @@ -8,7 +8,9 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" txtypes "github.com/cosmos/cosmos-sdk/types/tx" - "github.com/ethereum/go-ethereum" + + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/kava-labs/kava/tests/e2e/testutil" "github.com/kava-labs/kava/tests/util" evmutiltypes "github.com/kava-labs/kava/x/evmutil/types" @@ -38,6 +40,50 @@ func setupConvertToCoinTest( return denom, initialFunds, user } +// amount must be less than 1e10 +func (suite *IntegrationTestSuite) setupAccountWithCosmosCoinERC20Balance( + accountName string, amount int64, +) (user *testutil.SigningAccount, contractAddress *evmutiltypes.InternalEVMAddress, denom string, sdkBalance sdk.Coins) { + if amount > 1e10 { + panic("test erc20 amount must be less than 1e10") + } + + denom, sdkBalance, user = setupConvertToCoinTest(suite, accountName) + convertAmount := sdk.NewInt64Coin(denom, amount) + + fee := sdk.NewCoins(ukava(7500)) + + // setup user to have erc20 balance + msg := evmutiltypes.NewMsgConvertCosmosCoinToERC20( + user.SdkAddress.String(), + user.EvmAddress.Hex(), + convertAmount, + ) + tx := util.KavaMsgRequest{ + Msgs: []sdk.Msg{&msg}, + GasLimit: 2e6, + FeeAmount: fee, + Data: "converting sdk coin to erc20", + } + res := user.SignAndBroadcastKavaTx(tx) + suite.NoError(res.Err) + + // adjust sdk balance + sdkBalance = sdkBalance.Sub(convertAmount) + + // query for the deployed contract + deployedContracts, err := suite.Kava.Evmutil.DeployedCosmosCoinContracts( + context.Background(), + &evmutiltypes.QueryDeployedCosmosCoinContractsRequest{CosmosDenoms: []string{denom}}, + ) + suite.NoError(err) + suite.Len(deployedContracts.DeployedCosmosCoinContracts, 1) + + contractAddress = deployedContracts.DeployedCosmosCoinContracts[0].Address + + return user, contractAddress, denom, sdkBalance +} + func (suite *IntegrationTestSuite) TestConvertCosmosCoinsToFromERC20() { denom, initialFunds, user := setupConvertToCoinTest(suite, "cosmo-coin-converter") @@ -73,12 +119,7 @@ func (suite *IntegrationTestSuite) TestConvertCosmosCoinsToFromERC20() { contractAddress := deployedContracts.DeployedCosmosCoinContracts[0].Address // check erc20 balance - bz, err := suite.Kava.EvmClient.CallContract(context.Background(), ethereum.CallMsg{ - To: &contractAddress.Address, - Data: util.BuildErc20BalanceOfCallData(user.EvmAddress), - }, nil) - suite.NoError(err) - erc20Balance := new(big.Int).SetBytes(bz) + erc20Balance := suite.Kava.GetErc20Balance(contractAddress.Address, user.EvmAddress) suite.BigIntsEqual(big.NewInt(convertAmount), erc20Balance, "unexpected erc20 balance post-convert") // check cosmos coin is deducted from account @@ -110,12 +151,7 @@ func (suite *IntegrationTestSuite) TestConvertCosmosCoinsToFromERC20() { suite.NoError(res.Err) // check erc20 balance - bz, err = suite.Kava.EvmClient.CallContract(context.Background(), ethereum.CallMsg{ - To: &contractAddress.Address, - Data: util.BuildErc20BalanceOfCallData(user.EvmAddress), - }, nil) - suite.NoError(err) - erc20Balance = new(big.Int).SetBytes(bz) + erc20Balance = suite.Kava.GetErc20Balance(contractAddress.Address, user.EvmAddress) suite.BigIntsEqual(big.NewInt(0), erc20Balance, "expected all erc20 to be converted back") // check cosmos coin is added back to account @@ -176,12 +212,7 @@ func (suite *IntegrationTestSuite) TestEIP712ConvertCosmosCoinsToFromERC20() { contractAddress := deployedContracts.DeployedCosmosCoinContracts[0].Address // check erc20 balance - bz, err := suite.Kava.EvmClient.CallContract(context.Background(), ethereum.CallMsg{ - To: &contractAddress.Address, - Data: util.BuildErc20BalanceOfCallData(user.EvmAddress), - }, nil) - suite.NoError(err) - erc20Balance := new(big.Int).SetBytes(bz) + erc20Balance := suite.Kava.GetErc20Balance(contractAddress.Address, user.EvmAddress) suite.BigIntsEqual(big.NewInt(convertAmount), erc20Balance, "unexpected erc20 balance post-convert") // check cosmos coin is deducted from account @@ -225,12 +256,7 @@ func (suite *IntegrationTestSuite) TestEIP712ConvertCosmosCoinsToFromERC20() { suite.NoError(err) // check erc20 balance - bz, err = suite.Kava.EvmClient.CallContract(context.Background(), ethereum.CallMsg{ - To: &contractAddress.Address, - Data: util.BuildErc20BalanceOfCallData(user.EvmAddress), - }, nil) - suite.NoError(err) - erc20Balance = new(big.Int).SetBytes(bz) + erc20Balance = suite.Kava.GetErc20Balance(contractAddress.Address, user.EvmAddress) suite.BigIntsEqual(big.NewInt(0), erc20Balance, "expected all erc20 to be converted back") // check cosmos coin is added back to account @@ -242,3 +268,159 @@ func (suite *IntegrationTestSuite) TestEIP712ConvertCosmosCoinsToFromERC20() { actualModuleBalance = suite.Kava.GetModuleBalances(evmutiltypes.ModuleName).AmountOf(denom) suite.Equal(initialModuleBalance, actualModuleBalance) } + +func (suite *IntegrationTestSuite) TestConvertCosmosCoins_ForbiddenERC20Calls() { + user, contractAddress, _, _ := suite.setupAccountWithCosmosCoinERC20Balance("cosmo-coin-converter-unhappy", 1e6) + + suite.Run("users can't mint()", func() { + data := util.BuildErc20MintCallData(user.EvmAddress, big.NewInt(1)) + nonce, err := user.NextNonce() + suite.NoError(err) + + mintTx := util.EvmTxRequest{ + Tx: ethtypes.NewTx( + ðtypes.LegacyTx{ + Nonce: nonce, + GasPrice: minEvmGasPrice, + Gas: 1e6, + To: &contractAddress.Address, + Data: data, + }, + ), + Data: "attempting to mint, should fail", + } + res := user.SignAndBroadcastEvmTx(mintTx) + + suite.ErrorAs(res.Err, &util.ErrEvmTxFailed) + // TODO: when traceTransactions enabled, read actual error log + suite.ErrorContains(res.Err, "transaction was committed but failed. likely an execution revert by contract code") + }) + + suite.Run("users can't burn()", func() { + data := util.BuildErc20BurnCallData(user.EvmAddress, big.NewInt(1)) + nonce, err := user.NextNonce() + suite.NoError(err) + + burnTx := util.EvmTxRequest{ + Tx: ethtypes.NewTx( + ðtypes.LegacyTx{ + Nonce: nonce, + GasPrice: minEvmGasPrice, + Gas: 1e6, + To: &contractAddress.Address, + Data: data, + }, + ), + Data: "attempting to burn, should fail", + } + res := user.SignAndBroadcastEvmTx(burnTx) + + suite.ErrorAs(res.Err, &util.ErrEvmTxFailed) + // TODO: when traceTransactions enabled, read actual error log + suite.ErrorContains(res.Err, "transaction was committed but failed. likely an execution revert by contract code") + }) +} + +// - check approval flow of erc20. alice approves bob to move their funds +// - check complex conversion flow. bob converts funds they receive on evm back to sdk.Coin +func (suite *IntegrationTestSuite) TestConvertCosmosCoins_ERC20Magic() { + fee := sdk.NewCoins(ukava(7500)) + initialAliceAmount := int64(2e6) + alice, contractAddress, denom, _ := suite.setupAccountWithCosmosCoinERC20Balance( + "cosmo-coin-converter-complex-alice", initialAliceAmount, + ) + + gasMoney := sdk.NewCoins(ukava(1e6)) + bob := suite.Kava.NewFundedAccount("cosmo-coin-converter-complex-bob", gasMoney) + amount := big.NewInt(1e6) + + // bob can't move alice's funds + nonce, err := bob.NextNonce() + suite.NoError(err) + transferFromTxData := ðtypes.LegacyTx{ + Nonce: nonce, + GasPrice: minEvmGasPrice, + Gas: 1e6, + To: &contractAddress.Address, + Value: &big.Int{}, + Data: util.BuildErc20TransferFromCallData(alice.EvmAddress, bob.EvmAddress, amount), + } + transferTx := util.EvmTxRequest{ + Tx: ethtypes.NewTx(transferFromTxData), + Data: "bob can't move alice's funds, should fail", + } + res := bob.SignAndBroadcastEvmTx(transferTx) + suite.ErrorAs(res.Err, &util.ErrEvmTxFailed) + suite.ErrorContains(res.Err, "transaction was committed but failed. likely an execution revert by contract code") + + // approve bob to move alice's funds + nonce, err = alice.NextNonce() + suite.NoError(err) + approveTx := util.EvmTxRequest{ + Tx: ethtypes.NewTx(ðtypes.LegacyTx{ + Nonce: nonce, + GasPrice: minEvmGasPrice, + Gas: 1e6, + To: &contractAddress.Address, + Value: &big.Int{}, + Data: util.BuildErc20ApproveCallData(bob.EvmAddress, amount), + }), + Data: "alice approves bob to spend amount", + } + res = alice.SignAndBroadcastEvmTx(approveTx) + suite.NoError(res.Err) + + // bob can't move more than alice allowed + transferFromTxData.Data = util.BuildErc20TransferFromCallData( + alice.EvmAddress, bob.EvmAddress, new(big.Int).Add(amount, big.NewInt(1)), + ) + transferFromTxData.Nonce, err = bob.NextNonce() + suite.NoError(err) + transferTooMuchTx := util.EvmTxRequest{ + Tx: ethtypes.NewTx(transferFromTxData), + Data: "transferring more than approved, should fail", + } + res = bob.SignAndBroadcastEvmTx(transferTooMuchTx) + suite.ErrorAs(res.Err, &util.ErrEvmTxFailed) + suite.ErrorContains(res.Err, "transaction was committed but failed. likely an execution revert by contract code") + + // bob can move allowed amount + transferFromTxData.Data = util.BuildErc20TransferFromCallData(alice.EvmAddress, bob.EvmAddress, amount) + transferFromTxData.Nonce, err = bob.NextNonce() + suite.NoError(err) + transferJustRightTx := util.EvmTxRequest{ + Tx: ethtypes.NewTx(transferFromTxData), + Data: "bob transfers alice's funds, allowed because he's approved", + } + res = bob.SignAndBroadcastEvmTx(transferJustRightTx) + suite.NoError(res.Err) + + // alice should have amount deducted + erc20Balance := suite.Kava.GetErc20Balance(contractAddress.Address, alice.EvmAddress) + suite.BigIntsEqual(big.NewInt(initialAliceAmount-amount.Int64()), erc20Balance, "alice has unexpected erc20 balance") + // bob should have amount added + erc20Balance = suite.Kava.GetErc20Balance(contractAddress.Address, bob.EvmAddress) + suite.BigIntsEqual(amount, erc20Balance, "bob has unexpected erc20 balance") + + // convert bob's new funds back to an sdk.Coin + convertMsg := evmutiltypes.NewMsgConvertCosmosCoinFromERC20( + bob.EvmAddress.Hex(), + bob.SdkAddress.String(), + sdk.NewInt64Coin(denom, amount.Int64()), + ) + convertTx := util.KavaMsgRequest{ + Msgs: []sdk.Msg{&convertMsg}, + GasLimit: 2e6, + FeeAmount: fee, + Data: "bob converts his new erc20 to an sdk.Coin", + } + convertRes := bob.SignAndBroadcastKavaTx(convertTx) + suite.NoError(convertRes.Err) + + // bob should have no more erc20 balance + erc20Balance = suite.Kava.GetErc20Balance(contractAddress.Address, bob.EvmAddress) + suite.BigIntsEqual(big.NewInt(0), erc20Balance, "expected no erc20 balance for bob") + // bob should have sdk balance + balance := suite.Kava.QuerySdkForBalances(bob.SdkAddress).AmountOf(denom) + suite.Equal(sdk.NewIntFromBigInt(amount), balance) +} diff --git a/tests/e2e/e2e_evm_contracts_test.go b/tests/e2e/e2e_evm_contracts_test.go index 36296d83..422cea79 100644 --- a/tests/e2e/e2e_evm_contracts_test.go +++ b/tests/e2e/e2e_evm_contracts_test.go @@ -50,14 +50,14 @@ func (suite *IntegrationTestSuite) TestEthCallToErc20() { amount := big.NewInt(1e6) // make unauthenticated eth_call query to check balance - beforeBalance := suite.GetErc20Balance(randoReceiver) + beforeBalance := suite.Kava.GetErc20Balance(suite.DeployedErc20Address, randoReceiver) // make authenticate eth_call to transfer tokens res := suite.FundKavaErc20Balance(randoReceiver, amount) suite.NoError(res.Err) // make another unauthenticated eth_call query to check new balance - afterBalance := suite.GetErc20Balance(randoReceiver) + afterBalance := suite.Kava.GetErc20Balance(suite.DeployedErc20Address, randoReceiver) suite.BigIntsEqual(big.NewInt(0), beforeBalance, "expected before balance to be zero") suite.BigIntsEqual(amount, afterBalance, "unexpected post-transfer balance") @@ -161,7 +161,7 @@ func (suite *IntegrationTestSuite) TestEip712ConvertToCoinAndDepositToEarn() { suite.NoError(err) // check that depositor no longer has erc20 balance - balance := suite.GetErc20Balance(depositor.EvmAddress) + balance := suite.Kava.GetErc20Balance(suite.DeployedErc20Address, depositor.EvmAddress) suite.BigIntsEqual(big.NewInt(0), balance, "expected no erc20 balance") // check that account has an earn deposit position diff --git a/tests/e2e/testutil/account.go b/tests/e2e/testutil/account.go index 6a180436..11b7afa2 100644 --- a/tests/e2e/testutil/account.go +++ b/tests/e2e/testutil/account.go @@ -1,6 +1,7 @@ package testutil import ( + "context" "encoding/hex" "fmt" "log" @@ -215,3 +216,8 @@ func (chain *Chain) NewFundedAccount(name string, funds sdk.Coins) *SigningAccou return acc } + +// GetNonce fetches the next nonce / sequence number for the account. +func (a *SigningAccount) NextNonce() (uint64, error) { + return a.evmSigner.EvmClient.PendingNonceAt(context.Background(), a.EvmAddress) +} diff --git a/tests/e2e/testutil/chain.go b/tests/e2e/testutil/chain.go index a9f35dca..b10e6777 100644 --- a/tests/e2e/testutil/chain.go +++ b/tests/e2e/testutil/chain.go @@ -3,6 +3,7 @@ package testutil import ( "context" "fmt" + "math/big" "testing" "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" @@ -11,6 +12,7 @@ import ( txtypes "github.com/cosmos/cosmos-sdk/types/tx" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" evmtypes "github.com/evmos/ethermint/x/evm/types" @@ -123,3 +125,13 @@ func (chain *Chain) GetModuleBalances(moduleName string) sdk.Coins { addr := authtypes.NewModuleAddress(moduleName) return chain.QuerySdkForBalances(addr) } + +func (chain *Chain) GetErc20Balance(contract, address common.Address) *big.Int { + resData, err := chain.EvmClient.CallContract(context.Background(), ethereum.CallMsg{ + To: &contract, + Data: util.BuildErc20BalanceOfCallData(address), + }, nil) + require.NoError(chain.t, err) + + return new(big.Int).SetBytes(resData) +} diff --git a/tests/e2e/testutil/init_evm.go b/tests/e2e/testutil/init_evm.go index 1658332c..6106ad53 100644 --- a/tests/e2e/testutil/init_evm.go +++ b/tests/e2e/testutil/init_evm.go @@ -5,7 +5,6 @@ import ( "fmt" "math/big" - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" @@ -19,7 +18,7 @@ func (suite *E2eTestSuite) InitKavaEvmData() { whale := suite.Kava.GetAccount(FundedAccountName) // ensure funded account has nonzero erc20 balance - balance := suite.GetErc20Balance(whale.EvmAddress) + balance := suite.Kava.GetErc20Balance(suite.DeployedErc20Address, whale.EvmAddress) if balance.Cmp(big.NewInt(0)) != 1 { panic(fmt.Sprintf("expected funded account (%s) to have erc20 balance", whale.EvmAddress.Hex())) } @@ -38,7 +37,7 @@ func (suite *E2eTestSuite) FundKavaErc20Balance(toAddress common.Address, amount // funded account should have erc20 balance whale := suite.Kava.GetAccount(FundedAccountName) - data := util.BuildErc20TransferCallData(whale.EvmAddress, toAddress, amount) + data := util.BuildErc20TransferCallData(toAddress, amount) nonce, err := suite.Kava.EvmClient.PendingNonceAt(context.Background(), whale.EvmAddress) suite.NoError(err) @@ -49,13 +48,3 @@ func (suite *E2eTestSuite) FundKavaErc20Balance(toAddress common.Address, amount return whale.SignAndBroadcastEvmTx(req) } - -func (suite *E2eTestSuite) GetErc20Balance(address common.Address) *big.Int { - resData, err := suite.Kava.EvmClient.CallContract(context.Background(), ethereum.CallMsg{ - To: &suite.DeployedErc20Address, - Data: util.BuildErc20BalanceOfCallData(address), - }, nil) - suite.NoError(err) - - return new(big.Int).SetBytes(resData) -} diff --git a/tests/util/erc20.go b/tests/util/erc20.go index 86c8cc6c..fafee4bc 100644 --- a/tests/util/erc20.go +++ b/tests/util/erc20.go @@ -15,7 +15,20 @@ func EvmContractMethodId(signature string) []byte { return hash.Sum(nil)[:4] } -func BuildErc20TransferCallData(from common.Address, to common.Address, amount *big.Int) []byte { +func BuildErc20ApproveCallData(spender common.Address, amount *big.Int) []byte { + methodId := EvmContractMethodId("approve(address,uint256)") + paddedAddress := common.LeftPadBytes(spender.Bytes(), 32) + paddedAmount := common.LeftPadBytes(amount.Bytes(), 32) + + var data []byte + data = append(data, methodId...) + data = append(data, paddedAddress...) + data = append(data, paddedAmount...) + + return data +} + +func BuildErc20TransferCallData(to common.Address, amount *big.Int) []byte { methodId := EvmContractMethodId("transfer(address,uint256)") paddedAddress := common.LeftPadBytes(to.Bytes(), 32) paddedAmount := common.LeftPadBytes(amount.Bytes(), 32) @@ -28,6 +41,47 @@ func BuildErc20TransferCallData(from common.Address, to common.Address, amount * return data } +func BuildErc20TransferFromCallData(from common.Address, to common.Address, amount *big.Int) []byte { + methodId := EvmContractMethodId("transferFrom(address,address,uint256)") + paddedFrom := common.LeftPadBytes(from.Bytes(), 32) + paddedTo := common.LeftPadBytes(to.Bytes(), 32) + paddedAmount := common.LeftPadBytes(amount.Bytes(), 32) + + var data []byte + data = append(data, methodId...) + data = append(data, paddedFrom...) + data = append(data, paddedTo...) + data = append(data, paddedAmount...) + + return data +} + +func BuildErc20MintCallData(to common.Address, amount *big.Int) []byte { + methodId := EvmContractMethodId("mint(address,uint256)") + paddedAddress := common.LeftPadBytes(to.Bytes(), 32) + paddedAmount := common.LeftPadBytes(amount.Bytes(), 32) + + var data []byte + data = append(data, methodId...) + data = append(data, paddedAddress...) + data = append(data, paddedAmount...) + + return data +} + +func BuildErc20BurnCallData(from common.Address, amount *big.Int) []byte { + methodId := EvmContractMethodId("burn(address,uint256)") + paddedAddress := common.LeftPadBytes(from.Bytes(), 32) + paddedAmount := common.LeftPadBytes(amount.Bytes(), 32) + + var data []byte + data = append(data, methodId...) + data = append(data, paddedAddress...) + data = append(data, paddedAmount...) + + return data +} + func BuildErc20BalanceOfCallData(address common.Address) []byte { methodId := EvmContractMethodId("balanceOf(address)") paddedAddress := common.LeftPadBytes(address.Bytes(), 32)