Skip to content

Commit

Permalink
fix: sanity check events of ZetaSent/ZetaReceived/ZetaReverted/Withdr…
Browse files Browse the repository at this point in the history
…awn/Deposited (#1539)

* sanity check events of ZetaSent/ZetaReceived/ZetaRevertedWithdrawn/Deposited

* added unit tests, comments

---------

Co-authored-by: brewmaster012 <[email protected]>
  • Loading branch information
ws4charlie and brewmaster012 authored Jan 10, 2024
1 parent 3283191 commit 431eff5
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 31 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

### Fixes

* [1537](https://github.com/zeta-chain/node/issues/1537) - Sanity check events of ZetaSent/ZetaReceived/ZetaRevertedWithdrawn/Deposited
* [1530](https://github.com/zeta-chain/node/pull/1530) - Outbound tx confirmation/inclusion enhancement
* [1496](https://github.com/zeta-chain/node/issues/1496) - post block header for enabled EVM chains only
* [1518](https://github.com/zeta-chain/node/pull/1518) - Avoid duplicate keysign if an outTx is already pending
Expand Down
73 changes: 48 additions & 25 deletions zetaclient/evm_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ type EVMLog struct {
}

const (
DonationMessage = "I am rich!"
DonationMessage = "I am rich!"
TopicsZetaSent = 3 // [signature, zetaTxSenderAddress, destinationChainId] https://github.com/zeta-chain/protocol-contracts/blob/d65814debf17648a6c67d757ba03646415842790/contracts/evm/ZetaConnector.base.sol#L34
TopicsZetaReceived = 4 // [signature, sourceChainId, destinationAddress] https://github.com/zeta-chain/protocol-contracts/blob/d65814debf17648a6c67d757ba03646415842790/contracts/evm/ZetaConnector.base.sol#L45
TopicsZetaReverted = 3 // [signature, destinationChainId, internalSendHash] https://github.com/zeta-chain/protocol-contracts/blob/d65814debf17648a6c67d757ba03646415842790/contracts/evm/ZetaConnector.base.sol#L54
TopicsWithdrawn = 3 // [signature, recipient, asset] https://github.com/zeta-chain/protocol-contracts/blob/d65814debf17648a6c67d757ba03646415842790/contracts/evm/ERC20Custody.sol#L43
TopicsDeposited = 2 // [signature, asset] https://github.com/zeta-chain/protocol-contracts/blob/d65814debf17648a6c67d757ba03646415842790/contracts/evm/ERC20Custody.sol#L42
)

// EVMChainClient represents the chain configuration for an EVM chain
Expand Down Expand Up @@ -233,24 +238,28 @@ func (ob *EVMChainClient) GetChainParams() observertypes.ChainParams {
return ob.params
}

func (ob *EVMChainClient) GetConnectorContract() (*zetaconnector.ZetaConnectorNonEth, error) {
func (ob *EVMChainClient) GetConnectorContract() (ethcommon.Address, *zetaconnector.ZetaConnectorNonEth, error) {
addr := ethcommon.HexToAddress(ob.GetChainParams().ConnectorContractAddress)
return FetchConnectorContract(addr, ob.evmClient)
contract, err := FetchConnectorContract(addr, ob.evmClient)
return addr, contract, err
}

func (ob *EVMChainClient) GetConnectorContractEth() (*zetaconnectoreth.ZetaConnectorEth, error) {
func (ob *EVMChainClient) GetConnectorContractEth() (ethcommon.Address, *zetaconnectoreth.ZetaConnectorEth, error) {
addr := ethcommon.HexToAddress(ob.GetChainParams().ConnectorContractAddress)
return FetchConnectorContractEth(addr, ob.evmClient)
contract, err := FetchConnectorContractEth(addr, ob.evmClient)
return addr, contract, err
}

func (ob *EVMChainClient) GetZetaTokenNonEthContract() (*zeta.ZetaNonEth, error) {
func (ob *EVMChainClient) GetZetaTokenNonEthContract() (ethcommon.Address, *zeta.ZetaNonEth, error) {
addr := ethcommon.HexToAddress(ob.GetChainParams().ZetaTokenContractAddress)
return FetchZetaZetaNonEthTokenContract(addr, ob.evmClient)
contract, err := FetchZetaZetaNonEthTokenContract(addr, ob.evmClient)
return addr, contract, err
}

func (ob *EVMChainClient) GetERC20CustodyContract() (*erc20custody.ERC20Custody, error) {
func (ob *EVMChainClient) GetERC20CustodyContract() (ethcommon.Address, *erc20custody.ERC20Custody, error) {
addr := ethcommon.HexToAddress(ob.GetChainParams().Erc20CustodyContractAddress)
return FetchERC20CustodyContract(addr, ob.evmClient)
contract, err := FetchERC20CustodyContract(addr, ob.evmClient)
return addr, contract, err
}

func FetchConnectorContract(addr ethcommon.Address, client EVMRPCClient) (*zetaconnector.ZetaConnectorNonEth, error) {
Expand Down Expand Up @@ -378,7 +387,7 @@ func (ob *EVMChainClient) IsSendOutTxProcessed(sendHash string, nonce uint64, co
for _, vLog := range logs {
confHeight := vLog.BlockNumber + params.ConfirmationCount
// TODO rewrite this to return early if not confirmed
connector, err := ob.GetConnectorContract()
connectorAddr, connector, err := ob.GetConnectorContract()
if err != nil {
return false, false, fmt.Errorf("error getting connector contract: %w", err)
}
Expand All @@ -387,9 +396,11 @@ func (ob *EVMChainClient) IsSendOutTxProcessed(sendHash string, nonce uint64, co
logger.Info().Msgf("Found (outTx) sendHash %s on chain %s txhash %s", sendHash, ob.chain.String(), vLog.TxHash.Hex())
if confHeight <= ob.GetLastBlockHeight() {
logger.Info().Msg("Confirmed! Sending PostConfirmation to zetacore...")
if len(vLog.Topics) != 4 {
logger.Error().Msgf("wrong number of topics in log %d", len(vLog.Topics))
return false, false, fmt.Errorf("wrong number of topics in log %d", len(vLog.Topics))
// sanity check tx event
err = ob.CheckEvmTxLog(vLog, connectorAddr, transaction.Hash().Hex(), TopicsZetaReceived)
if err != nil {
logger.Error().Err(err).Msgf("CheckEvmTxLog error on ZetaReceived event, chain %d nonce %d txhash %s", ob.chain.ChainId, nonce, transaction.Hash().Hex())
return false, false, err
}
sendhash := vLog.Topics[3].Hex()
//var rxAddress string = ethcommon.HexToAddress(vLog.Topics[1].Hex()).Hex()
Expand Down Expand Up @@ -423,9 +434,11 @@ func (ob *EVMChainClient) IsSendOutTxProcessed(sendHash string, nonce uint64, co
logger.Info().Msgf("Found (revertTx) sendHash %s on chain %s txhash %s", sendHash, ob.chain.String(), vLog.TxHash.Hex())
if confHeight <= ob.GetLastBlockHeight() {
logger.Info().Msg("Confirmed! Sending PostConfirmation to zetacore...")
if len(vLog.Topics) != 3 {
logger.Error().Msgf("wrong number of topics in log %d", len(vLog.Topics))
return false, false, fmt.Errorf("wrong number of topics in log %d", len(vLog.Topics))
// sanity check tx event
err = ob.CheckEvmTxLog(vLog, connectorAddr, transaction.Hash().Hex(), TopicsZetaReverted)
if err != nil {
logger.Error().Err(err).Msgf("CheckEvmTxLog error on ZetaReverted event, chain %d nonce %d txhash %s", ob.chain.ChainId, nonce, transaction.Hash().Hex())
return false, false, err
}
sendhash := vLog.Topics[2].Hex()
mMint := revertedLog.RemainingZetaValue
Expand Down Expand Up @@ -480,7 +493,7 @@ func (ob *EVMChainClient) IsSendOutTxProcessed(sendHash string, nonce uint64, co
} else if cointype == common.CoinType_ERC20 {
if receipt.Status == 1 {
logs := receipt.Logs
ERC20Custody, err := ob.GetERC20CustodyContract()
addrCustody, ERC20Custody, err := ob.GetERC20CustodyContract()
if err != nil {
logger.Warn().Msgf("NewERC20Custody err: %s", err)
}
Expand All @@ -489,6 +502,12 @@ func (ob *EVMChainClient) IsSendOutTxProcessed(sendHash string, nonce uint64, co
confHeight := vLog.BlockNumber + params.ConfirmationCount
if err == nil {
logger.Info().Msgf("Found (ERC20Custody.Withdrawn Event) sendHash %s on chain %s txhash %s", sendHash, ob.chain.String(), vLog.TxHash.Hex())
// sanity check tx event
err = ob.CheckEvmTxLog(vLog, addrCustody, transaction.Hash().Hex(), TopicsWithdrawn)
if err != nil {
logger.Error().Err(err).Msgf("CheckEvmTxLog error on Withdrawn event, chain %d nonce %d txhash %s", ob.chain.ChainId, nonce, transaction.Hash().Hex())
return false, false, err
}
if confHeight <= ob.GetLastBlockHeight() {
logger.Info().Msg("Confirmed! Sending PostConfirmation to zetacore...")
zetaTxHash, ballot, err := ob.zetaClient.PostReceiveConfirmation(
Expand Down Expand Up @@ -893,7 +912,7 @@ func (ob *EVMChainClient) observeInTX(sampledLogger zerolog.Logger) error {
// returns the last block successfully scanned
func (ob *EVMChainClient) observeZetaSent(startBlock, toBlock uint64) uint64 {
// filter ZetaSent logs
connector, err := ob.GetConnectorContract()
addrConnector, connector, err := ob.GetConnectorContract()
if err != nil {
ob.logger.ChainLogger.Warn().Err(err).Msgf("observeZetaSent: GetConnectorContract error:")
return startBlock - 1 // lastScanned
Expand All @@ -912,12 +931,14 @@ func (ob *EVMChainClient) observeZetaSent(startBlock, toBlock uint64) uint64 {
// collect and sort events by block number, then tx index, then log index (ascending)
events := make([]*zetaconnector.ZetaConnectorNonEthZetaSent, 0)
for iter.Next() {
if !iter.Event.Raw.Removed && iter.Event.Raw.BlockNumber > 0 { // skip if chain reorg removed this event
// sanity check tx event
err := ob.CheckEvmTxLog(&iter.Event.Raw, addrConnector, "", TopicsZetaSent)
if err == nil {
events = append(events, iter.Event)
continue
}
ob.logger.ExternalChainWatcher.Warn().Msgf("observeZetaSent: invalid event in tx %s at height %d for chain %d",
iter.Event.Raw.TxHash.Hex(), iter.Event.Raw.BlockNumber, ob.chain.ChainId)
ob.logger.ExternalChainWatcher.Warn().Err(err).Msgf("observeZetaSent: invalid ZetaSent event in tx %s on chain %d at height %d",
iter.Event.Raw.TxHash.Hex(), ob.chain.ChainId, iter.Event.Raw.BlockNumber)
}
sort.SliceStable(events, func(i, j int) bool {
if events[i].Raw.BlockNumber == events[j].Raw.BlockNumber {
Expand Down Expand Up @@ -970,7 +991,7 @@ func (ob *EVMChainClient) observeZetaSent(startBlock, toBlock uint64) uint64 {
// returns the last block successfully scanned
func (ob *EVMChainClient) observeERC20Deposited(startBlock, toBlock uint64) uint64 {
// filter ERC20CustodyDeposited logs
erc20custodyContract, err := ob.GetERC20CustodyContract()
addrCustody, erc20custodyContract, err := ob.GetERC20CustodyContract()
if err != nil {
ob.logger.ExternalChainWatcher.Warn().Err(err).Msgf("observeERC20Deposited: GetERC20CustodyContract error:")
return startBlock - 1 // lastScanned
Expand All @@ -989,12 +1010,14 @@ func (ob *EVMChainClient) observeERC20Deposited(startBlock, toBlock uint64) uint
// collect and sort events by block number, then tx index, then log index (ascending)
events := make([]*erc20custody.ERC20CustodyDeposited, 0)
for iter.Next() {
if !iter.Event.Raw.Removed && iter.Event.Raw.BlockNumber > 0 { // skip if chain reorg removed this event
// sanity check tx event
err := ob.CheckEvmTxLog(&iter.Event.Raw, addrCustody, "", TopicsDeposited)
if err == nil {
events = append(events, iter.Event)
continue
}
ob.logger.ExternalChainWatcher.Warn().Msgf("observeERC20Deposited: invalid event in tx %s at height %d for chain %d",
iter.Event.Raw.TxHash.Hex(), iter.Event.Raw.BlockNumber, ob.chain.ChainId)
ob.logger.ExternalChainWatcher.Warn().Err(err).Msgf("observeERC20Deposited: invalid Deposited event in tx %s on chain %d at height %d",
iter.Event.Raw.TxHash.Hex(), ob.chain.ChainId, iter.Event.Raw.BlockNumber)
}
sort.SliceStable(events, func(i, j int) bool {
if events[i].Raw.BlockNumber == events[j].Raw.BlockNumber {
Expand Down
44 changes: 38 additions & 6 deletions zetaclient/inbound_tracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func (ob *EVMChainClient) ObserveTrackerSuggestions() error {
}

func (ob *EVMChainClient) CheckReceiptForCoinTypeZeta(txHash string, vote bool) (string, error) {
connector, err := ob.GetConnectorContract()
addrConnector, connector, err := ob.GetConnectorContract()
if err != nil {
return "", err
}
Expand All @@ -166,13 +166,25 @@ func (ob *EVMChainClient) CheckReceiptForCoinTypeZeta(txHash string, vote bool)
return "", err
}

// check if the tx is confirmed
lastHeight := ob.GetLastBlockHeight()
if !ob.HasEnoughConfirmations(receipt, lastHeight) {
return "", fmt.Errorf("txHash %s has not been confirmed yet: receipt block %d current block %d", txHash, receipt.BlockNumber, lastHeight)
}

var msg types.MsgVoteOnObservedInboundTx
for _, log := range receipt.Logs {
event, err := connector.ParseZetaSent(*log)
if err == nil && event != nil {
msg, err = ob.GetInboundVoteMsgForZetaSentEvent(event)
// sanity check tx event
err = ob.CheckEvmTxLog(&event.Raw, addrConnector, txHash, TopicsZetaSent)
if err == nil {
break
msg, err = ob.GetInboundVoteMsgForZetaSentEvent(event)
if err == nil {
break
}
} else {
ob.logger.ExternalChainWatcher.Error().Err(err).Msg("CheckEvmTxLog error on ZetaSent event")
}
}
}
Expand All @@ -192,7 +204,7 @@ func (ob *EVMChainClient) CheckReceiptForCoinTypeZeta(txHash string, vote bool)
}

func (ob *EVMChainClient) CheckReceiptForCoinTypeERC20(txHash string, vote bool) (string, error) {
custody, err := ob.GetERC20CustodyContract()
addrCustory, custody, err := ob.GetERC20CustodyContract()
if err != nil {
return "", err
}
Expand All @@ -201,13 +213,26 @@ func (ob *EVMChainClient) CheckReceiptForCoinTypeERC20(txHash string, vote bool)
if err != nil {
return "", err
}

// check if the tx is confirmed
lastHeight := ob.GetLastBlockHeight()
if !ob.HasEnoughConfirmations(receipt, lastHeight) {
return "", fmt.Errorf("txHash %s has not been confirmed yet: receipt block %d current block %d", txHash, receipt.BlockNumber, lastHeight)
}

var msg types.MsgVoteOnObservedInboundTx
for _, log := range receipt.Logs {
zetaDeposited, err := custody.ParseDeposited(*log)
if err == nil && zetaDeposited != nil {
msg, err = ob.GetInboundVoteMsgForDepositedEvent(zetaDeposited)
// sanity check tx event
err = ob.CheckEvmTxLog(&zetaDeposited.Raw, addrCustory, txHash, TopicsDeposited)
if err == nil {
break
msg, err = ob.GetInboundVoteMsgForDepositedEvent(zetaDeposited)
if err == nil {
break
}
} else {
ob.logger.ExternalChainWatcher.Error().Err(err).Msg("CheckEvmTxLog error on Deposited event")
}
}
}
Expand Down Expand Up @@ -245,6 +270,13 @@ func (ob *EVMChainClient) CheckReceiptForCoinTypeGas(txHash string, vote bool) (
ob.logger.ExternalChainWatcher.Info().Msgf("tx %s failed; don't act", tx.Hash().Hex())
return "", errors.New("tx not successful yet")
}

// check if the tx is confirmed
lastHeight := ob.GetLastBlockHeight()
if !ob.HasEnoughConfirmations(receipt, lastHeight) {
return "", fmt.Errorf("txHash %s has not been confirmed yet: receipt block %d current block %d", txHash, receipt.BlockNumber, lastHeight)
}

block, err := ob.evmClient.BlockByNumber(context.Background(), receipt.BlockNumber)
if err != nil {
ob.logger.ExternalChainWatcher.Err(err).Msg("BlockByNumber error")
Expand Down
26 changes: 26 additions & 0 deletions zetaclient/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,32 @@ func (t *DynamicTicker) Stop() {
t.impl.Stop()
}

// CheckEvmTxLog checks the basics of an EVM tx log
func (ob *EVMChainClient) CheckEvmTxLog(vLog *ethtypes.Log, wantAddress ethcommon.Address, wantHash string, wantTopics int) error {
if vLog.Removed {
return fmt.Errorf("log is removed, chain reorg?")
}
if vLog.Address != wantAddress {
return fmt.Errorf("log emitter address mismatch: want %s got %s", wantAddress.Hex(), vLog.Address.Hex())
}
if vLog.TxHash.Hex() == "" {
return fmt.Errorf("log tx hash is empty: %d %s", vLog.BlockNumber, vLog.TxHash.Hex())
}
if wantHash != "" && vLog.TxHash.Hex() != wantHash {
return fmt.Errorf("log tx hash mismatch: want %s got %s", wantHash, vLog.TxHash.Hex())
}
if len(vLog.Topics) != wantTopics {
return fmt.Errorf("number of topics mismatch: want %d got %d", wantTopics, len(vLog.Topics))
}
return nil
}

// HasEnoughConfirmations checks if the given receipt has enough confirmations
func (ob *EVMChainClient) HasEnoughConfirmations(receipt *ethtypes.Receipt, lastHeight uint64) bool {
confHeight := receipt.BlockNumber.Uint64() + ob.GetChainParams().ConfirmationCount
return lastHeight >= confHeight
}

func (ob *EVMChainClient) GetInboundVoteMsgForDepositedEvent(event *erc20custody.ERC20CustodyDeposited) (types.MsgVoteOnObservedInboundTx, error) {
ob.logger.ExternalChainWatcher.Info().Msgf("TxBlockNumber %d Transaction Hash: %s Message : %s", event.Raw.BlockNumber, event.Raw.TxHash, event.Message)
if bytes.Equal(event.Message, []byte(DonationMessage)) {
Expand Down
102 changes: 102 additions & 0 deletions zetaclient/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package zetaclient

import (
"fmt"
"testing"

ethcommon "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/require"
)

func TestCheckEvmTxLog(t *testing.T) {
// test data
connectorAddr := ethcommon.HexToAddress("0x00005e3125aba53c5652f9f0ce1a4cf91d8b15ea")
txHash := "0xb252c9e77feafdeeae25cc1f037a16c4b50fa03c494754b99a7339d816c79626"
topics := []ethcommon.Hash{
// https://goerli.etherscan.io/tx/0xb252c9e77feafdeeae25cc1f037a16c4b50fa03c494754b99a7339d816c79626#eventlog
ethcommon.HexToHash("0x7ec1c94701e09b1652f3e1d307e60c4b9ebf99aff8c2079fd1d8c585e031c4e4"),
ethcommon.HexToHash("0x00000000000000000000000023856df5d563bd893fc7df864102d8bbfe7fc487"),
ethcommon.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000061"),
}

tests := []struct {
name string
vLog *ethtypes.Log
fail bool
}{
{
name: "chain reorganization",
vLog: &ethtypes.Log{
Removed: true,
Address: connectorAddr,
TxHash: ethcommon.HexToHash(txHash),
Topics: topics,
},
fail: true,
},
{
name: "emitter address mismatch",
vLog: &ethtypes.Log{
Removed: false,
Address: ethcommon.HexToAddress("0x184ba627DB853244c9f17f3Cb4378cB8B39bf147"),
TxHash: ethcommon.HexToHash(txHash),
Topics: topics,
},
fail: true,
},
{
name: "tx hash mismatch",
vLog: &ethtypes.Log{
Removed: false,
Address: connectorAddr,
TxHash: ethcommon.HexToHash("0x781c018d604af4dad0fe5e3cea4ad9fb949a996d8cd0cd04a92cadd7f08c05f2"),
Topics: topics,
},
fail: true,
},
{
name: "topics mismatch",
vLog: &ethtypes.Log{
Removed: false,
Address: connectorAddr,
TxHash: ethcommon.HexToHash(txHash),
Topics: []ethcommon.Hash{
// https://goerli.etherscan.io/tx/0xb252c9e77feafdeeae25cc1f037a16c4b50fa03c494754b99a7339d816c79626#eventlog
ethcommon.HexToHash("0x7ec1c94701e09b1652f3e1d307e60c4b9ebf99aff8c2079fd1d8c585e031c4e4"),
ethcommon.HexToHash("0x00000000000000000000000023856df5d563bd893fc7df864102d8bbfe7fc487"),
},
},
fail: true,
},
{
name: "should pass",
vLog: &ethtypes.Log{
Removed: false,
Address: connectorAddr,
TxHash: ethcommon.HexToHash(txHash),
Topics: topics,
},
fail: false,
},
}

evmClient := EVMChainClient{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fmt.Printf("check test: %s\n", tt.name)
err := evmClient.CheckEvmTxLog(
tt.vLog,
connectorAddr,
"0xb252c9e77feafdeeae25cc1f037a16c4b50fa03c494754b99a7339d816c79626",
TopicsZetaSent,
)
if tt.fail {
require.Error(t, err)
return
} else {
require.NoError(t, err)
}
})
}
}

0 comments on commit 431eff5

Please sign in to comment.