diff --git a/changelog.md b/changelog.md index 6aee170385..f70579b6ca 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ * [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envolop parsing * [2533](https://github.com/zeta-chain/node/pull/2533) - parse memo from both OP_RETURN and inscription * [2597](https://github.com/zeta-chain/node/pull/2597) - Add generic rpc metrics to zetaclient +* [2634](https://github.com/zeta-chain/node/pull/2634) - add support for EIP-1559 gas fees ## v19.0.1 diff --git a/e2e/e2etests/test_eth_withdraw.go b/e2e/e2etests/test_eth_withdraw.go index 3415becca6..2d43bd095d 100644 --- a/e2e/e2etests/test_eth_withdraw.go +++ b/e2e/e2etests/test_eth_withdraw.go @@ -3,6 +3,8 @@ package e2etests import ( "math/big" + ethcommon "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/e2e/runner" @@ -37,5 +39,22 @@ func TestEtherWithdraw(r *runner.E2ERunner, args []string) { utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) + // Previous binary doesn't take EIP-1559 into account, so this will fail. + // Thus, we need to skip this check for upgrade tests + if !r.IsRunningUpgrade() { + withdrawalReceipt := mustFetchEthReceipt(r, cctx) + require.Equal(r, uint8(ethtypes.DynamicFeeTxType), withdrawalReceipt.Type, "receipt type mismatch") + } + r.Logger.Info("TestEtherWithdraw completed") } + +func mustFetchEthReceipt(r *runner.E2ERunner, cctx *crosschaintypes.CrossChainTx) *ethtypes.Receipt { + hash := cctx.GetCurrentOutboundParam().Hash + require.NotEmpty(r, hash, "outbound hash is empty") + + receipt, err := r.EVMClient.TransactionReceipt(r.Ctx, ethcommon.HexToHash(hash)) + require.NoError(r, err) + + return receipt +} diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index 2ff538584f..1dfe0d3fb8 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -42,6 +42,13 @@ import ( type E2ERunnerOption func(*E2ERunner) +// Important ENV +const ( + EnvKeyLocalnetMode = "LOCALNET_MODE" + + LocalnetModeUpgrade = "upgrade" +) + func WithZetaTxServer(txServer *txserver.ZetaTxServer) E2ERunnerOption { return func(r *E2ERunner) { r.ZetaTxServer = txServer @@ -322,6 +329,11 @@ func (r *E2ERunner) PrintContractAddresses() { r.Logger.Print("TestDappEVM: %s", r.EvmTestDAppAddr.Hex()) } +// IsRunningUpgrade returns true if the test is running an upgrade test suite. +func (r *E2ERunner) IsRunningUpgrade() bool { + return os.Getenv(EnvKeyLocalnetMode) == LocalnetModeUpgrade +} + // Errorf logs an error message. Mimics the behavior of testing.T.Errorf func (r *E2ERunner) Errorf(format string, args ...any) { r.Logger.Error(format, args...) diff --git a/zetaclient/chains/evm/observer/observer_gas.go b/zetaclient/chains/evm/observer/observer_gas.go index 8bbc32d3a0..311ea187b9 100644 --- a/zetaclient/chains/evm/observer/observer_gas.go +++ b/zetaclient/chains/evm/observer/observer_gas.go @@ -126,7 +126,7 @@ func (ob *Observer) supportsPriorityFee(ctx context.Context) (bool, error) { defer ob.Mu().Unlock() ob.priorityFeeConfig.checked = true - ob.priorityFeeConfig.checked = isSupported + ob.priorityFeeConfig.supported = isSupported return isSupported, nil } diff --git a/zetaclient/chains/evm/signer/gas.go b/zetaclient/chains/evm/signer/gas.go new file mode 100644 index 0000000000..575f004eb7 --- /dev/null +++ b/zetaclient/chains/evm/signer/gas.go @@ -0,0 +1,120 @@ +package signer + +import ( + "fmt" + "math/big" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + + "github.com/zeta-chain/zetacore/x/crosschain/types" +) + +const ( + minGasLimit = 100_000 + maxGasLimit = 1_000_000 +) + +// Gas represents gas parameters for EVM transactions. +// +// This is pretty interesting because all EVM chains now support EIP-1559, but some chains do it in a specific way +// https://eips.ethereum.org/EIPS/eip-1559 +// https://www.blocknative.com/blog/eip-1559-fees +// https://github.com/bnb-chain/BEPs/blob/master/BEPs/BEP226.md (tl;dr: baseFee is always zero) +// +// However, this doesn't affect tx creation nor broadcasting +type Gas struct { + Limit uint64 + + // This is a "total" gasPrice per 1 unit of gas. + // GasPrice for pre EIP-1559 transactions or maxFeePerGas for EIP-1559. + Price *big.Int + + // PriorityFee a fee paid directly to validators for EIP-1559. + PriorityFee *big.Int +} + +func (g Gas) validate() error { + switch { + case g.Limit == 0: + return errors.New("gas limit is zero") + case g.Price == nil: + return errors.New("max fee per unit is nil") + case g.PriorityFee == nil: + return errors.New("priority fee per unit is nil") + case g.Price.Cmp(g.PriorityFee) == -1: + return fmt.Errorf( + "max fee per unit (%d) is less than priority fee per unit (%d)", + g.Price.Int64(), + g.PriorityFee.Int64(), + ) + default: + return nil + } +} + +// isLegacy determines whether the gas is meant for LegacyTx{} (pre EIP-1559) +// or DynamicFeeTx{} (post EIP-1559). +// +// Returns true if priority fee is <= 0. +func (g Gas) isLegacy() bool { + return g.PriorityFee.Sign() < 1 +} + +func gasFromCCTX(cctx *types.CrossChainTx, logger zerolog.Logger) (Gas, error) { + var ( + params = cctx.GetCurrentOutboundParam() + limit = params.GasLimit + ) + + switch { + case limit < minGasLimit: + limit = minGasLimit + logger.Warn(). + Uint64("cctx.initial_gas_limit", params.GasLimit). + Uint64("cctx.gas_limit", limit). + Msgf("Gas limit is too low. Setting to the minimum (%d)", minGasLimit) + case limit > maxGasLimit: + limit = maxGasLimit + logger.Warn(). + Uint64("cctx.initial_gas_limit", params.GasLimit). + Uint64("cctx.gas_limit", limit). + Msgf("Gas limit is too high; Setting to the maximum (%d)", maxGasLimit) + } + + gasPrice, err := bigIntFromString(params.GasPrice) + if err != nil { + return Gas{}, errors.Wrap(err, "unable to parse gasPrice") + } + + priorityFee, err := bigIntFromString(params.GasPriorityFee) + switch { + case err != nil: + return Gas{}, errors.Wrap(err, "unable to parse priorityFee") + case gasPrice.Cmp(priorityFee) == -1: + return Gas{}, fmt.Errorf("gasPrice (%d) is less than priorityFee (%d)", gasPrice.Int64(), priorityFee.Int64()) + } + + return Gas{ + Limit: limit, + Price: gasPrice, + PriorityFee: priorityFee, + }, nil +} + +func bigIntFromString(s string) (*big.Int, error) { + if s == "" || s == "0" { + return big.NewInt(0), nil + } + + v, ok := new(big.Int).SetString(s, 10) + if !ok { + return nil, fmt.Errorf("unable to parse %q as big.Int", s) + } + + if v.Sign() == -1 { + return nil, fmt.Errorf("big.Int is negative: %d", v.Int64()) + } + + return v, nil +} diff --git a/zetaclient/chains/evm/signer/gas_test.go b/zetaclient/chains/evm/signer/gas_test.go new file mode 100644 index 0000000000..d68d8c26af --- /dev/null +++ b/zetaclient/chains/evm/signer/gas_test.go @@ -0,0 +1,144 @@ +package signer + +import ( + "math/big" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/zeta-chain/zetacore/x/crosschain/types" +) + +func TestGasFromCCTX(t *testing.T) { + logger := zerolog.New(zerolog.NewTestWriter(t)) + + makeCCTX := func(gasLimit uint64, price, priorityFee string) *types.CrossChainTx { + cctx := getCCTX(t) + cctx.GetOutboundParams()[0].GasLimit = gasLimit + cctx.GetOutboundParams()[0].GasPrice = price + cctx.GetOutboundParams()[0].GasPriorityFee = priorityFee + + return cctx + } + + for _, tt := range []struct { + name string + cctx *types.CrossChainTx + errorContains string + assert func(t *testing.T, g Gas) + }{ + { + name: "legacy: gas is too low", + cctx: makeCCTX(minGasLimit-200, gwei(2).String(), ""), + assert: func(t *testing.T, g Gas) { + assert.True(t, g.isLegacy()) + assertGasEquals(t, Gas{ + Limit: minGasLimit, + PriorityFee: gwei(0), + Price: gwei(2), + }, g) + }, + }, + { + name: "london: gas is too low", + cctx: makeCCTX(minGasLimit-200, gwei(2).String(), gwei(1).String()), + assert: func(t *testing.T, g Gas) { + assert.False(t, g.isLegacy()) + assertGasEquals(t, Gas{ + Limit: minGasLimit, + Price: gwei(2), + PriorityFee: gwei(1), + }, g) + }, + }, + { + name: "pre London gas logic", + cctx: makeCCTX(minGasLimit+100, gwei(3).String(), ""), + assert: func(t *testing.T, g Gas) { + assert.True(t, g.isLegacy()) + assertGasEquals(t, Gas{ + Limit: 100_100, + Price: gwei(3), + PriorityFee: gwei(0), + }, g) + }, + }, + { + name: "post London gas logic", + cctx: makeCCTX(minGasLimit+200, gwei(4).String(), gwei(1).String()), + assert: func(t *testing.T, g Gas) { + assert.False(t, g.isLegacy()) + assertGasEquals(t, Gas{ + Limit: 100_200, + Price: gwei(4), + PriorityFee: gwei(1), + }, g) + }, + }, + { + name: "gas is too high, force to the ceiling", + cctx: makeCCTX(maxGasLimit+200, gwei(4).String(), gwei(1).String()), + assert: func(t *testing.T, g Gas) { + assert.False(t, g.isLegacy()) + assertGasEquals(t, Gas{ + Limit: maxGasLimit, + Price: gwei(4), + PriorityFee: gwei(1), + }, g) + }, + }, + { + name: "priority fee is invalid", + cctx: makeCCTX(123_000, gwei(4).String(), "oopsie"), + errorContains: "unable to parse priorityFee", + }, + { + name: "priority fee is negative", + cctx: makeCCTX(123_000, gwei(4).String(), "-1"), + errorContains: "unable to parse priorityFee: big.Int is negative", + }, + { + name: "gasPrice is less than priorityFee", + cctx: makeCCTX(123_000, gwei(4).String(), gwei(5).String()), + errorContains: "gasPrice (4000000000) is less than priorityFee (5000000000)", + }, + { + name: "gasPrice is invalid", + cctx: makeCCTX(123_000, "hello", gwei(5).String()), + errorContains: "unable to parse gasPrice", + }, + } { + t.Run(tt.name, func(t *testing.T) { + g, err := gasFromCCTX(tt.cctx, logger) + if tt.errorContains != "" { + assert.ErrorContains(t, err, tt.errorContains) + return + } + + assert.NoError(t, err) + assert.NoError(t, g.validate()) + tt.assert(t, g) + }) + } + + t.Run("empty priority fee", func(t *testing.T) { + gas := Gas{ + Limit: 123_000, + Price: gwei(4), + PriorityFee: nil, + } + + assert.Error(t, gas.validate()) + }) +} + +func assertGasEquals(t *testing.T, expected, actual Gas) { + assert.Equal(t, int64(expected.Limit), int64(actual.Limit), "gas limit") + assert.Equal(t, expected.Price.Int64(), actual.Price.Int64(), "max fee per unit") + assert.Equal(t, expected.PriorityFee.Int64(), actual.PriorityFee.Int64(), "priority fee per unit") +} + +func gwei(i int64) *big.Int { + const g = 1_000_000_000 + return big.NewInt(i * g) +} diff --git a/zetaclient/chains/evm/signer/outbound_data.go b/zetaclient/chains/evm/signer/outbound_data.go index f59b897c61..6a4cec2154 100644 --- a/zetaclient/chains/evm/signer/outbound_data.go +++ b/zetaclient/chains/evm/signer/outbound_data.go @@ -11,33 +11,29 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog" - "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/x/crosschain/types" "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" - "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" zctx "github.com/zeta-chain/zetacore/zetaclient/context" ) -const ( - MinGasLimit = 100_000 - MaxGasLimit = 1_000_000 -) - // OutboundData is a data structure containing input fields used to construct each type of transaction. // This is populated using cctx and other input parameters passed to TryProcessOutbound type OutboundData struct { srcChainID *big.Int - toChainID *big.Int sender ethcommon.Address - to ethcommon.Address - asset ethcommon.Address - amount *big.Int - gasPrice *big.Int - gasLimit uint64 - message []byte - nonce uint64 - height uint64 + + toChainID *big.Int + to ethcommon.Address + + asset ethcommon.Address + amount *big.Int + + gas Gas + nonce uint64 + height uint64 + + message []byte // cctxIndex field is the inbound message digest that is sent to the destination contract cctxIndex [32]byte @@ -46,78 +42,14 @@ type OutboundData struct { outboundParams *types.OutboundParams } -// SetChainAndSender populates the destination address and Chain ID based on the status of the cross chain tx -// returns true if transaction should be skipped -// returns false otherwise -func (txData *OutboundData) SetChainAndSender(cctx *types.CrossChainTx, logger zerolog.Logger) bool { - switch cctx.CctxStatus.Status { - case types.CctxStatus_PendingRevert: - txData.to = ethcommon.HexToAddress(cctx.InboundParams.Sender) - txData.toChainID = big.NewInt(cctx.InboundParams.SenderChainId) - logger.Info().Msgf("Abort: reverting inbound") - case types.CctxStatus_PendingOutbound: - txData.to = ethcommon.HexToAddress(cctx.GetCurrentOutboundParam().Receiver) - txData.toChainID = big.NewInt(cctx.GetCurrentOutboundParam().ReceiverChainId) - default: - logger.Info().Msgf("Transaction doesn't need to be processed status: %d", cctx.CctxStatus.Status) - return true - } - return false -} - -// SetupGas sets the gas limit and price -func (txData *OutboundData) SetupGas( - cctx *types.CrossChainTx, - logger zerolog.Logger, - client interfaces.EVMRPCClient, - chain chains.Chain, -) error { - txData.gasLimit = cctx.GetCurrentOutboundParam().GasLimit - if txData.gasLimit < MinGasLimit { - txData.gasLimit = MinGasLimit - logger.Warn(). - Msgf("gasLimit %d is too low; set to %d", cctx.GetCurrentOutboundParam().GasLimit, txData.gasLimit) - } - if txData.gasLimit > MaxGasLimit { - txData.gasLimit = MaxGasLimit - logger.Warn(). - Msgf("gasLimit %d is too high; set to %d", cctx.GetCurrentOutboundParam().GasLimit, txData.gasLimit) - } - - // use dynamic gas price for ethereum chains. - // The code below is a fix for https://github.com/zeta-chain/node/issues/1085 - // doesn't close directly the issue because we should determine if we want to keep using SuggestGasPrice if no GasPrice - // we should possibly remove it completely and return an error if no GasPrice is provided because it means no fee is processed on ZetaChain - specified, ok := new(big.Int).SetString(cctx.GetCurrentOutboundParam().GasPrice, 10) - if !ok { - if chain.Network == chains.Network_eth { - suggested, err := client.SuggestGasPrice(context.Background()) - if err != nil { - return errors.Wrapf(err, "cannot get gas price from chain %s ", chain.String()) - } - txData.gasPrice = roundUpToNearestGwei(suggested) - } else { - return fmt.Errorf("cannot convert gas price %s ", cctx.GetCurrentOutboundParam().GasPrice) - } - } else { - txData.gasPrice = specified - } - return nil -} - -// NewOutboundData populates transaction input fields parsed from the cctx and other parameters -// returns -// 1. New NewOutboundData Data struct or nil if an error occurred. -// 2. bool (skipTx) - if the transaction doesn't qualify to be processed the function will return true, meaning that this -// cctx will be skipped and false otherwise. -// 3. error +// NewOutboundData creates OutboundData from the given CCTX. +// returns `bool true` when transaction should be skipped. func NewOutboundData( ctx context.Context, cctx *types.CrossChainTx, - evmObserver *observer.Observer, - evmRPC interfaces.EVMRPCClient, - logger zerolog.Logger, + observer *observer.Observer, height uint64, + logger zerolog.Logger, ) (*OutboundData, bool, error) { txData := OutboundData{} txData.outboundParams = cctx.GetCurrentOutboundParam() @@ -128,66 +60,145 @@ func NewOutboundData( txData.asset = ethcommon.HexToAddress(cctx.InboundParams.Asset) txData.height = height + if cctx == nil { + return nil, false, errors.New("cctx is nil") + } - skipTx := txData.SetChainAndSender(cctx, logger) - if skipTx { - return nil, true, nil + outboundParams := cctx.GetCurrentOutboundParam() + nonce := outboundParams.TssNonce + + if err := validateParams(outboundParams); err != nil { + return nil, false, errors.Wrap(err, "invalid outboundParams") } app, err := zctx.FromContext(ctx) if err != nil { - return nil, false, err + return nil, false, errors.Wrap(err, "unable to get app from context") } - chainID := txData.toChainID.Int64() + // recipient + destination chain + to, toChainID, skip := getDestination(cctx, logger) + if skip { + return nil, true, nil + } - toChain, err := app.GetChain(chainID) - switch { - case err != nil: - return nil, true, errors.Wrapf(err, "unable to get chain %d from app context", chainID) - case toChain.IsZeta(): - // should not happen - return nil, true, errors.New("destination chain is Zeta") + // ensure that chain exists in app's context + if _, err := app.GetChain(toChainID.Int64()); err != nil { + return nil, false, errors.Wrapf(err, "unable to get chain %d from app context", toChainID.Int64()) } - rawChain := toChain.RawChain() + gas, err := gasFromCCTX(cctx, logger) + if err != nil { + return nil, false, errors.Wrap(err, "unable to make gas from CCTX") + } - // Set up gas limit and gas price - err = txData.SetupGas(cctx, logger, evmRPC, *rawChain) + cctxIndex, err := getCCTXIndex(cctx) if err != nil { - return nil, true, errors.Wrap(err, "unable to setup gas") + return nil, false, errors.Wrap(err, "unable to get cctx index") } - nonce := cctx.GetCurrentOutboundParam().TssNonce + // In case there is a pending tx, make sure this keySign is a tx replacement + if tx := observer.GetPendingTx(nonce); tx != nil { + evt := logger.Info(). + Str("cctx.pending_tx_hash", tx.Hash().Hex()). + Uint64("cctx.pending_tx_nonce", nonce) - // Get sendHash - logger.Info(). - Msgf("chain %d minting %d to %s, nonce %d, finalized zeta bn %d", toChain.ID(), cctx.InboundParams.Amount, txData.to.Hex(), nonce, cctx.InboundParams.FinalizedZetaHeight) - cctxIndex, err := hex.DecodeString(cctx.Index[2:]) // remove the leading 0x - if err != nil || len(cctxIndex) != 32 { - return nil, true, fmt.Errorf("decode CCTX %s error", cctx.Index) - } - copy(txData.cctxIndex[:32], cctxIndex[:32]) - - // In case there is a pending transaction, make sure this keysign is a transaction replacement - pendingTx := evmObserver.GetPendingTx(nonce) - if pendingTx != nil { - if txData.gasPrice.Cmp(pendingTx.GasPrice()) > 0 { - logger.Info(). - Msgf("replace pending outbound %s nonce %d using gas price %d", pendingTx.Hash().Hex(), nonce, txData.gasPrice) - } else { - logger.Info().Msgf("please wait for pending outbound %s nonce %d to be included", pendingTx.Hash().Hex(), nonce) + // new gas price is less or equal to pending tx gas + if gas.Price.Cmp(tx.GasPrice()) <= 0 { + evt.Msg("Please wait for pending outbound to be included in the block") return nil, true, nil } + + evt. + Uint64("cctx.gas_price", gas.Price.Uint64()). + Uint64("cctx.priority_fee", gas.PriorityFee.Uint64()). + Msg("Replacing pending outbound transaction with higher gas fees") } // Base64 decode message + var message []byte if cctx.InboundParams.CoinType != coin.CoinType_Cmd { - txData.message, err = base64.StdEncoding.DecodeString(cctx.RelayedMessage) - if err != nil { - logger.Err(err).Msgf("decode CCTX.Message %s error", cctx.RelayedMessage) + msg, errDecode := base64.StdEncoding.DecodeString(cctx.RelayedMessage) + if errDecode != nil { + logger.Err(err).Str("cctx.relayed_message", cctx.RelayedMessage).Msg("Unable to decode relayed message") + } else { + message = msg } } - return &txData, false, nil + return &OutboundData{ + srcChainID: big.NewInt(cctx.InboundParams.SenderChainId), + sender: ethcommon.HexToAddress(cctx.InboundParams.Sender), + + toChainID: toChainID, + to: to, + + asset: ethcommon.HexToAddress(cctx.InboundParams.Asset), + amount: outboundParams.Amount.BigInt(), + + gas: gas, + nonce: outboundParams.TssNonce, + height: height, + + message: message, + + cctxIndex: cctxIndex, + + outboundParams: outboundParams, + }, false, nil +} + +func getCCTXIndex(cctx *types.CrossChainTx) ([32]byte, error) { + // `0x` + `64 chars`. Two chars ranging `00...FF` represent one byte (64 chars = 32 bytes) + if len(cctx.Index) != (2 + 64) { + return [32]byte{}, fmt.Errorf("cctx index %q is invalid", cctx.Index) + } + + // remove the leading `0x` + cctxIndexSlice, err := hex.DecodeString(cctx.Index[2:]) + if err != nil || len(cctxIndexSlice) != 32 { + return [32]byte{}, errors.Wrapf(err, "unable to decode cctx index %s", cctx.Index) + } + + var cctxIndex [32]byte + copy(cctxIndex[:32], cctxIndexSlice[:32]) + + return cctxIndex, nil +} + +// getDestination picks the destination address and Chain ID based on the status of the cross chain tx. +// returns true if transaction should be skipped. +func getDestination(cctx *types.CrossChainTx, logger zerolog.Logger) (ethcommon.Address, *big.Int, bool) { + switch cctx.CctxStatus.Status { + case types.CctxStatus_PendingRevert: + to := ethcommon.HexToAddress(cctx.InboundParams.Sender) + chainID := big.NewInt(cctx.InboundParams.SenderChainId) + + logger.Info(). + Str("cctx.index", cctx.Index). + Int64("cctx.chain_id", chainID.Int64()). + Msgf("Abort: reverting inbound") + + return to, chainID, false + case types.CctxStatus_PendingOutbound: + to := ethcommon.HexToAddress(cctx.GetCurrentOutboundParam().Receiver) + chainID := big.NewInt(cctx.GetCurrentOutboundParam().ReceiverChainId) + + return to, chainID, false + } + + logger.Info(). + Str("cctx.index", cctx.Index). + Str("cctx.status", cctx.CctxStatus.String()). + Msgf("CCTX doesn't need to be processed") + + return ethcommon.Address{}, nil, true +} + +func validateParams(params *types.OutboundParams) error { + if params == nil || params.GasLimit == 0 { + return errors.New("outboundParams is empty") + } + + return nil } diff --git a/zetaclient/chains/evm/signer/outbound_data_test.go b/zetaclient/chains/evm/signer/outbound_data_test.go index 0d44fd3dbb..03c1329ef1 100644 --- a/zetaclient/chains/evm/signer/outbound_data_test.go +++ b/zetaclient/chains/evm/signer/outbound_data_test.go @@ -1,7 +1,6 @@ package signer import ( - "context" "math/big" "testing" @@ -9,123 +8,143 @@ import ( "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - observertypes "github.com/zeta-chain/zetacore/x/observer/types" - "github.com/zeta-chain/zetacore/zetaclient/config" - zctx "github.com/zeta-chain/zetacore/zetaclient/context" - "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" - "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/x/crosschain/types" ) -func TestSigner_SetChainAndSender(t *testing.T) { - // setup inputs - cctx := getCCTX(t) - txData := &OutboundData{} - logger := zerolog.Logger{} +func TestNewOutboundData(t *testing.T) { + mockObserver, err := getNewEvmChainObserver(t, nil) + require.NoError(t, err) - t.Run("SetChainAndSender PendingRevert", func(t *testing.T) { - cctx.CctxStatus.Status = types.CctxStatus_PendingRevert - skipTx := txData.SetChainAndSender(cctx, logger) + logger := zerolog.New(zerolog.NewTestWriter(t)) - require.False(t, skipTx) - require.Equal(t, ethcommon.HexToAddress(cctx.InboundParams.Sender), txData.to) - require.Equal(t, big.NewInt(cctx.InboundParams.SenderChainId), txData.toChainID) - }) + ctx := makeCtx(t) - t.Run("SetChainAndSender PendingOutbound", func(t *testing.T) { - cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound - skipTx := txData.SetChainAndSender(cctx, logger) + newOutbound := func(cctx *types.CrossChainTx) (*OutboundData, bool, error) { + return NewOutboundData(ctx, cctx, mockObserver, 123, logger) + } - require.False(t, skipTx) - require.Equal(t, ethcommon.HexToAddress(cctx.GetCurrentOutboundParam().Receiver), txData.to) - require.Equal(t, big.NewInt(cctx.GetCurrentOutboundParam().ReceiverChainId), txData.toChainID) - }) + t.Run("success", func(t *testing.T) { + // ARRANGE + cctx := getCCTX(t) - t.Run("SetChainAndSender Should skip cctx", func(t *testing.T) { - cctx.CctxStatus.Status = types.CctxStatus_PendingInbound - skipTx := txData.SetChainAndSender(cctx, logger) - require.True(t, skipTx) - }) -} + // ACT + out, skip, err := newOutbound(cctx) -func TestSigner_SetupGas(t *testing.T) { - cctx := getCCTX(t) - evmSigner, err := getNewEvmSigner(nil) - require.NoError(t, err) + // ASSERT + require.NoError(t, err) + assert.False(t, skip) - txData := &OutboundData{} - logger := zerolog.Logger{} + assert.NotEmpty(t, out) - t.Run("SetupGas_success", func(t *testing.T) { - chain := chains.BscMainnet - err := txData.SetupGas(cctx, logger, evmSigner.EvmClient(), chain) - require.NoError(t, err) - }) + assert.NotEmpty(t, out.srcChainID) + assert.NotEmpty(t, out.sender) - t.Run("SetupGas_error", func(t *testing.T) { - cctx.GetCurrentOutboundParam().GasPrice = "invalidGasPrice" - chain := chains.BscMainnet - err := txData.SetupGas(cctx, logger, evmSigner.EvmClient(), chain) - require.ErrorContains(t, err, "cannot convert gas price") + assert.NotEmpty(t, out.toChainID) + assert.NotEmpty(t, out.to) + + assert.Equal(t, ethcommon.HexToAddress(cctx.InboundParams.Asset), out.asset) + assert.NotEmpty(t, out.amount) + + assert.NotEmpty(t, out.nonce) + assert.NotEmpty(t, out.height) + assert.NotEmpty(t, out.gas) + assert.True(t, out.gas.isLegacy()) + assert.Equal(t, uint64(minGasLimit), out.gas.Limit) + + assert.Empty(t, out.message) + assert.NotEmpty(t, out.cctxIndex) + assert.Equal(t, cctx.OutboundParams[0], out.outboundParams) }) -} -func TestSigner_NewOutboundData(t *testing.T) { - app := zctx.New(config.New(false), zerolog.Nop()) - ctx := zctx.WithAppContext(context.Background(), app) - - bscParams := mocks.MockChainParams(chains.BscMainnet.ChainId, 10) - - // Given app context - err := app.Update( - observertypes.Keygen{}, - []chains.Chain{chains.BscMainnet}, - nil, - map[int64]*observertypes.ChainParams{chains.BscMainnet.ChainId: &bscParams}, - "tssPubKey", - observertypes.CrosschainFlags{}, - ) - require.NoError(t, err) + t.Run("pending revert", func(t *testing.T) { + // ARRANGE + cctx := getCCTX(t) + cctx.CctxStatus.Status = types.CctxStatus_PendingRevert - // Setup evm signer - evmSigner, err := getNewEvmSigner(nil) - require.NoError(t, err) + // ACT + out, skip, err := newOutbound(cctx) - mockObserver, err := getNewEvmChainObserver(t, nil) - require.NoError(t, err) + // ASSERT + require.NoError(t, err) + assert.False(t, skip) + assert.Equal(t, ethcommon.HexToAddress(cctx.InboundParams.Sender), out.to) + assert.Equal(t, big.NewInt(cctx.InboundParams.SenderChainId), out.toChainID) + }) - t.Run("NewOutboundData success", func(t *testing.T) { + t.Run("pending outbound", func(t *testing.T) { + // ARRANGE cctx := getCCTX(t) + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + + // ACT + out, skip, err := newOutbound(cctx) - _, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + // ASSERT assert.NoError(t, err) assert.False(t, skip) + assert.Equal(t, ethcommon.HexToAddress(cctx.GetCurrentOutboundParam().Receiver), out.to) + assert.Equal(t, big.NewInt(cctx.GetCurrentOutboundParam().ReceiverChainId), out.toChainID) }) - t.Run("NewOutboundData skip", func(t *testing.T) { + t.Run("skip inbound", func(t *testing.T) { + // ARRANGE cctx := getCCTX(t) - cctx.CctxStatus.Status = types.CctxStatus_Aborted + cctx.CctxStatus.Status = types.CctxStatus_PendingInbound - _, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) - assert.NoError(t, err) + // ACT + _, skip, err := newOutbound(cctx) + + // ASSERT + require.NoError(t, err) assert.True(t, skip) }) - t.Run("NewOutboundData unknown chain", func(t *testing.T) { - cctx := getInvalidCCTX(t) + t.Run("skip aborted", func(t *testing.T) { + // ARRANGE + cctx := getCCTX(t) + cctx.CctxStatus.Status = types.CctxStatus_Aborted + + // ACT + _, skip, err := newOutbound(cctx) - _, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) - assert.ErrorContains(t, err, "unable to get chain 13378337 from app context: id=13378337: chain not found") + // ASSERT + require.NoError(t, err) assert.True(t, skip) }) - t.Run("NewOutboundData setup gas error", func(t *testing.T) { + t.Run("invalid gas price", func(t *testing.T) { + // ARRANGE cctx := getCCTX(t) cctx.GetCurrentOutboundParam().GasPrice = "invalidGasPrice" - _, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) - assert.True(t, skip) - assert.ErrorContains(t, err, "cannot convert gas price") + // ACT + _, _, err := newOutbound(cctx) + + // ASSERT + assert.ErrorContains(t, err, "unable to parse gasPrice") + }) + + t.Run("unknown chain", func(t *testing.T) { + // ARRANGE + cctx := getInvalidCCTX(t) + + // ACT + _, _, err := newOutbound(cctx) + + // ASSERT + assert.ErrorContains(t, err, "chain not found") + }) + + t.Run("no outbound params", func(t *testing.T) { + // ARRANGE + cctx := getCCTX(t) + cctx.OutboundParams = nil + + // ACT + _, _, err := newOutbound(cctx) + + // ASSERT + assert.ErrorContains(t, err, "outboundParams is empty") }) } diff --git a/zetaclient/chains/evm/signer/signer.go b/zetaclient/chains/evm/signer/signer.go index 5ec7df55a2..d30b887ea8 100644 --- a/zetaclient/chains/evm/signer/signer.go +++ b/zetaclient/chains/evm/signer/signer.go @@ -169,16 +169,20 @@ func (signer *Signer) Sign( data []byte, to ethcommon.Address, amount *big.Int, - gasLimit uint64, - gasPrice *big.Int, + gas Gas, nonce uint64, height uint64, ) (*ethtypes.Transaction, []byte, []byte, error) { - log.Debug().Str("tss.pub_key", signer.TSS().EVMAddress().String()).Msg("Sign: TSS signer") + signer.Logger().Std.Debug(). + Str("tss_pub_key", signer.TSS().EVMAddress().String()). + Msg("Signing evm transaction") + + chainID := big.NewInt(signer.Chain().ChainId) + tx, err := newTx(chainID, data, to, amount, gas, nonce) + if err != nil { + return nil, nil, nil, err + } - // TODO: use EIP-1559 transaction type - // https://github.com/zeta-chain/node/issues/1952 - tx := ethtypes.NewTransaction(nonce, to, amount, gasLimit, gasPrice, data) hashBytes := signer.ethSigner.Hash(tx).Bytes() sig, err := signer.TSS().Sign(ctx, hashBytes, height, nonce, signer.Chain().ChainId, "") @@ -202,11 +206,46 @@ func (signer *Signer) Sign( return signedTX, sig[:], hashBytes[:], nil } -// Broadcast takes in signed tx, broadcast to external chain node -func (signer *Signer) Broadcast(tx *ethtypes.Transaction) error { - ctxt, cancel := context.WithTimeout(context.Background(), 1*time.Second) +func newTx( + chainID *big.Int, + data []byte, + to ethcommon.Address, + amount *big.Int, + gas Gas, + nonce uint64, +) (*ethtypes.Transaction, error) { + if err := gas.validate(); err != nil { + return nil, errors.Wrap(err, "invalid gas parameters") + } + + if gas.isLegacy() { + return ethtypes.NewTx(ðtypes.LegacyTx{ + To: &to, + Value: amount, + Data: data, + GasPrice: gas.Price, + Gas: gas.Limit, + Nonce: nonce, + }), nil + } + + return ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainID, + To: &to, + Value: amount, + Data: data, + GasFeeCap: gas.Price, + GasTipCap: gas.PriorityFee, + Gas: gas.Limit, + Nonce: nonce, + }), nil +} + +func (signer *Signer) broadcast(ctx context.Context, tx *ethtypes.Transaction) error { + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() - return signer.client.SendTransaction(ctxt, tx) + + return signer.client.SendTransaction(ctx, tx) } // SignOutbound @@ -240,10 +279,10 @@ func (signer *Signer) SignOutbound(ctx context.Context, txData *OutboundData) (* data, signer.zetaConnectorAddress, zeroValue, - txData.gasLimit, - txData.gasPrice, + txData.gas, txData.nonce, - txData.height) + txData.height, + ) if err != nil { return nil, fmt.Errorf("sign onReceive error: %w", err) } @@ -262,17 +301,15 @@ func (signer *Signer) SignOutbound(ctx context.Context, txData *OutboundData) (* // bytes32 internalSendHash // ) external override whenNotPaused onlyTssAddress func (signer *Signer) SignRevertTx(ctx context.Context, txData *OutboundData) (*ethtypes.Transaction, error) { - var data []byte - var err error - - data, err = signer.zetaConnectorABI.Pack("onRevert", + data, err := signer.zetaConnectorABI.Pack("onRevert", txData.sender, txData.srcChainID, txData.to.Bytes(), txData.toChainID, txData.amount, txData.message, - txData.cctxIndex) + txData.cctxIndex, + ) if err != nil { return nil, fmt.Errorf("onRevert pack error: %w", err) } @@ -282,10 +319,10 @@ func (signer *Signer) SignRevertTx(ctx context.Context, txData *OutboundData) (* data, signer.zetaConnectorAddress, zeroValue, - txData.gasLimit, - txData.gasPrice, + txData.gas, txData.nonce, - txData.height) + txData.height, + ) if err != nil { return nil, fmt.Errorf("sign onRevert error: %w", err) } @@ -295,13 +332,13 @@ func (signer *Signer) SignRevertTx(ctx context.Context, txData *OutboundData) (* // SignCancelTx signs a transaction from TSS address to itself with a zero amount in order to increment the nonce func (signer *Signer) SignCancelTx(ctx context.Context, txData *OutboundData) (*ethtypes.Transaction, error) { + txData.gas.Limit = evm.EthTransferGasLimit tx, _, _, err := signer.Sign( ctx, nil, signer.TSS().EVMAddress(), zeroValue, // zero out the amount to cancel the tx - evm.EthTransferGasLimit, - txData.gasPrice, + txData.gas, txData.nonce, txData.height, ) @@ -314,13 +351,13 @@ func (signer *Signer) SignCancelTx(ctx context.Context, txData *OutboundData) (* // SignWithdrawTx signs a withdrawal transaction sent from the TSS address to the destination func (signer *Signer) SignWithdrawTx(ctx context.Context, txData *OutboundData) (*ethtypes.Transaction, error) { + txData.gas.Limit = evm.EthTransferGasLimit tx, _, _, err := signer.Sign( ctx, nil, txData.to, txData.amount, - evm.EthTransferGasLimit, - txData.gasPrice, + txData.gas, txData.nonce, txData.height, ) @@ -363,6 +400,21 @@ func (signer *Signer) TryProcessOutbound( zetacoreClient interfaces.ZetacoreClient, height uint64, ) { + var ( + params = cctx.GetCurrentOutboundParam() + myID = zetacoreClient.GetKeys().GetOperatorAddress() + logger = signer.Logger().Std.With(). + Str("method", "TryProcessOutbound"). + Int64("chain", signer.Chain().ChainId). + Uint64("nonce", params.TssNonce). + Str("cctx.index", cctx.Index). + Str("cctx.receiver", params.Receiver). + Str("cctx.amount", params.Amount.String()). + Logger() + ) + + logger.Info().Msgf("TryProcessOutbound") + app, err := zctx.FromContext(ctx) if err != nil { signer.Logger().Std.Error().Err(err).Msg("error getting app context") @@ -371,24 +423,12 @@ func (signer *Signer) TryProcessOutbound( // end outbound process on panic defer func() { - outboundProc.EndTryProcess(outboundID) - if err := recover(); err != nil { - signer.Logger().Std.Error().Msgf("EVM TryProcessOutbound: %s, caught panic error: %v", cctx.Index, err) + if r := recover(); r != nil { + logger.Error().Interface("panic", r).Msg("panic in TryProcessOutbound") } - }() - - // prepare logger - params := cctx.GetCurrentOutboundParam() - logger := signer.Logger().Std.With(). - Str("method", "TryProcessOutbound"). - Int64("chain", signer.Chain().ChainId). - Uint64("nonce", params.TssNonce). - Str("cctx", cctx.Index). - Logger() - myID := zetacoreClient.GetKeys().GetOperatorAddress() - logger.Info(). - Msgf("EVM TryProcessOutbound: %s, value %d to %s", cctx.Index, params.Amount.BigInt(), params.Receiver) + outboundProc.EndTryProcess(outboundID) + }() evmObserver, ok := chainObserver.(*observer.Observer) if !ok { @@ -397,11 +437,12 @@ func (signer *Signer) TryProcessOutbound( } // Setup Transaction input - txData, skipTx, err := NewOutboundData(ctx, cctx, evmObserver, signer.client, logger, height) + txData, skipTx, err := NewOutboundData(ctx, cctx, evmObserver, height, logger) if err != nil { logger.Err(err).Msg("error setting up transaction input fields") return } + if skipTx { return } @@ -467,7 +508,7 @@ func (signer *Signer) TryProcessOutbound( cctx.InboundParams.SenderChainId, toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + txData.gas.Price, ) tx, err = signer.SignWithdrawTx(ctx, txData) case coin.CoinType_ERC20: @@ -476,7 +517,7 @@ func (signer *Signer) TryProcessOutbound( cctx.InboundParams.SenderChainId, toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + txData.gas.Price, ) tx, err = signer.SignERC20WithdrawTx(ctx, txData) case coin.CoinType_Zeta: @@ -485,7 +526,7 @@ func (signer *Signer) TryProcessOutbound( cctx.InboundParams.SenderChainId, toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + txData.gas.Price, ) tx, err = signer.SignOutbound(ctx, txData) } @@ -500,7 +541,7 @@ func (signer *Signer) TryProcessOutbound( "SignRevertTx: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + txData.gas.Price, ) txData.srcChainID = big.NewInt(cctx.OutboundParams[0].ReceiverChainId) txData.toChainID = big.NewInt(cctx.GetCurrentOutboundParam().ReceiverChainId) @@ -511,7 +552,7 @@ func (signer *Signer) TryProcessOutbound( cctx.InboundParams.SenderChainId, toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + txData.gas.Price, ) tx, err = signer.SignWithdrawTx(ctx, txData) case coin.CoinType_ERC20: @@ -519,7 +560,7 @@ func (signer *Signer) TryProcessOutbound( cctx.InboundParams.SenderChainId, toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + txData.gas.Price, ) tx, err = signer.SignERC20WithdrawTx(ctx, txData) } @@ -533,7 +574,7 @@ func (signer *Signer) TryProcessOutbound( cctx.InboundParams.SenderChainId, toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + txData.gas.Price, ) txData.srcChainID = big.NewInt(cctx.OutboundParams[0].ReceiverChainId) txData.toChainID = big.NewInt(cctx.GetCurrentOutboundParam().ReceiverChainId) @@ -549,7 +590,7 @@ func (signer *Signer) TryProcessOutbound( cctx.InboundParams.SenderChainId, toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + txData.gas.Price, ) tx, err = signer.SignOutbound(ctx, txData) if err != nil { @@ -606,7 +647,7 @@ func (signer *Signer) BroadcastOutbound( backOff := broadcastBackoff for i := 0; i < broadcastRetries; i++ { time.Sleep(backOff) - err := signer.Broadcast(tx) + err := signer.broadcast(ctx, tx) if err != nil { log.Warn(). Err(err). @@ -641,9 +682,7 @@ func (signer *Signer) BroadcastOutbound( // uint256 amount, // ) external onlyTssAddress func (signer *Signer) SignERC20WithdrawTx(ctx context.Context, txData *OutboundData) (*ethtypes.Transaction, error) { - var data []byte - var err error - data, err = signer.erc20CustodyABI.Pack("withdraw", txData.to, txData.asset, txData.amount) + data, err := signer.erc20CustodyABI.Pack("withdraw", txData.to, txData.asset, txData.amount) if err != nil { return nil, fmt.Errorf("withdraw pack error: %w", err) } @@ -653,8 +692,7 @@ func (signer *Signer) SignERC20WithdrawTx(ctx context.Context, txData *OutboundD data, signer.er20CustodyAddress, zeroValue, - txData.gasLimit, - txData.gasPrice, + txData.gas, txData.nonce, txData.height, ) @@ -707,27 +745,30 @@ func (signer *Signer) SignWhitelistERC20Cmd( if erc20 == (ethcommon.Address{}) { return nil, fmt.Errorf("SignCommandTx: invalid erc20 address %s", params) } + custodyAbi, err := erc20custody.ERC20CustodyMetaData.GetAbi() if err != nil { return nil, err } + data, err := custodyAbi.Pack("whitelist", erc20) if err != nil { return nil, fmt.Errorf("whitelist pack error: %w", err) } + tx, _, _, err := signer.Sign( ctx, data, txData.to, zeroValue, - txData.gasLimit, - txData.gasPrice, + txData.gas, outboundParams.TssNonce, txData.height, ) if err != nil { return nil, fmt.Errorf("sign whitelist error: %w", err) } + return tx, nil } @@ -739,8 +780,7 @@ func (signer *Signer) SignMigrateTssFundsCmd(ctx context.Context, txData *Outbou nil, txData.to, txData.amount, - txData.gasLimit, - txData.gasPrice, + txData.gas, txData.nonce, txData.height, ) @@ -887,14 +927,3 @@ func getEVMRPC(ctx context.Context, endpoint string) (interfaces.EVMRPCClient, e return client, ethSigner, nil } - -// roundUpToNearestGwei rounds up the gas price to the nearest Gwei -func roundUpToNearestGwei(gasPrice *big.Int) *big.Int { - oneGwei := big.NewInt(1_000_000_000) // 1 Gwei - mod := new(big.Int) - mod.Mod(gasPrice, oneGwei) - if mod.Cmp(big.NewInt(0)) == 0 { // gasprice is already a multiple of 1 Gwei - return gasPrice - } - return new(big.Int).Add(gasPrice, new(big.Int).Sub(oneGwei, mod)) -} diff --git a/zetaclient/chains/evm/signer/signer_test.go b/zetaclient/chains/evm/signer/signer_test.go index 95da02e648..9233dfcbe2 100644 --- a/zetaclient/chains/evm/signer/signer_test.go +++ b/zetaclient/chains/evm/signer/signer_test.go @@ -10,6 +10,7 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" observertypes "github.com/zeta-chain/zetacore/x/observer/types" zctx "github.com/zeta-chain/zetacore/zetaclient/context" @@ -197,11 +198,11 @@ func TestSigner_SignOutbound(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, 123, makeLogger(t)) require.False(t, skip) require.NoError(t, err) - t.Run("SignOutbound - should successfully sign", func(t *testing.T) { + t.Run("SignOutbound - should successfully sign LegacyTx", func(t *testing.T) { // Call SignOutbound tx, err := evmSigner.SignOutbound(ctx, txData) require.NoError(t, err) @@ -209,6 +210,9 @@ func TestSigner_SignOutbound(t *testing.T) { // Verify Signature tss := mocks.NewTSSMainnet() verifyTxSignature(t, tx, tss.Pubkey(), evmSigner.EvmSigner()) + + // check that by default tx type is legacy tx + assert.Equal(t, ethtypes.LegacyTxType, int(tx.Type())) }) t.Run("SignOutbound - should fail if keysign fails", func(t *testing.T) { // Pause tss to make keysign fail @@ -219,6 +223,42 @@ func TestSigner_SignOutbound(t *testing.T) { require.ErrorContains(t, err, "sign onReceive error") require.Nil(t, tx) }) + + t.Run("SignOutbound - should successfully sign DynamicFeeTx", func(t *testing.T) { + // ARRANGE + const ( + gwei = 1_000_000_000 + priorityFee = 1 * gwei + gasPrice = 3 * gwei + ) + + // Given a CCTX with gas price and priority fee + cctx := getCCTX(t) + cctx.OutboundParams[0].GasPrice = big.NewInt(gasPrice).String() + cctx.OutboundParams[0].GasPriorityFee = big.NewInt(priorityFee).String() + + // Given outbound data + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, 123, makeLogger(t)) + require.False(t, skip) + require.NoError(t, err) + + // Given a working TSS + tss.Unpause() + + // ACT + tx, err := evmSigner.SignOutbound(ctx, txData) + require.NoError(t, err) + + // ASSERT + verifyTxSignature(t, tx, mocks.NewTSSMainnet().Pubkey(), evmSigner.EvmSigner()) + + // check that by default tx type is a dynamic fee tx + assert.Equal(t, ethtypes.DynamicFeeTxType, int(tx.Type())) + + // check that the gasPrice & priorityFee are set correctly + assert.Equal(t, int64(gasPrice), tx.GasFeeCap().Int64()) + assert.Equal(t, int64(priorityFee), tx.GasTipCap().Int64()) + }) } func TestSigner_SignRevertTx(t *testing.T) { @@ -233,7 +273,7 @@ func TestSigner_SignRevertTx(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, 123, zerolog.Logger{}) require.False(t, skip) require.NoError(t, err) @@ -273,7 +313,7 @@ func TestSigner_SignCancelTx(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, 123, zerolog.Logger{}) require.False(t, skip) require.NoError(t, err) @@ -313,7 +353,7 @@ func TestSigner_SignWithdrawTx(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, 123, zerolog.Logger{}) require.False(t, skip) require.NoError(t, err) @@ -351,7 +391,7 @@ func TestSigner_SignCommandTx(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, 123, zerolog.Logger{}) require.False(t, skip) require.NoError(t, err) @@ -398,7 +438,7 @@ func TestSigner_SignERC20WithdrawTx(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, 123, zerolog.Logger{}) require.False(t, skip) require.NoError(t, err) @@ -439,7 +479,7 @@ func TestSigner_BroadcastOutbound(t *testing.T) { mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, 123, zerolog.Logger{}) require.NoError(t, err) require.False(t, skip) @@ -496,7 +536,7 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, 123, zerolog.Logger{}) require.NoError(t, err) require.False(t, skip) @@ -541,7 +581,7 @@ func TestSigner_SignMigrateTssFundsCmd(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, 123, zerolog.Logger{}) require.False(t, skip) require.NoError(t, err) @@ -576,9 +616,11 @@ func makeCtx(t *testing.T) context.Context { err := app.Update( observertypes.Keygen{}, - []chains.Chain{chains.BscMainnet}, + []chains.Chain{chains.BscMainnet, chains.ZetaChainMainnet}, nil, - map[int64]*observertypes.ChainParams{chains.BscMainnet.ChainId: &bscParams}, + map[int64]*observertypes.ChainParams{ + chains.BscMainnet.ChainId: &bscParams, + }, "tssPubKey", observertypes.CrosschainFlags{}, ) @@ -586,3 +628,7 @@ func makeCtx(t *testing.T) context.Context { return zctx.WithAppContext(context.Background(), app) } + +func makeLogger(t *testing.T) zerolog.Logger { + return zerolog.New(zerolog.NewTestWriter(t)) +}