From 2ca464abb8bf131dc5447233d948fd1f09920a99 Mon Sep 17 00:00:00 2001 From: Charlie Chen <34498985+ws4charlie@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:54:58 -0600 Subject: [PATCH] fix: hardcode erc20 asset strings to align with the data in foreign coin store (#3273) * hardcode erc20 asset string to align with foreign coin store data * Update zetaclient/chains/evm/observer/inbound.go added Github issue link above hardcoded variable Co-authored-by: Lucas Bertrand * Update zetaclient/chains/evm/observer/inbound.go added Github issue link around the hotfix Co-authored-by: Lucas Bertrand * Update zetaclient/chains/evm/observer/v2_inbound.go added Github issue link around the hotfix Co-authored-by: Lucas Bertrand * Update zetaclient/chains/evm/observer/v2_inbound.go add Github issue at the hotfix Co-authored-by: Lucas Bertrand * remove checksum asset from erc20AddressToForeignCoinAssetMap to reduce the list * fix unit test --------- Co-authored-by: Lucas Bertrand --- pkg/contracts/solana/inbound_test.go | 23 ++++- zetaclient/chains/evm/observer/inbound.go | 74 ++++++++++++++- .../chains/evm/observer/inbound_test.go | 89 +++++++++++++++++-- zetaclient/chains/evm/observer/v2_inbound.go | 24 ++++- 4 files changed, 199 insertions(+), 11 deletions(-) diff --git a/pkg/contracts/solana/inbound_test.go b/pkg/contracts/solana/inbound_test.go index b8d1354a1f..bce251d977 100644 --- a/pkg/contracts/solana/inbound_test.go +++ b/pkg/contracts/solana/inbound_test.go @@ -160,7 +160,28 @@ func Test_ParseInboundAsDepositAndCall(t *testing.T) { // solana e2e deployer account sender := "37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ" // example contract deployed during e2e test, read from tx result - expectedReceiver := []byte{117, 160, 106, 140, 37, 135, 57, 218, 223, 226, 53, 45, 87, 151, 61, 239, 158, 231, 162, 186} + expectedReceiver := []byte{ + 117, + 160, + 106, + 140, + 37, + 135, + 57, + 218, + 223, + 226, + 53, + 45, + 87, + 151, + 61, + 239, + 158, + 231, + 162, + 186, + } expectedMsg := []byte("hello lamports") expectedDeposit := &Deposit{ Sender: sender, diff --git a/zetaclient/chains/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index 493a4e3c18..ea8e59416b 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -20,6 +20,7 @@ import ( "github.com/zeta-chain/protocol-contracts/v1/pkg/contracts/evm/erc20custody.sol" "github.com/zeta-chain/protocol-contracts/v1/pkg/contracts/evm/zetaconnector.non-eth.sol" + "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/pkg/memo" @@ -34,6 +35,67 @@ import ( "github.com/zeta-chain/node/zetaclient/zetacore" ) +var ( + // erc20AddressToForeignCoinAssetMap maps the chain id and foreign ERC20 address to the coin asset string + // this is currently necessary because of the following issue: https://github.com/zeta-chain/node/issues/3274 + erc20AddressToForeignCoinAssetMap = map[int64]map[ethcommon.Address]string{ + // Ethereum mainnet + chains.Ethereum.ChainId: { + // USDC.ETH + ethcommon.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"): "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // PEPE.ETH + ethcommon.HexToAddress("0x6982508145454ce325ddbe47a25d4ec3d2311933"): "0x6982508145454ce325ddbe47a25d4ec3d2311933", + // SHIB.ETH + ethcommon.HexToAddress("0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce"): "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", + // USDT.ETH + ethcommon.HexToAddress("0xdac17f958d2ee523a2206206994597c13d831ec7"): "0xdac17f958d2ee523a2206206994597c13d831ec7", + // DAI.ETH + ethcommon.HexToAddress("0x6b175474e89094c44da98b954eedeac495271d0f"): "0x6b175474e89094c44da98b954eedeac495271d0f", + }, + + // BSC mainnet + chains.BscMainnet.ChainId: { + // USDC.BSC + ethcommon.HexToAddress("0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d"): "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d", + // USDT.BSC + ethcommon.HexToAddress("0x55d398326f99059ff775485246999027b3197955"): "0x55d398326f99059ff775485246999027b3197955", + }, + + // Polygon mainnet + chains.Polygon.ChainId: { + // USDT.POL + ethcommon.HexToAddress("0xc2132d05d31c914a87c6611c10748aeb04b58e8f"): "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", + // USDC.POL + ethcommon.HexToAddress("0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"): "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + }, + + // Polygon Amoy + chains.Amoy.ChainId: { + // USDC.AMOY + ethcommon.HexToAddress("0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582"): "0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582", + }, + } +) + +// PatchZRC20Asset returns a patched asset string for the given chainID and erc20Address +// so that it matches the asset string in the foreign coin store. +func PatchZRC20Asset(chainID int64, erc20Address ethcommon.Address) string { + addressToAsset, found := erc20AddressToForeignCoinAssetMap[chainID] + if found { + // if found, convert the address to asset in foreigh coin store + asset, found := addressToAsset[erc20Address] + if found { + return asset + } + + // use the checksum address as asset string by default + return erc20Address.Hex() + } + + // use the checksum address as asset string by default + return erc20Address.Hex() +} + // WatchInbound watches evm chain for incoming txs and post votes to zetacore // TODO(revamp): move ticker function to a separate file func (ob *Observer) WatchInbound(ctx context.Context) error { @@ -672,11 +734,17 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( Msgf("thank you rich folk for your donation! tx %s chain %d", event.Raw.TxHash.Hex(), ob.Chain().ChainId) return nil } + + // get patched asset string so that it matches the asset in the foreign coin store + // TODO: remove once the checksum conversion is fixed in the protocol + // https://github.com/zeta-chain/node/issues/3274 + asset := PatchZRC20Asset(ob.Chain().ChainId, event.Asset) + message := hex.EncodeToString(event.Message) ob.Logger().Inbound.Info(). - Msgf("ERC20CustodyDeposited inbound detected on chain %d tx %s block %d from %s value %s message %s", + Msgf("ERC20CustodyDeposited inbound detected on chain %d tx %s block %d from %s value %s asset %s message %s", ob.Chain(). - ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender.Hex(), event.Amount.String(), message) + ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender.Hex(), event.Amount.String(), asset, message) return zetacore.GetInboundVoteMessage( sender.Hex(), @@ -690,7 +758,7 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( event.Raw.BlockNumber, 1_500_000, coin.CoinType_ERC20, - event.Asset.String(), + asset, ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), event.Raw.Index, ) diff --git a/zetaclient/chains/evm/observer/inbound_test.go b/zetaclient/chains/evm/observer/inbound_test.go index e3612678da..738690f21c 100644 --- a/zetaclient/chains/evm/observer/inbound_test.go +++ b/zetaclient/chains/evm/observer/inbound_test.go @@ -19,6 +19,7 @@ import ( "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/zetaclient/chains/evm" + "github.com/zeta-chain/node/zetaclient/chains/evm/observer" "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/config" "github.com/zeta-chain/node/zetaclient/testutils" @@ -26,6 +27,87 @@ import ( clienttypes "github.com/zeta-chain/node/zetaclient/types" ) +func Test_PatchZRC20Asset(t *testing.T) { + tests := []struct { + name string + chainID int64 + erc20Address ethcommon.Address + assetString string + }{ + // Ethereum Mainnet + { + name: "USDC.ETH", + chainID: chains.Ethereum.ChainId, + erc20Address: ethcommon.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), + assetString: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + }, + { + name: "PEPE.ETH", + chainID: chains.Ethereum.ChainId, + erc20Address: ethcommon.HexToAddress("0x6982508145454ce325ddbe47a25d4ec3d2311933"), + assetString: "0x6982508145454ce325ddbe47a25d4ec3d2311933", + }, + { + name: "SHIB.ETH", + chainID: chains.Ethereum.ChainId, + erc20Address: ethcommon.HexToAddress("0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce"), + assetString: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", + }, + { + name: "USDT.ETH", + chainID: chains.Ethereum.ChainId, + erc20Address: ethcommon.HexToAddress("0xdac17f958d2ee523a2206206994597c13d831ec7"), + assetString: "0xdac17f958d2ee523a2206206994597c13d831ec7", + }, + { + name: "DAI.ETH", + chainID: chains.Ethereum.ChainId, + erc20Address: ethcommon.HexToAddress("0x6b175474e89094c44da98b954eedeac495271d0f"), + assetString: "0x6b175474e89094c44da98b954eedeac495271d0f", + }, + // BSC Mainnet + { + name: "USDC.BSC", + chainID: chains.BscMainnet.ChainId, + erc20Address: ethcommon.HexToAddress("0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d"), + assetString: "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d", + }, + { + name: "USDT.BSC", + chainID: chains.BscMainnet.ChainId, + erc20Address: ethcommon.HexToAddress("0x55d398326f99059ff775485246999027b3197955"), + assetString: "0x55d398326f99059ff775485246999027b3197955", + }, + // Polygon Mainnet + { + name: "USDT.POL", + chainID: chains.Polygon.ChainId, + erc20Address: ethcommon.HexToAddress("0xc2132d05d31c914a87c6611c10748aeb04b58e8f"), + assetString: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", + }, + { + name: "USDC.POL", + chainID: chains.Polygon.ChainId, + erc20Address: ethcommon.HexToAddress("0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"), + assetString: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + }, + // Polygon Amoy + { + name: "USDC.AMOY", + chainID: chains.Amoy.ChainId, + erc20Address: ethcommon.HexToAddress("0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582"), + assetString: "0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + asset := observer.PatchZRC20Asset(tt.chainID, tt.erc20Address) + require.Equal(t, tt.assetString, asset) + }) + } +} + func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { // load archived ZetaSent inbound, receipt and cctx // https://etherscan.io/tx/0xf3935200c80f98502d5edc7e871ffc40ca898e134525c42c2ae3cbc5725f9d76 @@ -133,7 +215,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { ctx := context.Background() t.Run("should pass for archived inbound, receipt and cctx", func(t *testing.T) { - tx, receipt, cctx := testutils.LoadEVMInboundNReceiptNCctx( + tx, receipt, _ := testutils.LoadEVMInboundNReceiptNCctx( t, TestDataDir, chainID, @@ -144,9 +226,8 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) - ballot, err := ob.CheckAndVoteInboundTokenERC20(ctx, tx, receipt, false) + _, err := ob.CheckAndVoteInboundTokenERC20(ctx, tx, receipt, false) require.NoError(t, err) - require.Equal(t, cctx.InboundParams.BallotIndex, ballot) }) t.Run("should fail on unconfirmed inbound", func(t *testing.T) { tx, receipt, _ := testutils.LoadEVMInboundNReceiptNCctx( @@ -341,7 +422,6 @@ func Test_BuildInboundVoteMsgForDepositedEvent(t *testing.T) { chainID := chain.ChainId inboundHash := "0x4ea69a0e2ff36f7548ab75791c3b990e076e2a4bffeb616035b239b7d33843da" tx, receipt := testutils.LoadEVMInboundNReceipt(t, TestDataDir, chainID, inboundHash, coin.CoinType_ERC20) - cctx := testutils.LoadCctxByInbound(t, chainID, coin.CoinType_ERC20, inboundHash) // parse Deposited event ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) @@ -357,7 +437,6 @@ func Test_BuildInboundVoteMsgForDepositedEvent(t *testing.T) { t.Run("should return vote msg for archived Deposited event", func(t *testing.T) { msg := ob.BuildInboundVoteMsgForDepositedEvent(event, sender) require.NotNil(t, msg) - require.Equal(t, cctx.InboundParams.BallotIndex, msg.Digest()) }) t.Run("should return nil msg if sender is restricted", func(t *testing.T) { cfg.ComplianceConfig.RestrictedAddresses = []string{sender.Hex()} diff --git a/zetaclient/chains/evm/observer/v2_inbound.go b/zetaclient/chains/evm/observer/v2_inbound.go index 9688851af6..0d86bfffe7 100644 --- a/zetaclient/chains/evm/observer/v2_inbound.go +++ b/zetaclient/chains/evm/observer/v2_inbound.go @@ -181,6 +181,16 @@ func (ob *Observer) newDepositInboundVote(event *gatewayevm.GatewayEVMDeposited) isCrossChainCall = true } + // get patched asset string so that it matches the one in the foreign coin store + // TODO: remove once the checksum conversion is fixed in the protocol + // https://github.com/zeta-chain/node/issues/3274 + asset := PatchZRC20Asset(ob.Chain().ChainId, event.Asset) + if asset != event.Asset.Hex() { + ob.Logger(). + Inbound.Info(). + Msgf("newDepositInboundVote converted asset %s to %s for chain %d", event.Asset.Hex(), asset, ob.Chain().ChainId) + } + return *types.NewMsgVoteInbound( ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), event.Sender.Hex(), @@ -194,7 +204,7 @@ func (ob *Observer) newDepositInboundVote(event *gatewayevm.GatewayEVMDeposited) event.Raw.BlockNumber, zetacore.PostVoteInboundCallOptionsGasLimit, coinType, - event.Asset.Hex(), + asset, event.Raw.Index, types.ProtocolContractVersion_V2, false, // currently not relevant since calls are not arbitrary @@ -454,6 +464,16 @@ func (ob *Observer) newDepositAndCallInboundVote(event *gatewayevm.GatewayEVMDep coinType = coin.CoinType_Gas } + // get patched asset string so that it matches the one in the foreign coin store + // TODO: remove once the checksum conversion is fixed in the protocol + // https://github.com/zeta-chain/node/issues/3274 + asset := PatchZRC20Asset(ob.Chain().ChainId, event.Asset) + if asset != event.Asset.Hex() { + ob.Logger(). + Inbound.Info(). + Msgf("newDepositAndCallInboundVote converted asset %s to %s for chain %d", event.Asset.Hex(), asset, ob.Chain().ChainId) + } + return *types.NewMsgVoteInbound( ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), event.Sender.Hex(), @@ -467,7 +487,7 @@ func (ob *Observer) newDepositAndCallInboundVote(event *gatewayevm.GatewayEVMDep event.Raw.BlockNumber, 1_500_000, coinType, - event.Asset.Hex(), + asset, event.Raw.Index, types.ProtocolContractVersion_V2, false, // currently not relevant since calls are not arbitrary