feat: e2e test eip712 signing & erc20 interactions (#1535)

* add helpers & tests for erc20 eth_call query & transfer

* make encoding config public

* add evm client & raw evm signer to account

* test eip712 signing and broadcast

* update for cosmos v46

* update kvtool

* temporarily disable ibc tests & skip shutdown

* disable all but eip712 test and massively simplify

* add EIP712 tx builder & setup basic MsgSend test

* reenable all tests

* add eip712 test that deposits erc20 into earn

* update kvtool to master branch
This commit is contained in:
Robert Pirtle 2023-04-06 10:51:13 -07:00 committed by GitHub
parent 91e7933a55
commit 3366b3b3e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 406 additions and 8 deletions

View File

@ -1,4 +1,5 @@
# E2E_KAVA_FUNDED_ACCOUNT_MNEMONIC is for a funded account used to intialize all new testing accounts.
# Should be funded with KAVA and have an ERC20 balance
E2E_KAVA_FUNDED_ACCOUNT_MNEMONIC='tent fitness boat among census primary pipe nose dream glance cave turtle electric fabric jacket shaft easy myself genuine this sibling pulse word unfold'
# E2E_KVTOOL_KAVA_CONFIG_TEMPLATE is the kvtool template used to start the chain. See the `kava.configTemplate` flag in kvtool.
@ -22,3 +23,7 @@ E2E_KAVA_UPGRADE_NAME=
E2E_KAVA_UPGRADE_HEIGHT=
# E2E_KAVA_UPGRADE_BASE_IMAGE_TAG is the tag of the docker image the chain should upgrade from.
E2E_KAVA_UPGRADE_BASE_IMAGE_TAG=
# E2E_KAVA_ERC20_ADDRESS is the address of a pre-deployed ERC20 token.
# The E2E_KAVA_FUNDED_ACCOUNT_MNEMONIC account should have a balance.
E2E_KAVA_ERC20_ADDRESS=0xeA7100edA2f805356291B0E55DaD448599a72C6d

View File

@ -1,9 +1,18 @@
package e2e_test
import (
"context"
"math/big"
"time"
sdkerrors "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
txtypes "github.com/cosmos/cosmos-sdk/types/tx"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/kava-labs/kava/app"
earntypes "github.com/kava-labs/kava/x/earn/types"
evmutiltypes "github.com/kava-labs/kava/x/evmutil/types"
"github.com/kava-labs/kava/tests/e2e/contracts/greeter"
"github.com/kava-labs/kava/tests/util"
@ -35,3 +44,132 @@ func (suite *IntegrationTestSuite) TestEthCallToGreeterContract() {
suite.Equal("what's up!", beforeGreeting)
suite.Equal(updatedGreeting, afterGreeting)
}
func (suite *IntegrationTestSuite) TestEthCallToErc20() {
randoReceiver := util.SdkToEvmAddress(app.RandomAddress())
amount := big.NewInt(1e6)
// make unauthenticated eth_call query to check balance
beforeBalance := suite.GetErc20Balance(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)
suite.BigIntsEqual(big.NewInt(0), beforeBalance, "expected before balance to be zero")
suite.BigIntsEqual(amount, afterBalance, "unexpected post-transfer balance")
}
func (suite *IntegrationTestSuite) TestEip712BasicMessageAuthorization() {
// create new funded account
sender := suite.Kava.NewFundedAccount("eip712-msgSend", sdk.NewCoins(ukava(10e6)))
receiver := app.RandomAddress()
// setup message for sending 1KAVA to random receiver
msgs := []sdk.Msg{
banktypes.NewMsgSend(sender.SdkAddress, receiver, sdk.NewCoins(ukava(1e6))),
}
// create tx
tx := suite.NewEip712TxBuilder(
sender,
suite.Kava,
1e6,
sdk.NewCoins(ukava(1e4)),
msgs,
"this is a memo",
).GetTx()
txBytes, err := suite.Kava.EncodingConfig.TxConfig.TxEncoder()(tx)
suite.NoError(err)
// broadcast tx
res, err := suite.Kava.Tx.BroadcastTx(context.Background(), &txtypes.BroadcastTxRequest{
TxBytes: txBytes,
Mode: txtypes.BroadcastMode_BROADCAST_MODE_SYNC,
})
suite.NoError(err)
suite.Equal(sdkerrors.SuccessABCICode, res.TxResponse.Code)
_, err = util.WaitForSdkTxCommit(suite.Kava.Tx, res.TxResponse.TxHash, 6*time.Second)
suite.NoError(err)
// check that the message was processed & the kava is transferred.
balRes, err := suite.Kava.Bank.Balance(context.Background(), &banktypes.QueryBalanceRequest{
Address: receiver.String(),
Denom: "ukava",
})
suite.NoError(err)
suite.Equal(sdk.NewInt(1e6), balRes.Balance.Amount)
}
// Note that this test works because the deployed erc20 is configured in evmutil & earn params.
func (suite *IntegrationTestSuite) TestEip712ConvertToCoinAndDepositToEarn() {
amount := sdk.NewInt(10e6) // 10 USDC
sdkDenom := "erc20/multichain/usdc"
// create new funded account
depositor := suite.Kava.NewFundedAccount("eip712-earn-depositor", sdk.NewCoins(ukava(1e6)))
// give them erc20 balance to deposit
fundRes := suite.FundKavaErc20Balance(depositor.EvmAddress, amount.BigInt())
suite.NoError(fundRes.Err)
// setup messages for convert to coin & deposit into earn
convertMsg := evmutiltypes.NewMsgConvertERC20ToCoin(
evmutiltypes.NewInternalEVMAddress(depositor.EvmAddress),
depositor.SdkAddress,
evmutiltypes.NewInternalEVMAddress(suite.DeployedErc20Address),
amount,
)
depositMsg := earntypes.NewMsgDeposit(
depositor.SdkAddress.String(),
sdk.NewCoin(sdkDenom, amount),
earntypes.STRATEGY_TYPE_SAVINGS,
)
msgs := []sdk.Msg{
// convert to coin
&convertMsg,
// deposit into earn
depositMsg,
}
// create tx
tx := suite.NewEip712TxBuilder(
depositor,
suite.Kava,
1e6,
sdk.NewCoins(ukava(1e4)),
msgs,
"depositing my USDC into Earn!",
).GetTx()
txBytes, err := suite.Kava.EncodingConfig.TxConfig.TxEncoder()(tx)
suite.NoError(err)
// broadcast tx
res, err := suite.Kava.Tx.BroadcastTx(context.Background(), &txtypes.BroadcastTxRequest{
TxBytes: txBytes,
Mode: txtypes.BroadcastMode_BROADCAST_MODE_SYNC,
})
suite.NoError(err)
suite.Equal(sdkerrors.SuccessABCICode, res.TxResponse.Code)
_, err = util.WaitForSdkTxCommit(suite.Kava.Tx, res.TxResponse.TxHash, 6*time.Second)
suite.NoError(err)
// check that depositor no longer has erc20 balance
balance := suite.GetErc20Balance(depositor.EvmAddress)
suite.BigIntsEqual(big.NewInt(0), balance, "expected no erc20 balance")
// check that account has an earn deposit position
earnRes, err := suite.Kava.Earn.Deposits(context.Background(), &earntypes.QueryDepositsRequest{
Depositor: depositor.SdkAddress.String(),
Denom: sdkDenom,
})
suite.NoError(err)
suite.Len(earnRes.Deposits, 1)
suite.Equal(sdk.NewDecFromInt(amount), earnRes.Deposits[0].Shares.AmountOf(sdkDenom))
}

@ -1 +1 @@
Subproject commit 6022e137686e6ab648174b3487c3ee82ec7056ae
Subproject commit 9111087667b4ec136a4dd684762e41515a240a0f

View File

@ -9,6 +9,7 @@ import (
sdkmath "cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/types"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/cosmos/go-bip39"
@ -17,6 +18,7 @@ import (
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/evmos/ethermint/crypto/ethsecp256k1"
emtests "github.com/evmos/ethermint/tests"
emtypes "github.com/evmos/ethermint/types"
"github.com/stretchr/testify/require"
@ -28,6 +30,7 @@ type SigningAccount struct {
name string
mnemonic string
evmPrivKey *ethsecp256k1.PrivKey
evmSigner *util.EvmSigner
evmReqChan chan<- util.EvmTxRequest
evmResChan <-chan util.EvmTxResponse
@ -66,7 +69,7 @@ func (chain *Chain) AddNewSigningAccount(name string, hdPath *hd.BIP44Params, ch
kavaSigner := util.NewKavaSigner(
chainId,
chain.encodingConfig,
chain.EncodingConfig,
chain.Auth,
chain.Tx,
privKey,
@ -100,6 +103,7 @@ func (chain *Chain) AddNewSigningAccount(name string, hdPath *hd.BIP44Params, ch
mnemonic: mnemonic,
l: logger,
evmPrivKey: privKey,
evmSigner: evmSigner,
evmReqChan: evmReqChan,
evmResChan: evmResChan,
@ -168,6 +172,11 @@ func (a *SigningAccount) SignAndBroadcastEvmTx(req util.EvmTxRequest) EvmTxRespo
return response
}
func (a *SigningAccount) SignRawEvmData(msg []byte) ([]byte, types.PubKey, error) {
keyringSigner := emtests.NewSigner(a.evmPrivKey)
return keyringSigner.SignByAddress(a.SdkAddress, msg)
}
// NewFundedAccount creates a SigningAccount for a random account & funds the account from the whale.
func (chain *Chain) NewFundedAccount(name string, funds sdk.Coins) *SigningAccount {
entropy, err := bip39.NewEntropy(128)

View File

@ -13,6 +13,7 @@ import (
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
evmtypes "github.com/evmos/ethermint/x/evm/types"
"github.com/stretchr/testify/require"
"github.com/kava-labs/kava/app"
@ -25,9 +26,8 @@ import (
// Chain wraps query clients & accounts for a network
type Chain struct {
encodingConfig kavaparams.EncodingConfig
accounts map[string]*SigningAccount
t *testing.T
accounts map[string]*SigningAccount
t *testing.T
StakingDenom string
ChainId string
@ -35,10 +35,13 @@ type Chain struct {
EvmClient *ethclient.Client
ContractAddrs map[string]common.Address
EncodingConfig kavaparams.EncodingConfig
Auth authtypes.QueryClient
Bank banktypes.QueryClient
Community communitytypes.QueryClient
Earn earntypes.QueryClient
Evm evmtypes.QueryClient
Tm tmservice.ServiceClient
Tx txtypes.ServiceClient
}
@ -53,7 +56,7 @@ func NewChain(t *testing.T, details *runner.ChainDetails, fundedAccountMnemonic
ChainId: details.ChainId,
ContractAddrs: make(map[string]common.Address),
}
chain.encodingConfig = app.MakeEncodingConfig()
chain.EncodingConfig = app.MakeEncodingConfig()
grpcUrl := fmt.Sprintf("http://localhost:%s", details.GrpcPort)
grpcConn, err := util.NewGrpcConnection(grpcUrl)
@ -71,6 +74,7 @@ func NewChain(t *testing.T, details *runner.ChainDetails, fundedAccountMnemonic
chain.Bank = banktypes.NewQueryClient(grpcConn)
chain.Community = communitytypes.NewQueryClient(grpcConn)
chain.Earn = earntypes.NewQueryClient(grpcConn)
chain.Evm = evmtypes.NewQueryClient(grpcConn)
chain.Tm = tmservice.NewServiceClient(grpcConn)
chain.Tx = txtypes.NewServiceClient(grpcConn)

View File

@ -31,6 +31,9 @@ type SuiteConfig struct {
// Tag of kava docker image that will be upgraded to the current image before tests are run, if upgrade is enabled.
KavaUpgradeBaseImageTag string
// The contract address of a deployed ERC-20 token
KavaErc20Address string
// When true, the chains will remain running after tests complete (pass or fail)
SkipShutdown bool
}
@ -41,6 +44,7 @@ func ParseSuiteConfig() SuiteConfig {
// new accounts created during tests. it will be available under Accounts["whale"]
FundedAccountMnemonic: nonemptyStringEnv("E2E_KAVA_FUNDED_ACCOUNT_MNEMONIC"),
KavaConfigTemplate: nonemptyStringEnv("E2E_KVTOOL_KAVA_CONFIG_TEMPLATE"),
KavaErc20Address: nonemptyStringEnv("E2E_KAVA_ERC20_ADDRESS"),
IncludeIbcTests: mustParseBool("E2E_INCLUDE_IBC_TESTS"),
IncludeAutomatedUpgrade: mustParseBool("E2E_INCLUDE_AUTOMATED_UPGRADE"),
}

View File

@ -0,0 +1,107 @@
package testutil
import (
"context"
"github.com/cosmos/cosmos-sdk/client"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
"github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx"
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/evmos/ethermint/ethereum/eip712"
emtypes "github.com/evmos/ethermint/types"
evmtypes "github.com/evmos/ethermint/x/evm/types"
)
func (suite *E2eTestSuite) NewEip712TxBuilder(
acc *SigningAccount, chain *Chain, gas uint64, gasAmount sdk.Coins, msgs []sdk.Msg, memo string,
) client.TxBuilder {
// get account details
var accDetails authtypes.AccountI
a, err := chain.Auth.Account(context.Background(), &authtypes.QueryAccountRequest{
Address: acc.SdkAddress.String(),
})
suite.NoError(err)
err = chain.EncodingConfig.InterfaceRegistry.UnpackAny(a.Account, &accDetails)
suite.NoError(err)
// get nonce & acc number
nonce := accDetails.GetSequence()
accNumber := accDetails.GetAccountNumber()
// get chain id
pc, err := emtypes.ParseChainID(chain.ChainId)
suite.NoError(err)
ethChainId := pc.Uint64()
evmParams, err := chain.Evm.Params(context.Background(), &evmtypes.QueryParamsRequest{})
suite.NoError(err)
fee := legacytx.NewStdFee(gas, gasAmount)
// build EIP712 tx
// -- untyped data
untypedData := eip712.ConstructUntypedEIP712Data(
chain.ChainId,
accNumber,
nonce,
0, // no timeout
fee,
msgs,
memo,
nil,
)
// -- typed data
typedData, err := eip712.WrapTxToTypedData(ethChainId, msgs, untypedData, &eip712.FeeDelegationOptions{
FeePayer: acc.SdkAddress,
}, evmParams.Params)
suite.NoError(err)
// -- raw data hash!
data, err := eip712.ComputeTypedDataHash(typedData)
suite.NoError(err)
// -- sign the hash
signature, pubKey, err := acc.SignRawEvmData(data)
suite.NoError(err)
signature[crypto.RecoveryIDOffset] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
// add ExtensionOptionsWeb3Tx extension
var option *codectypes.Any
option, err = codectypes.NewAnyWithValue(&emtypes.ExtensionOptionsWeb3Tx{
FeePayer: acc.SdkAddress.String(),
TypedDataChainID: ethChainId,
FeePayerSig: signature,
})
suite.NoError(err)
// create cosmos sdk tx builder
txBuilder := chain.EncodingConfig.TxConfig.NewTxBuilder()
builder, ok := txBuilder.(authtx.ExtensionOptionsTxBuilder)
suite.True(ok)
builder.SetExtensionOptions(option)
builder.SetFeeAmount(fee.Amount)
builder.SetGasLimit(fee.Gas)
sigsV2 := signing.SignatureV2{
PubKey: pubKey,
Data: &signing.SingleSignatureData{
SignMode: signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON,
},
Sequence: nonce,
}
err = builder.SetSignatures(sigsV2)
suite.Require().NoError(err)
err = builder.SetMsgs(msgs...)
suite.Require().NoError(err)
builder.SetMemo(memo)
return builder
}

View File

@ -1,12 +1,29 @@
package testutil
import "github.com/kava-labs/kava/tests/e2e/contracts/greeter"
import (
"context"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/kava-labs/kava/tests/e2e/contracts/greeter"
"github.com/kava-labs/kava/tests/util"
)
// InitKavaEvmData is run after the chain is running, but before the tests are run.
// It is used to initialize some EVM state, such as deploying contracts.
func (suite *E2eTestSuite) InitKavaEvmData() {
whale := suite.Kava.GetAccount(FundedAccountName)
// ensure funded account has nonzero erc20 balance
balance := suite.GetErc20Balance(whale.EvmAddress)
if balance.Cmp(big.NewInt(0)) != 1 {
panic(fmt.Sprintf("expected funded account (%s) to have erc20 balance", whale.EvmAddress.Hex()))
}
// deploy an example contract
greeterAddr, _, _, err := greeter.DeployGreeter(
whale.evmSigner.Auth,
@ -16,3 +33,29 @@ func (suite *E2eTestSuite) InitKavaEvmData() {
suite.NoError(err, "failed to deploy a contract to the EVM")
suite.Kava.ContractAddrs["greeter"] = greeterAddr
}
func (suite *E2eTestSuite) FundKavaErc20Balance(toAddress common.Address, amount *big.Int) EvmTxResponse {
// funded account should have erc20 balance
whale := suite.Kava.GetAccount(FundedAccountName)
data := util.BuildErc20TransferCallData(whale.EvmAddress, toAddress, amount)
nonce, err := suite.Kava.EvmClient.PendingNonceAt(context.Background(), whale.EvmAddress)
suite.NoError(err)
req := util.EvmTxRequest{
Tx: ethtypes.NewTransaction(nonce, suite.DeployedErc20Address, big.NewInt(0), 1e5, big.NewInt(1e10), data),
Data: fmt.Sprintf("fund %s with ERC20 balance (%s)", toAddress.Hex(), amount.String()),
}
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

@ -2,8 +2,10 @@ package testutil
import (
"fmt"
"math/big"
"path/filepath"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/suite"
"github.com/kava-labs/kava/app"
@ -30,7 +32,8 @@ type E2eTestSuite struct {
Kava *Chain
Ibc *Chain
UpgradeHeight int64
UpgradeHeight int64
DeployedErc20Address common.Address
}
func (suite *E2eTestSuite) SetupSuite() {
@ -41,6 +44,7 @@ func (suite *E2eTestSuite) SetupSuite() {
suiteConfig := ParseSuiteConfig()
suite.config = suiteConfig
suite.UpgradeHeight = suiteConfig.KavaUpgradeHeight
suite.DeployedErc20Address = common.HexToAddress(suiteConfig.KavaErc20Address)
runnerConfig := runner.Config{
KavaConfigTemplate: suiteConfig.KavaConfigTemplate,
@ -105,3 +109,8 @@ func (suite *E2eTestSuite) SkipIfUpgradeDisabled() {
func (suite *E2eTestSuite) KavaHomePath() string {
return filepath.Join("kvtool", "full_configs", "generated", "kava", "initstate", ".kava")
}
// BigIntsEqual is a helper method for comparing the equality of two big ints
func (suite *E2eTestSuite) BigIntsEqual(expected *big.Int, actual *big.Int, msg string) {
suite.Truef(expected.Cmp(actual) == 0, "%s (expected: %s, actual: %s)", msg, expected.String(), actual.String())
}

40
tests/util/erc20.go Normal file
View File

@ -0,0 +1,40 @@
package util
import (
"math/big"
"github.com/ethereum/go-ethereum/common"
"golang.org/x/crypto/sha3"
)
// EvmContractMethodId encodes a method signature to the method id used in eth calldata.
func EvmContractMethodId(signature string) []byte {
transferFnSignature := []byte(signature)
hash := sha3.NewLegacyKeccak256()
hash.Write(transferFnSignature)
return hash.Sum(nil)[:4]
}
func BuildErc20TransferCallData(from common.Address, to common.Address, amount *big.Int) []byte {
methodId := EvmContractMethodId("transfer(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 BuildErc20BalanceOfCallData(address common.Address) []byte {
methodId := EvmContractMethodId("balanceOf(address)")
paddedAddress := common.LeftPadBytes(address.Bytes(), 32)
var data []byte
data = append(data, methodId...)
data = append(data, paddedAddress...)
return data
}

View File

@ -2,10 +2,13 @@ package util
import (
"context"
"errors"
"fmt"
"time"
"github.com/kava-labs/kava/app/params"
"google.golang.org/grpc/codes"
grpcstatus "google.golang.org/grpc/status"
errorsmod "cosmossdk.io/errors"
sdkclient "github.com/cosmos/cosmos-sdk/client"
@ -19,6 +22,10 @@ import (
tmmempool "github.com/tendermint/tendermint/mempool"
)
var (
ErrSdkBroadcastTimeout = errors.New("timed out waiting for tx to be committed to block")
)
type KavaMsgRequest struct {
Msgs []sdk.Msg
GasLimit uint64
@ -420,3 +427,35 @@ func Sign(
func GetAccAddress(privKey cryptotypes.PrivKey) sdk.AccAddress {
return privKey.PubKey().Address().Bytes()
}
// WaitForSdkTxCommit polls the chain until the tx hash is found or times out.
// Returns an error immediately if tx hash is empty
func WaitForSdkTxCommit(txClient txtypes.ServiceClient, txHash string, timeout time.Duration) (*sdk.TxResponse, error) {
if txHash == "" {
return nil, fmt.Errorf("tx hash is empty")
}
var err error
var txRes *sdk.TxResponse
var res *txtypes.GetTxResponse
outOfTime := time.After(timeout)
for {
select {
case <-outOfTime:
err = ErrSdkBroadcastTimeout
default:
res, err = txClient.GetTx(context.Background(), &txtypes.GetTxRequest{Hash: txHash})
if err != nil {
status, ok := grpcstatus.FromError(err)
if ok && status.Code() == codes.NotFound {
// tx still not committed to a block. retry!
time.Sleep(100 * time.Millisecond)
continue
}
break
}
txRes = res.TxResponse
}
break
}
return txRes, err
}