test(e2e): test more complex cosmos assert conversion (#1616)

* ensure users can mint() or burn() erc20s

* refactor test setup

* refactor GetErc20Balance for arbitrary erc20 contracts

* move GetErc20Balance() to Chain

* test complex erc20 operations & convert

* undo changes to e2e env
This commit is contained in:
Robert Pirtle 2023-06-06 09:40:17 -07:00 committed by GitHub
parent 0ec64c9378
commit c90c7a8647
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 285 additions and 42 deletions

View File

@ -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(
&ethtypes.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(
&ethtypes.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 := &ethtypes.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(&ethtypes.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)
}

View File

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

View File

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

View File

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

View File

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

View File

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