From 8779394041a86a2e6df609513856a09de407dc38 Mon Sep 17 00:00:00 2001 From: zsystm Date: Tue, 1 Apr 2025 09:10:01 +0900 Subject: [PATCH 1/9] add basic test cases for ibc middleware v1, v2 more test cases will be added. --- tests/ibc/ibc_middleware_test.go | 91 ++++++++++++++++++++++++++++ tests/ibc/v2_ibc_middleware_test.go | 94 +++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 tests/ibc/ibc_middleware_test.go create mode 100644 tests/ibc/v2_ibc_middleware_test.go diff --git a/tests/ibc/ibc_middleware_test.go b/tests/ibc/ibc_middleware_test.go new file mode 100644 index 000000000..512b83809 --- /dev/null +++ b/tests/ibc/ibc_middleware_test.go @@ -0,0 +1,91 @@ +package ibc + +import ( + "errors" + "testing" + + ibctesting "github.com/cosmos/ibc-go/v10/testing" + testifysuite "github.com/stretchr/testify/suite" + + "github.com/cosmos/evm/ibc" + evmibctesting "github.com/cosmos/evm/ibc/testing" + "github.com/cosmos/evm/x/erc20" + erc20Keeper "github.com/cosmos/evm/x/erc20/keeper" +) + +// MiddlewareTestSuite tests the IBC middleware for the ERC20 module. +type MiddlewareTestSuite struct { + testifysuite.Suite + + coordinator *ibctesting.Coordinator + + // testing chains used for convenience and readability + evmChainA *ibctesting.TestChain + chainB *ibctesting.TestChain + + // chainB to evmChainA for testing OnRecvPacket, OnAckPacket, and OnTimeoutPacket + pathBToA *ibctesting.Path +} + +func (suite *MiddlewareTestSuite) SetupTest() { + suite.coordinator = evmibctesting.NewCoordinator(suite.T(), 1, 2) + suite.evmChainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) + suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(2)) + + // setup between chainB and evmChainA + // pathBToA.EndpointA = endpoint on chainB + // pathBToA.EndpointB = endpoint on evmChainA + suite.pathBToA = ibctesting.NewPath(suite.chainB, suite.evmChainA) +} + +func TestMiddlewareTestSuite(t *testing.T) { + testifysuite.Run(t, new(MiddlewareTestSuite)) +} + +func (s *MiddlewareTestSuite) TestNewIBCMiddleware() { + testCases := []struct { + name string + instantiateFn func() + expError error + }{ + { + "success", + func() { + _ = erc20.NewIBCMiddleware(erc20Keeper.Keeper{}, ibc.Module{}) + }, + nil, + }, + { + "panics with nil underlying app", + func() { + _ = erc20.NewIBCMiddleware(erc20Keeper.Keeper{}, nil) + }, + errors.New("underlying application cannot be nil"), + }, + { + "panics with nil erc20 keeper", + func() { + _ = erc20.NewIBCMiddleware(nil, ibc.Module{}) + }, + errors.New("erc20 keeper cannot be nil"), + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + if tc.expError == nil { + s.Require().NotPanics( + tc.instantiateFn, + "unexpected panic: NewIBCMiddleware", + ) + } else { + s.Require().PanicsWithError( + tc.expError.Error(), + tc.instantiateFn, + "expected panic with error: ", tc.expError.Error(), + ) + } + }) + } +} diff --git a/tests/ibc/v2_ibc_middleware_test.go b/tests/ibc/v2_ibc_middleware_test.go new file mode 100644 index 000000000..f066b5588 --- /dev/null +++ b/tests/ibc/v2_ibc_middleware_test.go @@ -0,0 +1,94 @@ +package ibc + +import ( + "errors" + "testing" + + ibctesting "github.com/cosmos/ibc-go/v10/testing" + ibcmockv2 "github.com/cosmos/ibc-go/v10/testing/mock/v2" + testifysuite "github.com/stretchr/testify/suite" + + evmibctesting "github.com/cosmos/evm/ibc/testing" + erc20Keeper "github.com/cosmos/evm/x/erc20/keeper" + "github.com/cosmos/evm/x/erc20/v2" +) + +// MiddlewareTestSuite tests the v2 IBC middleware for the ERC20 module. +type MiddlewareV2TestSuite struct { + testifysuite.Suite + + coordinator *ibctesting.Coordinator + + // testing chains used for convenience and readability + evmChainA *ibctesting.TestChain + chainB *ibctesting.TestChain + + // chainB to evmChainA for testing OnRecvPacket, OnAckPacket, and OnTimeoutPacket + pathBToA *ibctesting.Path +} + +func (suite *MiddlewareV2TestSuite) SetupTest() { + suite.coordinator = evmibctesting.NewCoordinator(suite.T(), 1, 2) + suite.evmChainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) + suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(2)) + + // setup between chainB and evmChainA + // pathBToA.EndpointA = endpoint on chainB + // pathBToA.EndpointB = endpoint on evmChainA + suite.pathBToA = ibctesting.NewPath(suite.chainB, suite.evmChainA) + + // setup IBC v2 paths between the chains + suite.pathBToA.SetupV2() +} + +func TestMiddlewareV2TestSuite(t *testing.T) { + testifysuite.Run(t, new(MiddlewareV2TestSuite)) +} + +func (s *MiddlewareV2TestSuite) TestNewIBCMiddleware() { + testCases := []struct { + name string + instantiateFn func() + expError error + }{ + { + "success", + func() { + _ = v2.NewIBCMiddleware(ibcmockv2.IBCModule{}, erc20Keeper.Keeper{}) + }, + nil, + }, + { + "panics with nil underlying app", + func() { + _ = v2.NewIBCMiddleware(nil, erc20Keeper.Keeper{}) + }, + errors.New("underlying application cannot be nil"), + }, + { + "panics with nil erc20 keeper", + func() { + _ = v2.NewIBCMiddleware(ibcmockv2.IBCModule{}, nil) + }, + errors.New("erc20 keeper cannot be nil"), + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + if tc.expError == nil { + s.Require().NotPanics( + tc.instantiateFn, + "unexpected panic: NewIBCMiddleware", + ) + } else { + s.Require().PanicsWithError( + tc.expError.Error(), + tc.instantiateFn, + "expected panic with error: ", tc.expError.Error(), + ) + } + }) + } +} From 968f81b45302737346905a9f1be9209273c13377 Mon Sep 17 00:00:00 2001 From: zsystm Date: Tue, 1 Apr 2025 16:05:19 +0900 Subject: [PATCH 2/9] add test cases for ibc middleware v2 --- tests/ibc/v2_ibc_middleware_test.go | 384 +++++++++++++++++++++++++++- 1 file changed, 382 insertions(+), 2 deletions(-) diff --git a/tests/ibc/v2_ibc_middleware_test.go b/tests/ibc/v2_ibc_middleware_test.go index f066b5588..d2dcf3f07 100644 --- a/tests/ibc/v2_ibc_middleware_test.go +++ b/tests/ibc/v2_ibc_middleware_test.go @@ -4,10 +4,15 @@ import ( "errors" "testing" + sdk "github.com/cosmos/cosmos-sdk/types" + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" ibctesting "github.com/cosmos/ibc-go/v10/testing" ibcmockv2 "github.com/cosmos/ibc-go/v10/testing/mock/v2" testifysuite "github.com/stretchr/testify/suite" + "github.com/cosmos/evm/evmd" evmibctesting "github.com/cosmos/evm/ibc/testing" erc20Keeper "github.com/cosmos/evm/x/erc20/keeper" "github.com/cosmos/evm/x/erc20/v2" @@ -23,21 +28,28 @@ type MiddlewareV2TestSuite struct { evmChainA *ibctesting.TestChain chainB *ibctesting.TestChain - // chainB to evmChainA for testing OnRecvPacket, OnAckPacket, and OnTimeoutPacket + // evmChainA to chainB for testing OnSendPacket, OnAckPacket, and OnTimeoutPacket + pathAToB *ibctesting.Path + // chainB to evmChainA for testing OnRecvPacket pathBToA *ibctesting.Path } func (suite *MiddlewareV2TestSuite) SetupTest() { - suite.coordinator = evmibctesting.NewCoordinator(suite.T(), 1, 2) + suite.coordinator = evmibctesting.NewCoordinator(suite.T(), 1, 1) suite.evmChainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(2)) + // setup between evmChainA and chainB + // pathAToB.EndpointA = endpoint on evmChainA + // pathAToB.EndpointB = endpoint on chainB + suite.pathAToB = ibctesting.NewPath(suite.evmChainA, suite.chainB) // setup between chainB and evmChainA // pathBToA.EndpointA = endpoint on chainB // pathBToA.EndpointB = endpoint on evmChainA suite.pathBToA = ibctesting.NewPath(suite.chainB, suite.evmChainA) // setup IBC v2 paths between the chains + suite.pathAToB.SetupV2() suite.pathBToA.SetupV2() } @@ -92,3 +104,371 @@ func (s *MiddlewareV2TestSuite) TestNewIBCMiddleware() { }) } } + +func (s *MiddlewareV2TestSuite) TestOnSendPacket() { + var ( + ctx sdk.Context + packetData transfertypes.FungibleTokenPacketData + payload channeltypesv2.Payload + ) + + testCases := []struct { + name string + malleate func() + expError string + }{ + { + name: "pass", + malleate: nil, + expError: "", + }, + { + name: "fail: malformed packet data", + malleate: func() { + payload.Value = []byte("malformed") + }, + expError: "cannot unmarshal ICS20-V1 transfer packet data", + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + s.SetupTest() + ctx = s.evmChainA.GetContext() + evmApp := s.evmChainA.App.(*evmd.EVMD) + bondDenom, err := evmApp.StakingKeeper.BondDenom(ctx) + s.Require().NoError(err) + packetData = transfertypes.NewFungibleTokenPacketData( + bondDenom, + ibctesting.DefaultCoinAmount.String(), + s.evmChainA.SenderAccount.GetAddress().String(), + s.chainB.SenderAccount.GetAddress().String(), + "", + ) + + payload = channeltypesv2.NewPayload( + transfertypes.PortID, transfertypes.PortID, + transfertypes.V1, transfertypes.EncodingJSON, + packetData.GetBytes(), + ) + + if tc.malleate != nil { + tc.malleate() + } + + onSendPacket := func() error { + return evmApp.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort).OnSendPacket( + ctx, + s.pathAToB.EndpointA.ClientID, + s.pathAToB.EndpointB.ClientID, + 1, + payload, + s.evmChainA.SenderAccount.GetAddress(), + ) + } + + err = onSendPacket() + if tc.expError != "" { + s.Require().Error(err) + s.Require().ErrorContains(err, tc.expError) + } else { + s.Require().NoError(err) + // check that the escrowed coins are in the escrow account + escrowAddress := transfertypes.GetEscrowAddress( + transfertypes.PortID, + s.pathAToB.EndpointA.ClientID, + ) + escrowedCoins := evmApp.BankKeeper.GetAllBalances(ctx, escrowAddress) + s.Require().Equal(1, len(escrowedCoins)) + s.Require().Equal(ibctesting.DefaultCoinAmount.String(), escrowedCoins[0].Amount.String()) + s.Require().Equal(bondDenom, escrowedCoins[0].Denom) + } + }) + } +} + +func (s *MiddlewareV2TestSuite) TestOnRecvPacket() { + var ( + ctx sdk.Context + packetData transfertypes.FungibleTokenPacketData + payload channeltypesv2.Payload + ) + + testCases := []struct { + name string + malleate func() + expResult channeltypesv2.PacketStatus + }{ + { + name: "pass", + malleate: nil, + expResult: channeltypesv2.PacketStatus_Success, + }, + { + name: "fail: malformed packet data", + malleate: func() { + payload.Value = []byte("malformed") + }, + expResult: channeltypesv2.PacketStatus_Failure, + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + s.SetupTest() + ctx = s.chainB.GetContext() + bondDenom, err := s.chainB.GetSimApp().StakingKeeper.BondDenom(ctx) + s.Require().NoError(err) + packetData = transfertypes.NewFungibleTokenPacketData( + bondDenom, + ibctesting.DefaultCoinAmount.String(), + s.chainB.SenderAccount.GetAddress().String(), + s.evmChainA.SenderAccount.GetAddress().String(), + "", + ) + + payload = channeltypesv2.NewPayload( + transfertypes.PortID, transfertypes.PortID, + transfertypes.V1, transfertypes.EncodingJSON, + packetData.GetBytes(), + ) + + if tc.malleate != nil { + tc.malleate() + } + + // erc20 module is routed as top level middleware + erc20mod := s.evmChainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort) + onRecvPacket := func() channeltypesv2.RecvPacketResult { + ctx = s.evmChainA.GetContext() + return erc20mod.OnRecvPacket( + ctx, + s.pathBToA.EndpointB.ClientID, + s.pathBToA.EndpointA.ClientID, + 1, + payload, + s.evmChainA.SenderAccount.GetAddress(), + ) + } + + recvResult := onRecvPacket() + s.Require().Equal(tc.expResult, recvResult.Status) + }) + } +} + +func (s *MiddlewareV2TestSuite) TestOnAcknowledgementPacket() { + var ( + ctx sdk.Context + packetData transfertypes.FungibleTokenPacketData + ack []byte + payload channeltypesv2.Payload + ) + + testCases := []struct { + name string + malleate func() + onSendRequired bool + expError string + }{ + { + name: "pass", + malleate: nil, + onSendRequired: false, + expError: "", + }, + { + name: "pass: refund escrowed token because ack err(UNIVERSAL_ERROR_ACKNOWLEDGEMENT)", + malleate: func() { + ack = channeltypesv2.ErrorAcknowledgement[:] + }, + onSendRequired: true, // this test case handles the refund of the escrowed token, so we need to call OnSendPacket. + expError: "", + }, + { + name: "fail: malformed packet data", + malleate: func() { + payload.Value = []byte("malformed") + }, + onSendRequired: false, + expError: "cannot unmarshal ICS20-V1 transfer packet data", + }, + { + name: "fail: empty ack", + malleate: func() { + ack = []byte{} + }, + onSendRequired: false, + expError: "cannot unmarshal ICS-20 transfer packet acknowledgement", + }, + { + name: "fail: ack error", + malleate: func() { + ackErr := channeltypes.NewErrorAcknowledgement(errors.New("error")) + ack = ackErr.Acknowledgement() + }, + onSendRequired: false, + expError: "cannot pass in a custom error acknowledgement with IBC v2", + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + s.SetupTest() + ctx = s.evmChainA.GetContext() + evmApp := s.evmChainA.App.(*evmd.EVMD) + bondDenom, err := evmApp.StakingKeeper.BondDenom(ctx) + s.Require().NoError(err) + packetData = transfertypes.NewFungibleTokenPacketData( + bondDenom, + ibctesting.DefaultCoinAmount.String(), + s.evmChainA.SenderAccount.GetAddress().String(), + s.chainB.SenderAccount.GetAddress().String(), + "", + ) + + ack = channeltypes.NewResultAcknowledgement([]byte{1}).Acknowledgement() + + payload = channeltypesv2.NewPayload( + transfertypes.PortID, transfertypes.PortID, + transfertypes.V1, transfertypes.EncodingJSON, + packetData.GetBytes(), + ) + + if tc.malleate != nil { + tc.malleate() + } + + // erc20 module is routed as top level middleware + erc20mod := s.evmChainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort) + if tc.onSendRequired { + s.NoError(erc20mod.OnSendPacket( + ctx, + s.pathAToB.EndpointA.ClientID, + s.pathAToB.EndpointB.ClientID, + 1, + payload, + s.evmChainA.SenderAccount.GetAddress(), + )) + } + onAckPacket := func() error { + return erc20mod.OnAcknowledgementPacket( + ctx, + s.pathAToB.EndpointA.ClientID, + s.pathAToB.EndpointB.ClientID, + 1, + ack, + payload, + s.evmChainA.SenderAccount.GetAddress(), + ) + } + + err = onAckPacket() + if tc.expError != "" { + s.Require().Error(err) + s.Require().ErrorContains(err, tc.expError) + } else { + s.Require().NoError(err) + } + }) + } +} + +func (s *MiddlewareV2TestSuite) TestOnTimeoutPacket() { + var ( + ctx sdk.Context + packetData transfertypes.FungibleTokenPacketData + payload channeltypesv2.Payload + ) + + testCases := []struct { + name string + malleate func() + onSendRequired bool + expError string + }{ + { + name: "pass", + malleate: nil, + onSendRequired: true, + expError: "", + }, + { + name: "fail: malformed packet data", + malleate: func() { + payload.Value = []byte("malformed") + }, + onSendRequired: false, // malformed packet data cannot be sent + expError: "cannot unmarshal ICS20-V1 transfer packet data", + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + s.SetupTest() + ctx = s.evmChainA.GetContext() + evmApp := s.evmChainA.App.(*evmd.EVMD) + bondDenom, err := evmApp.StakingKeeper.BondDenom(ctx) + s.Require().NoError(err) + packetData = transfertypes.NewFungibleTokenPacketData( + bondDenom, + ibctesting.DefaultCoinAmount.String(), + s.evmChainA.SenderAccount.GetAddress().String(), + s.chainB.SenderAccount.GetAddress().String(), + "", + ) + + payload = channeltypesv2.NewPayload( + transfertypes.PortID, transfertypes.PortID, + transfertypes.V1, transfertypes.EncodingJSON, + packetData.GetBytes(), + ) + + if tc.malleate != nil { + tc.malleate() + } + + erc20mod := s.evmChainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort) + if tc.onSendRequired { + s.NoError(erc20mod.OnSendPacket( + ctx, + s.pathAToB.EndpointA.ClientID, + s.pathAToB.EndpointB.ClientID, + 1, + payload, + s.evmChainA.SenderAccount.GetAddress(), + )) + } + + onTimeoutPacket := func() error { + return erc20mod.OnTimeoutPacket( + ctx, + s.pathAToB.EndpointA.ClientID, + s.pathAToB.EndpointB.ClientID, + 1, + payload, + s.evmChainA.SenderAccount.GetAddress(), + ) + } + + err = onTimeoutPacket() + if tc.expError != "" { + s.Require().Error(err) + s.Require().ErrorContains(err, tc.expError) + } else { + s.Require().NoError(err) + // check that the escrowed coins are un-escrowed + escrowAddress := transfertypes.GetEscrowAddress( + transfertypes.PortID, + s.pathAToB.EndpointA.ClientID, + ) + escrowedCoins := evmApp.BankKeeper.GetAllBalances(ctx, escrowAddress) + s.Require().Equal(0, len(escrowedCoins)) + } + }) + } +} From c8947e9f279adce7f68b1dad45f8a6426abe5f9c Mon Sep 17 00:00:00 2001 From: zsystm Date: Wed, 2 Apr 2025 02:23:51 +0900 Subject: [PATCH 3/9] add test cases for ibc middleware v1 and post state check --- tests/ibc/ibc_middleware_test.go | 132 ++++++++++++++++++++++++++-- tests/ibc/v2_ibc_middleware_test.go | 32 +++++-- testutil/ibc.go | 17 ++++ 3 files changed, 166 insertions(+), 15 deletions(-) create mode 100644 testutil/ibc.go diff --git a/tests/ibc/ibc_middleware_test.go b/tests/ibc/ibc_middleware_test.go index 512b83809..8f72b3809 100644 --- a/tests/ibc/ibc_middleware_test.go +++ b/tests/ibc/ibc_middleware_test.go @@ -4,13 +4,20 @@ import ( "errors" "testing" + sdk "github.com/cosmos/cosmos-sdk/types" + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" ibctesting "github.com/cosmos/ibc-go/v10/testing" testifysuite "github.com/stretchr/testify/suite" + "github.com/cosmos/evm/evmd" "github.com/cosmos/evm/ibc" evmibctesting "github.com/cosmos/evm/ibc/testing" + "github.com/cosmos/evm/testutil" "github.com/cosmos/evm/x/erc20" erc20Keeper "github.com/cosmos/evm/x/erc20/keeper" + "github.com/cosmos/evm/x/erc20/types" ) // MiddlewareTestSuite tests the IBC middleware for the ERC20 module. @@ -23,19 +30,28 @@ type MiddlewareTestSuite struct { evmChainA *ibctesting.TestChain chainB *ibctesting.TestChain - // chainB to evmChainA for testing OnRecvPacket, OnAckPacket, and OnTimeoutPacket + pathAToB *ibctesting.Path pathBToA *ibctesting.Path } -func (suite *MiddlewareTestSuite) SetupTest() { - suite.coordinator = evmibctesting.NewCoordinator(suite.T(), 1, 2) - suite.evmChainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) - suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(2)) +func (s *MiddlewareTestSuite) SetupTest() { + s.coordinator = evmibctesting.NewCoordinator(s.T(), 1, 2) + s.evmChainA = s.coordinator.GetChain(ibctesting.GetChainID(1)) + s.chainB = s.coordinator.GetChain(ibctesting.GetChainID(2)) - // setup between chainB and evmChainA - // pathBToA.EndpointA = endpoint on chainB - // pathBToA.EndpointB = endpoint on evmChainA - suite.pathBToA = ibctesting.NewPath(suite.chainB, suite.evmChainA) + s.pathAToB = ibctesting.NewPath(s.evmChainA, s.chainB) + s.pathAToB.EndpointA.ChannelConfig.PortID = ibctesting.TransferPort + s.pathAToB.EndpointB.ChannelConfig.PortID = ibctesting.TransferPort + s.pathAToB.EndpointA.ChannelConfig.Version = transfertypes.V1 + s.pathAToB.EndpointB.ChannelConfig.Version = transfertypes.V1 + s.pathAToB.Setup() + + s.pathBToA = ibctesting.NewPath(s.chainB, s.evmChainA) + s.pathBToA.EndpointA.ChannelConfig.PortID = ibctesting.TransferPort + s.pathBToA.EndpointB.ChannelConfig.PortID = ibctesting.TransferPort + s.pathBToA.EndpointA.ChannelConfig.Version = transfertypes.V1 + s.pathBToA.EndpointB.ChannelConfig.Version = transfertypes.V1 + s.pathBToA.Setup() } func TestMiddlewareTestSuite(t *testing.T) { @@ -89,3 +105,101 @@ func (s *MiddlewareTestSuite) TestNewIBCMiddleware() { }) } } + +func (s *MiddlewareTestSuite) TestOnRecvPacket() { + var ( + ctx sdk.Context + packet channeltypes.Packet + ) + + testCases := []struct { + name string + malleate func() + expError string + }{ + { + name: "pass", + malleate: nil, + expError: "", + }, + { + name: "fail: malformed packet data", + malleate: func() { + packet.Data = []byte("malformed data") + }, + expError: "handling packet", + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + s.SetupTest() + + ctx = s.chainB.GetContext() + bondDenom, err := s.chainB.GetSimApp().StakingKeeper.BondDenom(ctx) + s.Require().NoError(err) + sendAmt := ibctesting.DefaultCoinAmount + receiver := s.evmChainA.SenderAccount.GetAddress() + packetData := transfertypes.NewFungibleTokenPacketData( + bondDenom, + sendAmt.String(), + s.chainB.SenderAccount.GetAddress().String(), + receiver.String(), + "", + ) + packet = channeltypes.Packet{ + Sequence: 1, + SourcePort: s.pathBToA.EndpointB.ChannelConfig.PortID, + SourceChannel: s.pathBToA.EndpointB.ChannelID, + DestinationPort: s.pathBToA.EndpointA.ChannelConfig.PortID, + DestinationChannel: s.pathBToA.EndpointA.ChannelID, + Data: packetData.GetBytes(), + TimeoutHeight: s.evmChainA.GetTimeoutHeight(), + TimeoutTimestamp: 0, + } + + if tc.malleate != nil { + tc.malleate() + } + + transferStack, ok := s.evmChainA.App.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) + s.Require().True(ok) + + ctx = s.evmChainA.GetContext() + sourceChan := s.pathBToA.EndpointB.GetChannel() + onRecvPacket := func() ibcexported.Acknowledgement { + return transferStack.OnRecvPacket( + ctx, + sourceChan.Version, + packet, + s.evmChainA.SenderAccount.GetAddress()) + } + + ack := onRecvPacket() + if tc.expError == "" { + s.Require().True(ack.Success()) + // make sure voucher coins are sent to the receiver + data, ackErr := transfertypes.UnmarshalPacketData(packetData.GetBytes(), sourceChan.Version, "") + s.Require().Nil(ackErr) + voucherDenom := testutil.GetVoucherDenomFromPacketData(data, packet.GetDestPort(), packet.GetDestChannel()) + evmApp := s.evmChainA.App.(*evmd.EVMD) + voucherCoin := evmApp.BankKeeper.GetBalance(ctx, receiver, voucherDenom) + s.Require().Equal(sendAmt.String(), voucherCoin.Amount.String()) + // make sure token pair is registered + tp, err := types.NewTokenPairSTRv2(voucherDenom) + s.Require().NoError(err) + tokenPair, found := evmApp.Erc20Keeper.GetTokenPair(ctx, tp.GetID()) + s.Require().True(found) + s.Require().Equal(voucherDenom, tokenPair.Denom) + } else { + s.Require().False(ack.Success()) + acknowledgement, ok := ack.(channeltypes.Acknowledgement) + s.Require().True(ok) + ackErr, ok := acknowledgement.Response.(*channeltypes.Acknowledgement_Error) + s.Require().True(ok) + s.Require().Contains(ackErr.Error, tc.expError) + } + }) + } +} diff --git a/tests/ibc/v2_ibc_middleware_test.go b/tests/ibc/v2_ibc_middleware_test.go index d2dcf3f07..ac0c83672 100644 --- a/tests/ibc/v2_ibc_middleware_test.go +++ b/tests/ibc/v2_ibc_middleware_test.go @@ -14,7 +14,9 @@ import ( "github.com/cosmos/evm/evmd" evmibctesting "github.com/cosmos/evm/ibc/testing" + "github.com/cosmos/evm/testutil" erc20Keeper "github.com/cosmos/evm/x/erc20/keeper" + "github.com/cosmos/evm/x/erc20/types" "github.com/cosmos/evm/x/erc20/v2" ) @@ -221,11 +223,13 @@ func (s *MiddlewareV2TestSuite) TestOnRecvPacket() { ctx = s.chainB.GetContext() bondDenom, err := s.chainB.GetSimApp().StakingKeeper.BondDenom(ctx) s.Require().NoError(err) + receiver := s.evmChainA.SenderAccount.GetAddress() + sendAmt := ibctesting.DefaultCoinAmount packetData = transfertypes.NewFungibleTokenPacketData( bondDenom, - ibctesting.DefaultCoinAmount.String(), + sendAmt.String(), s.chainB.SenderAccount.GetAddress().String(), - s.evmChainA.SenderAccount.GetAddress().String(), + receiver.String(), "", ) @@ -239,22 +243,38 @@ func (s *MiddlewareV2TestSuite) TestOnRecvPacket() { tc.malleate() } + evmApp := s.evmChainA.App.(*evmd.EVMD) // erc20 module is routed as top level middleware - erc20mod := s.evmChainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort) + transferStack := evmApp.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort) + sourceClient := s.pathBToA.EndpointB.ClientID onRecvPacket := func() channeltypesv2.RecvPacketResult { ctx = s.evmChainA.GetContext() - return erc20mod.OnRecvPacket( + return transferStack.OnRecvPacket( ctx, - s.pathBToA.EndpointB.ClientID, + sourceClient, s.pathBToA.EndpointA.ClientID, 1, payload, - s.evmChainA.SenderAccount.GetAddress(), + receiver, ) } recvResult := onRecvPacket() s.Require().Equal(tc.expResult, recvResult.Status) + if recvResult.Status == channeltypesv2.PacketStatus_Success { + // make sure voucher coins are sent to the receiver + data, ackErr := transfertypes.UnmarshalPacketData(packetData.GetBytes(), transfertypes.V1, "") + s.Require().Nil(ackErr) + voucherDenom := testutil.GetVoucherDenomFromPacketData(data, payload.GetSourcePort(), sourceClient) + voucherCoin := evmApp.BankKeeper.GetBalance(ctx, receiver, voucherDenom) + s.Require().Equal(sendAmt.String(), voucherCoin.Amount.String()) + // make sure token pair is registered + tp, err := types.NewTokenPairSTRv2(voucherDenom) + s.Require().NoError(err) + tokenPair, found := evmApp.Erc20Keeper.GetTokenPair(ctx, tp.GetID()) + s.Require().True(found) + s.Require().Equal(voucherDenom, tokenPair.Denom) + } }) } } diff --git a/testutil/ibc.go b/testutil/ibc.go new file mode 100644 index 000000000..dc5636133 --- /dev/null +++ b/testutil/ibc.go @@ -0,0 +1,17 @@ +package testutil + +import ( + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" +) + +func GetVoucherDenomFromPacketData( + data transfertypes.InternalTransferRepresentation, + destPort string, + destChannel string, +) string { + token := data.Token + trace := []transfertypes.Hop{transfertypes.NewHop(destPort, destChannel)} + token.Denom.Trace = append(trace, token.Denom.Trace...) + voucherDenom := token.Denom.IBCDenom() + return voucherDenom +} From 9663e4aa4b8b526f47c13aba843afb4e4fb6d361 Mon Sep 17 00:00:00 2001 From: zsystm Date: Wed, 2 Apr 2025 03:13:40 +0900 Subject: [PATCH 4/9] add OnAcknowledgementPacket tc for v1 ibc middleware --- tests/ibc/ibc_middleware_test.go | 128 +++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/tests/ibc/ibc_middleware_test.go b/tests/ibc/ibc_middleware_test.go index 8f72b3809..02451f0da 100644 --- a/tests/ibc/ibc_middleware_test.go +++ b/tests/ibc/ibc_middleware_test.go @@ -6,6 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" ibctesting "github.com/cosmos/ibc-go/v10/testing" @@ -203,3 +204,130 @@ func (s *MiddlewareTestSuite) TestOnRecvPacket() { }) } } + +func (s *MiddlewareTestSuite) TestOnAcknowledgementPacket() { + var ( + ctx sdk.Context + packet channeltypes.Packet + ack []byte + ) + + testCases := []struct { + name string + malleate func() + onSendRequired bool + expError string + }{ + { + name: "pass", + malleate: nil, + onSendRequired: false, + expError: "", + }, + { + name: "pass: refund escrowed token", + malleate: func() { + ackErr := channeltypes.NewErrorAcknowledgement(errors.New("error")) + ack = ackErr.Acknowledgement() + }, + onSendRequired: true, + expError: "", + }, + { + name: "fail: malformed packet data", + malleate: func() { + packet.Data = []byte("malformed data") + }, + onSendRequired: false, + expError: "cannot unmarshal ICS-20 transfer packet data", + }, + { + name: "fail: empty ack", + malleate: func() { + ack = []byte{} + }, + onSendRequired: false, + expError: "cannot unmarshal ICS-20 transfer packet acknowledgement", + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + s.SetupTest() + + ctx = s.evmChainA.GetContext() + evmApp := s.evmChainA.App.(*evmd.EVMD) + bondDenom, err := evmApp.StakingKeeper.BondDenom(ctx) + s.Require().NoError(err) + sendAmt := ibctesting.DefaultCoinAmount + sender := s.evmChainA.SenderAccount.GetAddress() + receiver := s.chainB.SenderAccount.GetAddress() + packetData := transfertypes.NewFungibleTokenPacketData( + bondDenom, + sendAmt.String(), + sender.String(), + receiver.String(), + "", + ) + path := s.pathAToB + packet = channeltypes.Packet{ + Sequence: 1, + SourcePort: path.EndpointA.ChannelConfig.PortID, + SourceChannel: path.EndpointA.ChannelID, + DestinationPort: path.EndpointB.ChannelConfig.PortID, + DestinationChannel: path.EndpointB.ChannelID, + Data: packetData.GetBytes(), + TimeoutHeight: s.chainB.GetTimeoutHeight(), + TimeoutTimestamp: 0, + } + + ack = channeltypes.NewResultAcknowledgement([]byte{1}).Acknowledgement() + + if tc.malleate != nil { + tc.malleate() + } + + transferStack, ok := evmApp.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) + s.Require().True(ok) + + sourceChan := s.pathAToB.EndpointA.GetChannel() + onAcknowledgementPacket := func() error { + return transferStack.OnAcknowledgementPacket( + ctx, + sourceChan.Version, + packet, + ack, + receiver, + ) + } + if tc.onSendRequired { + timeoutHeight := clienttypes.NewHeight(1, 110) + msg := transfertypes.NewMsgTransfer( + path.EndpointA.ChannelConfig.PortID, + path.EndpointA.ChannelID, + sdk.NewCoin(bondDenom, sendAmt), + sender.String(), + receiver.String(), + timeoutHeight, 0, "", + ) + res, err := s.evmChainA.SendMsgs(msg) + s.Require().NoError(err) // message committed + packet, err := ibctesting.ParsePacketFromEvents(res.Events) + s.Require().NoError(err) + + // relay send + err = path.RelayPacket(packet) + s.Require().NoError(err) // relay committed + } + + err = onAcknowledgementPacket() + if tc.expError == "" { + s.Require().NoError(err) + } else { + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.expError) + } + }) + } +} From 6da9ab38299e388009b9ebe10dc1eba1069ad415 Mon Sep 17 00:00:00 2001 From: zsystm Date: Wed, 2 Apr 2025 03:18:26 +0900 Subject: [PATCH 5/9] add OnTimeoutPacket tc for v1 ibc middleware --- tests/ibc/ibc_middleware_test.go | 107 +++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/tests/ibc/ibc_middleware_test.go b/tests/ibc/ibc_middleware_test.go index 02451f0da..5820d4a9c 100644 --- a/tests/ibc/ibc_middleware_test.go +++ b/tests/ibc/ibc_middleware_test.go @@ -331,3 +331,110 @@ func (s *MiddlewareTestSuite) TestOnAcknowledgementPacket() { }) } } + +func (s *MiddlewareTestSuite) TestOnTimeoutPacket() { + var ( + ctx sdk.Context + packet channeltypes.Packet + ) + + testCases := []struct { + name string + malleate func() + onSendRequired bool + expError string + }{ + { + name: "pass", + malleate: nil, + onSendRequired: true, + expError: "", + }, + { + name: "fail: malformed packet data", + malleate: func() { + packet.Data = []byte("malformed data") + }, + onSendRequired: false, + expError: "cannot unmarshal ICS-20 transfer packet data", + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + s.SetupTest() + + ctx = s.evmChainA.GetContext() + evmApp := s.evmChainA.App.(*evmd.EVMD) + bondDenom, err := evmApp.StakingKeeper.BondDenom(ctx) + s.Require().NoError(err) + sendAmt := ibctesting.DefaultCoinAmount + sender := s.evmChainA.SenderAccount.GetAddress() + receiver := s.chainB.SenderAccount.GetAddress() + packetData := transfertypes.NewFungibleTokenPacketData( + bondDenom, + sendAmt.String(), + sender.String(), + receiver.String(), + "", + ) + path := s.pathAToB + packet = channeltypes.Packet{ + Sequence: 1, + SourcePort: path.EndpointA.ChannelConfig.PortID, + SourceChannel: path.EndpointA.ChannelID, + DestinationPort: path.EndpointB.ChannelConfig.PortID, + DestinationChannel: path.EndpointB.ChannelID, + Data: packetData.GetBytes(), + TimeoutHeight: s.chainB.GetTimeoutHeight(), + TimeoutTimestamp: 0, + } + + if tc.malleate != nil { + tc.malleate() + } + + timeoutStack, ok := evmApp.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) + s.Require().True(ok) + + sourceChan := s.pathAToB.EndpointA.GetChannel() + onTimeoutPacket := func() error { + return timeoutStack.OnTimeoutPacket( + ctx, + sourceChan.Version, + packet, + sender, + ) + } + + if tc.onSendRequired { + timeoutHeight := clienttypes.NewHeight(1, 110) + msg := transfertypes.NewMsgTransfer( + path.EndpointA.ChannelConfig.PortID, + path.EndpointA.ChannelID, + sdk.NewCoin(bondDenom, sendAmt), + sender.String(), + receiver.String(), + timeoutHeight, 0, "", + ) + res, err := s.evmChainA.SendMsgs(msg) + s.Require().NoError(err) // message committed + packet, err := ibctesting.ParsePacketFromEvents(res.Events) + s.Require().NoError(err) + + // relay send + err = path.RelayPacket(packet) + s.Require().NoError(err) // relay committed + } + + err = onTimeoutPacket() + if tc.expError == "" { + s.Require().NoError(err) + } else { + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.expError) + } + }) + } +} From 5ac48ae3bd7d2f60a11f438ea129356866929c50 Mon Sep 17 00:00:00 2001 From: zsystm Date: Wed, 2 Apr 2025 03:26:19 +0900 Subject: [PATCH 6/9] chore: unify variable names --- tests/ibc/ibc_middleware_test.go | 4 ++-- tests/ibc/v2_ibc_middleware_test.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/ibc/ibc_middleware_test.go b/tests/ibc/ibc_middleware_test.go index 5820d4a9c..ec387cebd 100644 --- a/tests/ibc/ibc_middleware_test.go +++ b/tests/ibc/ibc_middleware_test.go @@ -395,12 +395,12 @@ func (s *MiddlewareTestSuite) TestOnTimeoutPacket() { tc.malleate() } - timeoutStack, ok := evmApp.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) + transferStack, ok := evmApp.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) s.Require().True(ok) sourceChan := s.pathAToB.EndpointA.GetChannel() onTimeoutPacket := func() error { - return timeoutStack.OnTimeoutPacket( + return transferStack.OnTimeoutPacket( ctx, sourceChan.Version, packet, diff --git a/tests/ibc/v2_ibc_middleware_test.go b/tests/ibc/v2_ibc_middleware_test.go index ac0c83672..6144b60eb 100644 --- a/tests/ibc/v2_ibc_middleware_test.go +++ b/tests/ibc/v2_ibc_middleware_test.go @@ -363,9 +363,9 @@ func (s *MiddlewareV2TestSuite) TestOnAcknowledgementPacket() { } // erc20 module is routed as top level middleware - erc20mod := s.evmChainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort) + transferStack := s.evmChainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort) if tc.onSendRequired { - s.NoError(erc20mod.OnSendPacket( + s.NoError(transferStack.OnSendPacket( ctx, s.pathAToB.EndpointA.ClientID, s.pathAToB.EndpointB.ClientID, @@ -375,7 +375,7 @@ func (s *MiddlewareV2TestSuite) TestOnAcknowledgementPacket() { )) } onAckPacket := func() error { - return erc20mod.OnAcknowledgementPacket( + return transferStack.OnAcknowledgementPacket( ctx, s.pathAToB.EndpointA.ClientID, s.pathAToB.EndpointB.ClientID, @@ -452,9 +452,9 @@ func (s *MiddlewareV2TestSuite) TestOnTimeoutPacket() { tc.malleate() } - erc20mod := s.evmChainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort) + transferStack := s.evmChainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort) if tc.onSendRequired { - s.NoError(erc20mod.OnSendPacket( + s.NoError(transferStack.OnSendPacket( ctx, s.pathAToB.EndpointA.ClientID, s.pathAToB.EndpointB.ClientID, @@ -465,7 +465,7 @@ func (s *MiddlewareV2TestSuite) TestOnTimeoutPacket() { } onTimeoutPacket := func() error { - return erc20mod.OnTimeoutPacket( + return transferStack.OnTimeoutPacket( ctx, s.pathAToB.EndpointA.ClientID, s.pathAToB.EndpointB.ClientID, From 638ae17bdfc93b5b6e262f472ce20461df54d5da Mon Sep 17 00:00:00 2001 From: zsystm Date: Thu, 3 Apr 2025 13:41:10 +0900 Subject: [PATCH 7/9] use internal testing pkg and add TestOnRecvPacketNativeErc20 tc --- tests/ibc/ibc_middleware_test.go | 142 +++++++++++++++++++++++++--- tests/ibc/v2_ibc_middleware_test.go | 18 ++-- 2 files changed, 139 insertions(+), 21 deletions(-) diff --git a/tests/ibc/ibc_middleware_test.go b/tests/ibc/ibc_middleware_test.go index ec387cebd..54108b042 100644 --- a/tests/ibc/ibc_middleware_test.go +++ b/tests/ibc/ibc_middleware_test.go @@ -2,16 +2,22 @@ package ibc import ( "errors" + "math/big" "testing" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" ibctesting "github.com/cosmos/ibc-go/v10/testing" + "github.com/ethereum/go-ethereum/common" testifysuite "github.com/stretchr/testify/suite" + "github.com/cosmos/evm/contracts" "github.com/cosmos/evm/evmd" "github.com/cosmos/evm/ibc" evmibctesting "github.com/cosmos/evm/ibc/testing" @@ -19,20 +25,21 @@ import ( "github.com/cosmos/evm/x/erc20" erc20Keeper "github.com/cosmos/evm/x/erc20/keeper" "github.com/cosmos/evm/x/erc20/types" + erc20types "github.com/cosmos/evm/x/erc20/types" ) // MiddlewareTestSuite tests the IBC middleware for the ERC20 module. type MiddlewareTestSuite struct { testifysuite.Suite - coordinator *ibctesting.Coordinator + coordinator *evmibctesting.Coordinator // testing chains used for convenience and readability - evmChainA *ibctesting.TestChain - chainB *ibctesting.TestChain + evmChainA *evmibctesting.TestChain + chainB *evmibctesting.TestChain - pathAToB *ibctesting.Path - pathBToA *ibctesting.Path + pathAToB *evmibctesting.Path + pathBToA *evmibctesting.Path } func (s *MiddlewareTestSuite) SetupTest() { @@ -40,14 +47,14 @@ func (s *MiddlewareTestSuite) SetupTest() { s.evmChainA = s.coordinator.GetChain(ibctesting.GetChainID(1)) s.chainB = s.coordinator.GetChain(ibctesting.GetChainID(2)) - s.pathAToB = ibctesting.NewPath(s.evmChainA, s.chainB) + s.pathAToB = evmibctesting.NewPath(s.evmChainA, s.chainB) s.pathAToB.EndpointA.ChannelConfig.PortID = ibctesting.TransferPort s.pathAToB.EndpointB.ChannelConfig.PortID = ibctesting.TransferPort s.pathAToB.EndpointA.ChannelConfig.Version = transfertypes.V1 s.pathAToB.EndpointB.ChannelConfig.Version = transfertypes.V1 s.pathAToB.Setup() - s.pathBToA = ibctesting.NewPath(s.chainB, s.evmChainA) + s.pathBToA = evmibctesting.NewPath(s.chainB, s.evmChainA) s.pathBToA.EndpointA.ChannelConfig.PortID = ibctesting.TransferPort s.pathBToA.EndpointB.ChannelConfig.PortID = ibctesting.TransferPort s.pathBToA.EndpointA.ChannelConfig.Version = transfertypes.V1 @@ -149,12 +156,13 @@ func (s *MiddlewareTestSuite) TestOnRecvPacket() { receiver.String(), "", ) + path := s.pathAToB packet = channeltypes.Packet{ Sequence: 1, - SourcePort: s.pathBToA.EndpointB.ChannelConfig.PortID, - SourceChannel: s.pathBToA.EndpointB.ChannelID, - DestinationPort: s.pathBToA.EndpointA.ChannelConfig.PortID, - DestinationChannel: s.pathBToA.EndpointA.ChannelID, + SourcePort: path.EndpointB.ChannelConfig.PortID, + SourceChannel: path.EndpointB.ChannelID, + DestinationPort: path.EndpointA.ChannelConfig.PortID, + DestinationChannel: path.EndpointA.ChannelID, Data: packetData.GetBytes(), TimeoutHeight: s.evmChainA.GetTimeoutHeight(), TimeoutTimestamp: 0, @@ -168,7 +176,7 @@ func (s *MiddlewareTestSuite) TestOnRecvPacket() { s.Require().True(ok) ctx = s.evmChainA.GetContext() - sourceChan := s.pathBToA.EndpointB.GetChannel() + sourceChan := path.EndpointB.GetChannel() onRecvPacket := func() ibcexported.Acknowledgement { return transferStack.OnRecvPacket( ctx, @@ -205,6 +213,116 @@ func (s *MiddlewareTestSuite) TestOnRecvPacket() { } } +// TestOnRecvPacketNativeErc20 tests the OnRecvPacket method for native ERC20 tokens. +func (s *MiddlewareTestSuite) TestOnRecvPacketNativeErc20() { + s.SetupTest() + evmCtx := s.evmChainA.GetContext() + evmApp := s.evmChainA.App.(*evmd.EVMD) + + // Scenario: Native ERC20 token transfer from evmChainA to chainB + deployedContractAddr, err := evmApp.Erc20Keeper.DeployERC20Contract(evmCtx, banktypes.Metadata{ + DenomUnits: []*banktypes.DenomUnit{ + { + Denom: "example", + Exponent: 18, + }, + }, + Name: "Example", + Symbol: "Ex", + }) + s.Require().NoError(err) + s.evmChainA.NextBlock() + // Create a new token pair for the deployed contract. + _, err = evmApp.Erc20Keeper.RegisterERC20( + evmCtx, + &erc20types.MsgRegisterERC20{ + Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), + Erc20Addresses: []string{deployedContractAddr.Hex()}, + }, + ) + s.Require().NoError(err) + + // evmChainA sender must have `sendAmt` erc20 tokens before sending the token to chainB via IBC. + abi := contracts.ERC20MinterBurnerDecimalsContract.ABI + nativeErc20Denom := types.CreateDenom(deployedContractAddr.String()) + sendAmt := evmibctesting.DefaultCoinAmount + evmChainAAccount := s.evmChainA.SenderAccount.GetAddress() + // mint native erc20 + _, err = evmApp.EVMKeeper.CallEVM( + evmCtx, abi, erc20types.ModuleAddress, deployedContractAddr, + true, "mint", common.BytesToAddress(evmChainAAccount), big.NewInt(sendAmt.Int64()), + ) + s.Require().NoError(err) + erc20Bal := evmApp.Erc20Keeper.BalanceOf(evmCtx, abi, deployedContractAddr, common.BytesToAddress(evmChainAAccount)) + s.Require().Equal(sendAmt.String(), erc20Bal.String(), "erc20 balance should be equal to minted sendAmt") + + timeoutHeight := clienttypes.NewHeight(1, 110) + path := s.pathAToB + chainBAccount := s.chainB.SenderAccount.GetAddress() + // IBC send from evmChainA to chainB. + msg := transfertypes.NewMsgTransfer( + path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, + sdk.NewCoin(nativeErc20Denom, sendAmt), evmChainAAccount.String(), chainBAccount.String(), + timeoutHeight, 0, "", + ) + _, err = s.evmChainA.SendMsgs(msg) + s.Require().NoError(err) // message committed + erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, abi, deployedContractAddr, common.BytesToAddress(evmChainAAccount)) + s.Require().Equal(erc20BalAfterIbcTransfer.String(), new(big.Int).Sub(erc20Bal, sendAmt.BigInt()).String(), "erc20 balance should be equal to minted sendAmt - sendAmt after IBC transfer") + // Check native erc20 token is escrowed on evmChainA for sending to chainB. + escrowAddr := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) + escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20Denom) + s.Require().Equal(escrowedBal.Amount.String(), sendAmt.String(), "escrowed balance should be equal to sendAmt after IBC transfer") + + // chainBNativeErc20Denom is the native erc20 token denom on chainB from evmChainA through IBC. + chainBNativeErc20Denom := transfertypes.NewDenom( + nativeErc20Denom, + transfertypes.NewHop( + s.pathAToB.EndpointB.ChannelConfig.PortID, + s.pathAToB.EndpointB.ChannelID, + ), + ) + // Mock the transfer of received native erc20 token by evmChainA to evmChainA. + // Note that ChainB didn't receive the native erc20 token. We just assume that. + packetData := transfertypes.NewFungibleTokenPacketData( + chainBNativeErc20Denom.Path(), + sendAmt.String(), + chainBAccount.String(), + evmChainAAccount.String(), + "", + ) + packet := channeltypes.Packet{ + Sequence: 1, + SourcePort: path.EndpointB.ChannelConfig.PortID, + SourceChannel: path.EndpointB.ChannelID, + DestinationPort: path.EndpointA.ChannelConfig.PortID, + DestinationChannel: path.EndpointA.ChannelID, + Data: packetData.GetBytes(), + TimeoutHeight: s.evmChainA.GetTimeoutHeight(), + TimeoutTimestamp: 0, + } + + transferStack, ok := s.evmChainA.App.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) + s.Require().True(ok) + + sourceChan := path.EndpointB.GetChannel() + onRecvPacket := func() ibcexported.Acknowledgement { + return transferStack.OnRecvPacket( + evmCtx, + sourceChan.Version, + packet, + s.evmChainA.SenderAccount.GetAddress()) + } + + ack := onRecvPacket() + s.Require().True(ack.Success()) + // make sure voucher coins are sent to the sender + _, ackErr := transfertypes.UnmarshalPacketData(packetData.GetBytes(), sourceChan.Version, "") + s.Require().Nil(ackErr) + escrowedBal = evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20Denom) + s.Require().True(escrowedBal.IsZero(), "escrowed balance should be un-escrowed after receiving the packet") +} + func (s *MiddlewareTestSuite) TestOnAcknowledgementPacket() { var ( ctx sdk.Context diff --git a/tests/ibc/v2_ibc_middleware_test.go b/tests/ibc/v2_ibc_middleware_test.go index 6144b60eb..420b71749 100644 --- a/tests/ibc/v2_ibc_middleware_test.go +++ b/tests/ibc/v2_ibc_middleware_test.go @@ -24,31 +24,31 @@ import ( type MiddlewareV2TestSuite struct { testifysuite.Suite - coordinator *ibctesting.Coordinator + coordinator *evmibctesting.Coordinator // testing chains used for convenience and readability - evmChainA *ibctesting.TestChain - chainB *ibctesting.TestChain + evmChainA *evmibctesting.TestChain + chainB *evmibctesting.TestChain // evmChainA to chainB for testing OnSendPacket, OnAckPacket, and OnTimeoutPacket - pathAToB *ibctesting.Path + pathAToB *evmibctesting.Path // chainB to evmChainA for testing OnRecvPacket - pathBToA *ibctesting.Path + pathBToA *evmibctesting.Path } func (suite *MiddlewareV2TestSuite) SetupTest() { suite.coordinator = evmibctesting.NewCoordinator(suite.T(), 1, 1) - suite.evmChainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) - suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(2)) + suite.evmChainA = suite.coordinator.GetChain(evmibctesting.GetChainID(1)) + suite.chainB = suite.coordinator.GetChain(evmibctesting.GetChainID(2)) // setup between evmChainA and chainB // pathAToB.EndpointA = endpoint on evmChainA // pathAToB.EndpointB = endpoint on chainB - suite.pathAToB = ibctesting.NewPath(suite.evmChainA, suite.chainB) + suite.pathAToB = evmibctesting.NewPath(suite.evmChainA, suite.chainB) // setup between chainB and evmChainA // pathBToA.EndpointA = endpoint on chainB // pathBToA.EndpointB = endpoint on evmChainA - suite.pathBToA = ibctesting.NewPath(suite.chainB, suite.evmChainA) + suite.pathBToA = evmibctesting.NewPath(suite.chainB, suite.evmChainA) // setup IBC v2 paths between the chains suite.pathAToB.SetupV2() From 463c4ebc648e211cc3f07755e2f3c36335e36646 Mon Sep 17 00:00:00 2001 From: zsystm Date: Thu, 3 Apr 2025 14:22:35 +0900 Subject: [PATCH 8/9] add v1 tcs for handling erc20 native coin OnTimeoutPacket, OnAcknowledgementPacket --- tests/ibc/ibc_middleware_test.go | 395 +++++++++++++++++++++++++++---- 1 file changed, 353 insertions(+), 42 deletions(-) diff --git a/tests/ibc/ibc_middleware_test.go b/tests/ibc/ibc_middleware_test.go index 54108b042..4c4d64603 100644 --- a/tests/ibc/ibc_middleware_test.go +++ b/tests/ibc/ibc_middleware_test.go @@ -5,6 +5,7 @@ import ( "math/big" "testing" + "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" @@ -14,6 +15,7 @@ import ( channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" ibctesting "github.com/cosmos/ibc-go/v10/testing" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" testifysuite "github.com/stretchr/testify/suite" @@ -42,6 +44,14 @@ type MiddlewareTestSuite struct { pathBToA *evmibctesting.Path } +type NativeErc20Info struct { + denom string + contractAbi abi.ABI + contractAddr common.Address + account common.Address + initialBal *big.Int +} + func (s *MiddlewareTestSuite) SetupTest() { s.coordinator = evmibctesting.NewCoordinator(s.T(), 1, 2) s.evmChainA = s.coordinator.GetChain(ibctesting.GetChainID(1)) @@ -62,6 +72,57 @@ func (s *MiddlewareTestSuite) SetupTest() { s.pathBToA.Setup() } +// SetupNativeErc20 sets up a native ERC20 token on evmChainA and registers it with the ERC20 module. +func (s *MiddlewareTestSuite) SetupNativeErc20() *NativeErc20Info { + evmCtx := s.evmChainA.GetContext() + evmApp := s.evmChainA.App.(*evmd.EVMD) + + // Scenario: Native ERC20 token transfer from evmChainA to chainB + deployedContractAddr, err := evmApp.Erc20Keeper.DeployERC20Contract(evmCtx, banktypes.Metadata{ + DenomUnits: []*banktypes.DenomUnit{ + { + Denom: "example", + Exponent: 18, + }, + }, + Name: "Example", + Symbol: "Ex", + }) + s.Require().NoError(err) + s.evmChainA.NextBlock() + // Create a new token pair for the deployed contract. + _, err = evmApp.Erc20Keeper.RegisterERC20( + evmCtx, + &erc20types.MsgRegisterERC20{ + Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), + Erc20Addresses: []string{deployedContractAddr.Hex()}, + }, + ) + s.Require().NoError(err) + + // evmChainA sender must have `sendAmt` erc20 tokens before sending the token to chainB via IBC. + contractAbi := contracts.ERC20MinterBurnerDecimalsContract.ABI + nativeErc20Denom := types.CreateDenom(deployedContractAddr.String()) + sendAmt := evmibctesting.DefaultCoinAmount + evmChainAAccount := s.evmChainA.SenderAccount.GetAddress() + // mint native erc20 + _, err = evmApp.EVMKeeper.CallEVM( + evmCtx, contractAbi, erc20types.ModuleAddress, deployedContractAddr, + true, "mint", common.BytesToAddress(evmChainAAccount), big.NewInt(sendAmt.Int64()), + ) + s.Require().NoError(err) + erc20Bal := evmApp.Erc20Keeper.BalanceOf(evmCtx, contractAbi, deployedContractAddr, common.BytesToAddress(evmChainAAccount)) + s.Require().Equal(sendAmt.String(), erc20Bal.String(), "erc20 balance should be equal to minted sendAmt") + + return &NativeErc20Info{ + denom: nativeErc20Denom, + contractAbi: contractAbi, + contractAddr: deployedContractAddr, + account: common.BytesToAddress(evmChainAAccount), + initialBal: sendAmt.BigInt(), + } +} + func TestMiddlewareTestSuite(t *testing.T) { testifysuite.Run(t, new(MiddlewareTestSuite)) } @@ -216,79 +277,51 @@ func (s *MiddlewareTestSuite) TestOnRecvPacket() { // TestOnRecvPacketNativeErc20 tests the OnRecvPacket method for native ERC20 tokens. func (s *MiddlewareTestSuite) TestOnRecvPacketNativeErc20() { s.SetupTest() + nativeErc20 := s.SetupNativeErc20() + evmCtx := s.evmChainA.GetContext() evmApp := s.evmChainA.App.(*evmd.EVMD) // Scenario: Native ERC20 token transfer from evmChainA to chainB - deployedContractAddr, err := evmApp.Erc20Keeper.DeployERC20Contract(evmCtx, banktypes.Metadata{ - DenomUnits: []*banktypes.DenomUnit{ - { - Denom: "example", - Exponent: 18, - }, - }, - Name: "Example", - Symbol: "Ex", - }) - s.Require().NoError(err) - s.evmChainA.NextBlock() - // Create a new token pair for the deployed contract. - _, err = evmApp.Erc20Keeper.RegisterERC20( - evmCtx, - &erc20types.MsgRegisterERC20{ - Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), - Erc20Addresses: []string{deployedContractAddr.Hex()}, - }, - ) - s.Require().NoError(err) - // evmChainA sender must have `sendAmt` erc20 tokens before sending the token to chainB via IBC. - abi := contracts.ERC20MinterBurnerDecimalsContract.ABI - nativeErc20Denom := types.CreateDenom(deployedContractAddr.String()) - sendAmt := evmibctesting.DefaultCoinAmount - evmChainAAccount := s.evmChainA.SenderAccount.GetAddress() - // mint native erc20 - _, err = evmApp.EVMKeeper.CallEVM( - evmCtx, abi, erc20types.ModuleAddress, deployedContractAddr, - true, "mint", common.BytesToAddress(evmChainAAccount), big.NewInt(sendAmt.Int64()), - ) - s.Require().NoError(err) - erc20Bal := evmApp.Erc20Keeper.BalanceOf(evmCtx, abi, deployedContractAddr, common.BytesToAddress(evmChainAAccount)) - s.Require().Equal(sendAmt.String(), erc20Bal.String(), "erc20 balance should be equal to minted sendAmt") timeoutHeight := clienttypes.NewHeight(1, 110) path := s.pathAToB chainBAccount := s.chainB.SenderAccount.GetAddress() // IBC send from evmChainA to chainB. + sendAmt := math.NewIntFromBigInt(nativeErc20.initialBal) + senderEthAddr := nativeErc20.account + sender := sdk.AccAddress(senderEthAddr.Bytes()) msg := transfertypes.NewMsgTransfer( path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, - sdk.NewCoin(nativeErc20Denom, sendAmt), evmChainAAccount.String(), chainBAccount.String(), + sdk.NewCoin(nativeErc20.denom, sendAmt), sender.String(), chainBAccount.String(), timeoutHeight, 0, "", ) - _, err = s.evmChainA.SendMsgs(msg) + _, err := s.evmChainA.SendMsgs(msg) s.Require().NoError(err) // message committed - erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, abi, deployedContractAddr, common.BytesToAddress(evmChainAAccount)) - s.Require().Equal(erc20BalAfterIbcTransfer.String(), new(big.Int).Sub(erc20Bal, sendAmt.BigInt()).String(), "erc20 balance should be equal to minted sendAmt - sendAmt after IBC transfer") + erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.contractAbi, nativeErc20.contractAddr, senderEthAddr) + s.Require().Equal(erc20BalAfterIbcTransfer.String(), new(big.Int).Sub(nativeErc20.initialBal, sendAmt.BigInt()).String(), "erc20 balance should be equal to minted sendAmt - sendAmt after IBC transfer") // Check native erc20 token is escrowed on evmChainA for sending to chainB. escrowAddr := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) - escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20Denom) + escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.denom) s.Require().Equal(escrowedBal.Amount.String(), sendAmt.String(), "escrowed balance should be equal to sendAmt after IBC transfer") // chainBNativeErc20Denom is the native erc20 token denom on chainB from evmChainA through IBC. chainBNativeErc20Denom := transfertypes.NewDenom( - nativeErc20Denom, + nativeErc20.denom, transfertypes.NewHop( s.pathAToB.EndpointB.ChannelConfig.PortID, s.pathAToB.EndpointB.ChannelID, ), ) + receiver := sender // the receiver is the sender on evmChainA // Mock the transfer of received native erc20 token by evmChainA to evmChainA. // Note that ChainB didn't receive the native erc20 token. We just assume that. packetData := transfertypes.NewFungibleTokenPacketData( chainBNativeErc20Denom.Path(), sendAmt.String(), chainBAccount.String(), - evmChainAAccount.String(), + receiver.String(), "", ) packet := channeltypes.Packet{ @@ -319,7 +352,7 @@ func (s *MiddlewareTestSuite) TestOnRecvPacketNativeErc20() { // make sure voucher coins are sent to the sender _, ackErr := transfertypes.UnmarshalPacketData(packetData.GetBytes(), sourceChan.Version, "") s.Require().Nil(ackErr) - escrowedBal = evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20Denom) + escrowedBal = evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.denom) s.Require().True(escrowedBal.IsZero(), "escrowed balance should be un-escrowed after receiving the packet") } @@ -450,6 +483,155 @@ func (s *MiddlewareTestSuite) TestOnAcknowledgementPacket() { } } +// TestOnAcknowledgementPacketNativeErc20 tests the OnAcknowledgementPacket method for native ERC20 tokens. +func (s *MiddlewareTestSuite) TestOnAcknowledgementPacketNativeErc20() { + var ( + packet channeltypes.Packet + ack []byte + ) + + testCases := []struct { + name string + malleate func() + expError string + expRefund bool + }{ + { + name: "pass", + malleate: nil, + expError: "", + expRefund: false, + }, + { + name: "pass: refund escrowed token", + malleate: func() { + ackErr := channeltypes.NewErrorAcknowledgement(errors.New("error")) + ack = ackErr.Acknowledgement() + }, + expError: "", + expRefund: true, + }, + { + name: "fail: malformed packet data", + malleate: func() { + packet.Data = []byte("malformed data") + }, + expError: "cannot unmarshal ICS-20 transfer packet data", + expRefund: false, + }, + { + name: "fail: empty ack", + malleate: func() { + ack = []byte{} + }, + expError: "cannot unmarshal ICS-20 transfer packet acknowledgement", + expRefund: false, + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + s.SetupTest() + nativeErc20 := s.SetupNativeErc20() + + evmCtx := s.evmChainA.GetContext() + evmApp := s.evmChainA.App.(*evmd.EVMD) + + // Scenario: Native ERC20 token transfer from evmChainA to chainB + // evmChainA sender must have `sendAmt` erc20 tokens before sending the token to chainB via IBC. + + timeoutHeight := clienttypes.NewHeight(1, 110) + path := s.pathAToB + chainBAccount := s.chainB.SenderAccount.GetAddress() + // IBC send from evmChainA to chainB. + sendAmt := math.NewIntFromBigInt(nativeErc20.initialBal) + senderEthAddr := nativeErc20.account + sender := sdk.AccAddress(senderEthAddr.Bytes()) + receiver := s.chainB.SenderAccount.GetAddress() + msg := transfertypes.NewMsgTransfer( + path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, + sdk.NewCoin(nativeErc20.denom, sendAmt), sender.String(), receiver.String(), + timeoutHeight, 0, "", + ) + // escrowCheck is a function that checks the state of the evmChainA after the IBC transfer. + // sendAmt should be escrowed on evmChainA for sending to chainB. + // eerc20 balance should be decreased by sendAmt. + escrowCheck := func() { + erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.contractAbi, nativeErc20.contractAddr, senderEthAddr) + s.Require().Equal(erc20BalAfterIbcTransfer.String(), new(big.Int).Sub(nativeErc20.initialBal, sendAmt.BigInt()).String(), "erc20 balance should be equal to minted sendAmt - sendAmt after IBC transfer") + // Check native erc20 token is escrowed on evmChainA for sending to chainB. + escrowAddr := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) + escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.denom) + s.Require().Equal(escrowedBal.Amount.String(), sendAmt.String(), "escrowed balance should be equal to sendAmt after IBC transfer") + } + refundCheck := func() { + // Check native erc20 token is un-escrowed on evmChainA after refund. + escrowAddr := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) + escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.denom) + s.Require().True(escrowedBal.IsZero(), "escrowed balance should be un-escrowed after receiving the packet") + + // Check erc20 balance is same as initial balance after refund. + erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.contractAbi, nativeErc20.contractAddr, senderEthAddr) + s.Require().Equal(erc20BalAfterIbcTransfer.String(), nativeErc20.initialBal.String(), "erc20 balance should be equal to initial balance after refund") + } + _, err := s.evmChainA.SendMsgs(msg) + s.Require().NoError(err) // message committed + escrowCheck() + + transferStack, ok := s.evmChainA.App.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) + s.Require().True(ok) + + packetData := transfertypes.NewFungibleTokenPacketData( + nativeErc20.denom, + sendAmt.String(), + sender.String(), + chainBAccount.String(), + "", + ) + packet = channeltypes.Packet{ + Sequence: 1, + SourcePort: path.EndpointA.ChannelConfig.PortID, + SourceChannel: path.EndpointA.ChannelID, + DestinationPort: path.EndpointB.ChannelConfig.PortID, + DestinationChannel: path.EndpointB.ChannelID, + Data: packetData.GetBytes(), + TimeoutHeight: s.chainB.GetTimeoutHeight(), + TimeoutTimestamp: 0, + } + ack = channeltypes.NewResultAcknowledgement([]byte{1}).Acknowledgement() + + if tc.malleate != nil { + tc.malleate() + } + + sourceChan := path.EndpointA.GetChannel() + onAcknowledgementPacket := func() error { + return transferStack.OnAcknowledgementPacket( + evmCtx, + sourceChan.Version, + packet, + ack, + receiver, + ) + } + + err = onAcknowledgementPacket() + if tc.expError == "" { + s.Require().NoError(err) + } else { + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.expError) + } + if tc.expRefund { + refundCheck() + } else { + escrowCheck() + } + }) + } +} + func (s *MiddlewareTestSuite) TestOnTimeoutPacket() { var ( ctx sdk.Context @@ -556,3 +738,132 @@ func (s *MiddlewareTestSuite) TestOnTimeoutPacket() { }) } } + +// TestOnTimeoutPacketNativeErc20 tests the OnTimeoutPacket method for native ERC20 tokens. +func (s *MiddlewareTestSuite) TestOnTimeoutPacketNativeErc20() { + var ( + packet channeltypes.Packet + ) + + testCases := []struct { + name string + malleate func() + expError string + expRefund bool + }{ + { + name: "pass: refund escrowed native erc20 coin", + malleate: nil, + expError: "", + expRefund: true, + }, + { + name: "fail: malformed packet data", + malleate: func() { + packet.Data = []byte("malformed data") + }, + expError: "cannot unmarshal ICS-20 transfer packet data", + expRefund: false, + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + s.SetupTest() + nativeErc20 := s.SetupNativeErc20() + + evmCtx := s.evmChainA.GetContext() + evmApp := s.evmChainA.App.(*evmd.EVMD) + + // Scenario: Native ERC20 token transfer from evmChainA to chainB + // evmChainA sender must have `sendAmt` erc20 tokens before sending the token to chainB via IBC. + + timeoutHeight := clienttypes.NewHeight(1, 110) + path := s.pathAToB + chainBAccount := s.chainB.SenderAccount.GetAddress() + // IBC send from evmChainA to chainB. + sendAmt := math.NewIntFromBigInt(nativeErc20.initialBal) + senderEthAddr := nativeErc20.account + sender := sdk.AccAddress(senderEthAddr.Bytes()) + receiver := s.chainB.SenderAccount.GetAddress() + msg := transfertypes.NewMsgTransfer( + path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, + sdk.NewCoin(nativeErc20.denom, sendAmt), sender.String(), receiver.String(), + timeoutHeight, 0, "", + ) + // escrowCheck is a function that checks the state of the evmChainA after the IBC transfer. + // sendAmt should be escrowed on evmChainA for sending to chainB. + // eerc20 balance should be decreased by sendAmt. + escrowCheck := func() { + erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.contractAbi, nativeErc20.contractAddr, senderEthAddr) + s.Require().Equal(erc20BalAfterIbcTransfer.String(), new(big.Int).Sub(nativeErc20.initialBal, sendAmt.BigInt()).String(), "erc20 balance should be equal to minted sendAmt - sendAmt after IBC transfer") + // Check native erc20 token is escrowed on evmChainA for sending to chainB. + escrowAddr := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) + escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.denom) + s.Require().Equal(escrowedBal.Amount.String(), sendAmt.String(), "escrowed balance should be equal to sendAmt after IBC transfer") + } + refundCheck := func() { + // Check native erc20 token is un-escrowed on evmChainA after refund. + escrowAddr := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) + escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.denom) + s.Require().True(escrowedBal.IsZero(), "escrowed balance should be un-escrowed after receiving the packet") + + // Check erc20 balance is same as initial balance after refund. + erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.contractAbi, nativeErc20.contractAddr, senderEthAddr) + s.Require().Equal(erc20BalAfterIbcTransfer.String(), nativeErc20.initialBal.String(), "erc20 balance should be equal to initial balance after refund") + } + _, err := s.evmChainA.SendMsgs(msg) + s.Require().NoError(err) // message committed + escrowCheck() + + transferStack, ok := s.evmChainA.App.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) + s.Require().True(ok) + + packetData := transfertypes.NewFungibleTokenPacketData( + nativeErc20.denom, + sendAmt.String(), + sender.String(), + chainBAccount.String(), + "", + ) + packet = channeltypes.Packet{ + Sequence: 1, + SourcePort: path.EndpointA.ChannelConfig.PortID, + SourceChannel: path.EndpointA.ChannelID, + DestinationPort: path.EndpointB.ChannelConfig.PortID, + DestinationChannel: path.EndpointB.ChannelID, + Data: packetData.GetBytes(), + TimeoutHeight: s.chainB.GetTimeoutHeight(), + TimeoutTimestamp: 0, + } + + if tc.malleate != nil { + tc.malleate() + } + + sourceChan := path.EndpointA.GetChannel() + onTimeoutPacket := func() error { + return transferStack.OnTimeoutPacket( + evmCtx, + sourceChan.Version, + packet, + receiver, + ) + } + + err = onTimeoutPacket() + if tc.expError == "" { + s.Require().NoError(err) + } else { + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.expError) + } + if tc.expRefund { + refundCheck() + } else { + escrowCheck() + } + }) + } +} From fb1aeae3ad4e55e7519d747454524201ed7bc91c Mon Sep 17 00:00:00 2001 From: zsystm Date: Thu, 3 Apr 2025 15:29:10 +0900 Subject: [PATCH 9/9] refactor v1 middleware test codes --- tests/ibc/helper.go | 95 +++++++++ tests/ibc/ibc_middleware_test.go | 346 +++++++++++++------------------ 2 files changed, 244 insertions(+), 197 deletions(-) create mode 100644 tests/ibc/helper.go diff --git a/tests/ibc/helper.go b/tests/ibc/helper.go new file mode 100644 index 000000000..34029eda4 --- /dev/null +++ b/tests/ibc/helper.go @@ -0,0 +1,95 @@ +package ibc + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + ibctesting "github.com/cosmos/ibc-go/v10/testing" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + "github.com/cosmos/evm/contracts" + "github.com/cosmos/evm/evmd" + evmibctesting "github.com/cosmos/evm/ibc/testing" + erc20types "github.com/cosmos/evm/x/erc20/types" +) + +// NativeErc20Info holds details about a deployed ERC20 token. +type NativeErc20Info struct { + Denom string + ContractAbi abi.ABI + ContractAddr common.Address + Account common.Address // The address of the minter on the EVM chain + InitialBal *big.Int +} + +// SetupNativeErc20 deploys, registers, and mints a native ERC20 token on an EVM-based chain. +// Similar to what you used in your original ICS-20 tests, but extracted to a common helper. +func SetupNativeErc20(t *testing.T, chain *evmibctesting.TestChain) *NativeErc20Info { + t.Helper() + + evmCtx := chain.GetContext() + evmApp := chain.App.(*evmd.EVMD) + + // Deploy new ERC20 contract with default metadata + contractAddr, err := evmApp.Erc20Keeper.DeployERC20Contract(evmCtx, banktypes.Metadata{ + DenomUnits: []*banktypes.DenomUnit{ + {Denom: "example", Exponent: 18}, + }, + Name: "Example", + Symbol: "Ex", + }) + if err != nil { + t.Fatalf("ERC20 deployment failed: %v", err) + } + chain.NextBlock() + + // Register the contract + _, err = evmApp.Erc20Keeper.RegisterERC20(evmCtx, &erc20types.MsgRegisterERC20{ + Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), + Erc20Addresses: []string{contractAddr.Hex()}, + }) + if err != nil { + t.Fatalf("RegisterERC20 failed: %v", err) + } + + // Mint tokens to default sender + contractAbi := contracts.ERC20MinterBurnerDecimalsContract.ABI + nativeDenom := erc20types.CreateDenom(contractAddr.String()) + sendAmt := ibctesting.DefaultCoinAmount + senderAcc := chain.SenderAccount.GetAddress() + + _, err = evmApp.EVMKeeper.CallEVM( + evmCtx, + contractAbi, + erc20types.ModuleAddress, + contractAddr, + true, + "mint", + common.BytesToAddress(senderAcc), + big.NewInt(sendAmt.Int64()), + ) + if err != nil { + t.Fatalf("mint call failed: %v", err) + } + + // Verify minted balance + bal := evmApp.Erc20Keeper.BalanceOf(evmCtx, contractAbi, contractAddr, common.BytesToAddress(senderAcc)) + if bal.Cmp(big.NewInt(sendAmt.Int64())) != 0 { + t.Fatalf("unexpected ERC20 balance; got %s, want %s", bal.String(), sendAmt.String()) + } + + return &NativeErc20Info{ + Denom: nativeDenom, + ContractAbi: contractAbi, + ContractAddr: contractAddr, + Account: common.BytesToAddress(senderAcc), + InitialBal: big.NewInt(sendAmt.Int64()), + } +} diff --git a/tests/ibc/ibc_middleware_test.go b/tests/ibc/ibc_middleware_test.go index 4c4d64603..03355212a 100644 --- a/tests/ibc/ibc_middleware_test.go +++ b/tests/ibc/ibc_middleware_test.go @@ -7,19 +7,12 @@ import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" - ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" ibctesting "github.com/cosmos/ibc-go/v10/testing" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" testifysuite "github.com/stretchr/testify/suite" - "github.com/cosmos/evm/contracts" "github.com/cosmos/evm/evmd" "github.com/cosmos/evm/ibc" evmibctesting "github.com/cosmos/evm/ibc/testing" @@ -27,7 +20,6 @@ import ( "github.com/cosmos/evm/x/erc20" erc20Keeper "github.com/cosmos/evm/x/erc20/keeper" "github.com/cosmos/evm/x/erc20/types" - erc20types "github.com/cosmos/evm/x/erc20/types" ) // MiddlewareTestSuite tests the IBC middleware for the ERC20 module. @@ -44,19 +36,13 @@ type MiddlewareTestSuite struct { pathBToA *evmibctesting.Path } -type NativeErc20Info struct { - denom string - contractAbi abi.ABI - contractAddr common.Address - account common.Address - initialBal *big.Int -} - +// SetupTest initializes the coordinator and test chains before each test. func (s *MiddlewareTestSuite) SetupTest() { s.coordinator = evmibctesting.NewCoordinator(s.T(), 1, 2) s.evmChainA = s.coordinator.GetChain(ibctesting.GetChainID(1)) s.chainB = s.coordinator.GetChain(ibctesting.GetChainID(2)) + // Setup path for A->B s.pathAToB = evmibctesting.NewPath(s.evmChainA, s.chainB) s.pathAToB.EndpointA.ChannelConfig.PortID = ibctesting.TransferPort s.pathAToB.EndpointB.ChannelConfig.PortID = ibctesting.TransferPort @@ -64,6 +50,7 @@ func (s *MiddlewareTestSuite) SetupTest() { s.pathAToB.EndpointB.ChannelConfig.Version = transfertypes.V1 s.pathAToB.Setup() + // Setup path for B->A s.pathBToA = evmibctesting.NewPath(s.chainB, s.evmChainA) s.pathBToA.EndpointA.ChannelConfig.PortID = ibctesting.TransferPort s.pathBToA.EndpointB.ChannelConfig.PortID = ibctesting.TransferPort @@ -72,61 +59,11 @@ func (s *MiddlewareTestSuite) SetupTest() { s.pathBToA.Setup() } -// SetupNativeErc20 sets up a native ERC20 token on evmChainA and registers it with the ERC20 module. -func (s *MiddlewareTestSuite) SetupNativeErc20() *NativeErc20Info { - evmCtx := s.evmChainA.GetContext() - evmApp := s.evmChainA.App.(*evmd.EVMD) - - // Scenario: Native ERC20 token transfer from evmChainA to chainB - deployedContractAddr, err := evmApp.Erc20Keeper.DeployERC20Contract(evmCtx, banktypes.Metadata{ - DenomUnits: []*banktypes.DenomUnit{ - { - Denom: "example", - Exponent: 18, - }, - }, - Name: "Example", - Symbol: "Ex", - }) - s.Require().NoError(err) - s.evmChainA.NextBlock() - // Create a new token pair for the deployed contract. - _, err = evmApp.Erc20Keeper.RegisterERC20( - evmCtx, - &erc20types.MsgRegisterERC20{ - Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(), - Erc20Addresses: []string{deployedContractAddr.Hex()}, - }, - ) - s.Require().NoError(err) - - // evmChainA sender must have `sendAmt` erc20 tokens before sending the token to chainB via IBC. - contractAbi := contracts.ERC20MinterBurnerDecimalsContract.ABI - nativeErc20Denom := types.CreateDenom(deployedContractAddr.String()) - sendAmt := evmibctesting.DefaultCoinAmount - evmChainAAccount := s.evmChainA.SenderAccount.GetAddress() - // mint native erc20 - _, err = evmApp.EVMKeeper.CallEVM( - evmCtx, contractAbi, erc20types.ModuleAddress, deployedContractAddr, - true, "mint", common.BytesToAddress(evmChainAAccount), big.NewInt(sendAmt.Int64()), - ) - s.Require().NoError(err) - erc20Bal := evmApp.Erc20Keeper.BalanceOf(evmCtx, contractAbi, deployedContractAddr, common.BytesToAddress(evmChainAAccount)) - s.Require().Equal(sendAmt.String(), erc20Bal.String(), "erc20 balance should be equal to minted sendAmt") - - return &NativeErc20Info{ - denom: nativeErc20Denom, - contractAbi: contractAbi, - contractAddr: deployedContractAddr, - account: common.BytesToAddress(evmChainAAccount), - initialBal: sendAmt.BigInt(), - } -} - func TestMiddlewareTestSuite(t *testing.T) { testifysuite.Run(t, new(MiddlewareTestSuite)) } +// TestNewIBCMiddleware verifies the middleware instantiation logic. func (s *MiddlewareTestSuite) TestNewIBCMiddleware() { testCases := []struct { name string @@ -175,9 +112,9 @@ func (s *MiddlewareTestSuite) TestNewIBCMiddleware() { } } +// TestOnRecvPacket checks the OnRecvPacket logic for ICS-20. func (s *MiddlewareTestSuite) TestOnRecvPacket() { var ( - ctx sdk.Context packet channeltypes.Packet ) @@ -205,11 +142,13 @@ func (s *MiddlewareTestSuite) TestOnRecvPacket() { s.Run(tc.name, func() { s.SetupTest() - ctx = s.chainB.GetContext() - bondDenom, err := s.chainB.GetSimApp().StakingKeeper.BondDenom(ctx) + ctxB := s.chainB.GetContext() + bondDenom, err := s.chainB.GetSimApp().StakingKeeper.BondDenom(ctxB) s.Require().NoError(err) + sendAmt := ibctesting.DefaultCoinAmount receiver := s.evmChainA.SenderAccount.GetAddress() + packetData := transfertypes.NewFungibleTokenPacketData( bondDenom, sendAmt.String(), @@ -236,37 +175,41 @@ func (s *MiddlewareTestSuite) TestOnRecvPacket() { transferStack, ok := s.evmChainA.App.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) s.Require().True(ok) - ctx = s.evmChainA.GetContext() + ctxA := s.evmChainA.GetContext() sourceChan := path.EndpointB.GetChannel() - onRecvPacket := func() ibcexported.Acknowledgement { - return transferStack.OnRecvPacket( - ctx, - sourceChan.Version, - packet, - s.evmChainA.SenderAccount.GetAddress()) - } - ack := onRecvPacket() + ack := transferStack.OnRecvPacket( + ctxA, + sourceChan.Version, + packet, + s.evmChainA.SenderAccount.GetAddress(), + ) + if tc.expError == "" { s.Require().True(ack.Success()) - // make sure voucher coins are sent to the receiver + + // Ensure ibc transfer from chainB to evmChainA is successful. data, ackErr := transfertypes.UnmarshalPacketData(packetData.GetBytes(), sourceChan.Version, "") s.Require().Nil(ackErr) + voucherDenom := testutil.GetVoucherDenomFromPacketData(data, packet.GetDestPort(), packet.GetDestChannel()) + evmApp := s.evmChainA.App.(*evmd.EVMD) - voucherCoin := evmApp.BankKeeper.GetBalance(ctx, receiver, voucherDenom) + voucherCoin := evmApp.BankKeeper.GetBalance(ctxA, receiver, voucherDenom) s.Require().Equal(sendAmt.String(), voucherCoin.Amount.String()) - // make sure token pair is registered + + // Make sure token pair is registered tp, err := types.NewTokenPairSTRv2(voucherDenom) s.Require().NoError(err) - tokenPair, found := evmApp.Erc20Keeper.GetTokenPair(ctx, tp.GetID()) + tokenPair, found := evmApp.Erc20Keeper.GetTokenPair(ctxA, tp.GetID()) s.Require().True(found) s.Require().Equal(voucherDenom, tokenPair.Denom) } else { s.Require().False(ack.Success()) - acknowledgement, ok := ack.(channeltypes.Acknowledgement) + + ackObj, ok := ack.(channeltypes.Acknowledgement) s.Require().True(ok) - ackErr, ok := acknowledgement.Response.(*channeltypes.Acknowledgement_Error) + ackErr, ok := ackObj.Response.(*channeltypes.Acknowledgement_Error) s.Require().True(ok) s.Require().Contains(ackErr.Error, tc.expError) } @@ -274,41 +217,46 @@ func (s *MiddlewareTestSuite) TestOnRecvPacket() { } } -// TestOnRecvPacketNativeErc20 tests the OnRecvPacket method for native ERC20 tokens. +// TestOnRecvPacketNativeErc20 checks receiving a native ERC20 token. func (s *MiddlewareTestSuite) TestOnRecvPacketNativeErc20() { s.SetupTest() - nativeErc20 := s.SetupNativeErc20() + nativeErc20 := SetupNativeErc20(s.T(), s.evmChainA) evmCtx := s.evmChainA.GetContext() evmApp := s.evmChainA.App.(*evmd.EVMD) // Scenario: Native ERC20 token transfer from evmChainA to chainB - // evmChainA sender must have `sendAmt` erc20 tokens before sending the token to chainB via IBC. - timeoutHeight := clienttypes.NewHeight(1, 110) path := s.pathAToB chainBAccount := s.chainB.SenderAccount.GetAddress() - // IBC send from evmChainA to chainB. - sendAmt := math.NewIntFromBigInt(nativeErc20.initialBal) - senderEthAddr := nativeErc20.account + + sendAmt := math.NewIntFromBigInt(nativeErc20.InitialBal) + senderEthAddr := nativeErc20.Account sender := sdk.AccAddress(senderEthAddr.Bytes()) + msg := transfertypes.NewMsgTransfer( path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, - sdk.NewCoin(nativeErc20.denom, sendAmt), sender.String(), chainBAccount.String(), + sdk.NewCoin(nativeErc20.Denom, sendAmt), + sender.String(), chainBAccount.String(), timeoutHeight, 0, "", ) _, err := s.evmChainA.SendMsgs(msg) s.Require().NoError(err) // message committed - erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.contractAbi, nativeErc20.contractAddr, senderEthAddr) - s.Require().Equal(erc20BalAfterIbcTransfer.String(), new(big.Int).Sub(nativeErc20.initialBal, sendAmt.BigInt()).String(), "erc20 balance should be equal to minted sendAmt - sendAmt after IBC transfer") + + balAfterTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.ContractAbi, nativeErc20.ContractAddr, senderEthAddr) + s.Require().Equal( + new(big.Int).Sub(nativeErc20.InitialBal, sendAmt.BigInt()).String(), + balAfterTransfer.String(), + ) + // Check native erc20 token is escrowed on evmChainA for sending to chainB. escrowAddr := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) - escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.denom) - s.Require().Equal(escrowedBal.Amount.String(), sendAmt.String(), "escrowed balance should be equal to sendAmt after IBC transfer") + escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.Denom) + s.Require().Equal(sendAmt.String(), escrowedBal.Amount.String()) // chainBNativeErc20Denom is the native erc20 token denom on chainB from evmChainA through IBC. chainBNativeErc20Denom := transfertypes.NewDenom( - nativeErc20.denom, + nativeErc20.Denom, transfertypes.NewHop( s.pathAToB.EndpointB.ChannelConfig.PortID, s.pathAToB.EndpointB.ChannelID, @@ -339,26 +287,23 @@ func (s *MiddlewareTestSuite) TestOnRecvPacketNativeErc20() { s.Require().True(ok) sourceChan := path.EndpointB.GetChannel() - onRecvPacket := func() ibcexported.Acknowledgement { - return transferStack.OnRecvPacket( - evmCtx, - sourceChan.Version, - packet, - s.evmChainA.SenderAccount.GetAddress()) - } - - ack := onRecvPacket() + ack := transferStack.OnRecvPacket( + evmCtx, + sourceChan.Version, + packet, + s.evmChainA.SenderAccount.GetAddress(), + ) s.Require().True(ack.Success()) - // make sure voucher coins are sent to the sender - _, ackErr := transfertypes.UnmarshalPacketData(packetData.GetBytes(), sourceChan.Version, "") - s.Require().Nil(ackErr) - escrowedBal = evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.denom) + + // Check un-escrowed balance on evmChainA after receiving the packet. + escrowedBal = evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.Denom) s.Require().True(escrowedBal.IsZero(), "escrowed balance should be un-escrowed after receiving the packet") + balAfterUnescrow := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.ContractAbi, nativeErc20.ContractAddr, senderEthAddr) + s.Require().Equal(nativeErc20.InitialBal.String(), balAfterUnescrow.String()) } func (s *MiddlewareTestSuite) TestOnAcknowledgementPacket() { var ( - ctx sdk.Context packet channeltypes.Packet ack []byte ) @@ -407,13 +352,16 @@ func (s *MiddlewareTestSuite) TestOnAcknowledgementPacket() { s.Run(tc.name, func() { s.SetupTest() - ctx = s.evmChainA.GetContext() + ctxA := s.evmChainA.GetContext() evmApp := s.evmChainA.App.(*evmd.EVMD) - bondDenom, err := evmApp.StakingKeeper.BondDenom(ctx) + + bondDenom, err := evmApp.StakingKeeper.BondDenom(ctxA) s.Require().NoError(err) + sendAmt := ibctesting.DefaultCoinAmount sender := s.evmChainA.SenderAccount.GetAddress() receiver := s.chainB.SenderAccount.GetAddress() + packetData := transfertypes.NewFungibleTokenPacketData( bondDenom, sendAmt.String(), @@ -421,6 +369,7 @@ func (s *MiddlewareTestSuite) TestOnAcknowledgementPacket() { receiver.String(), "", ) + path := s.pathAToB packet = channeltypes.Packet{ Sequence: 1, @@ -434,7 +383,6 @@ func (s *MiddlewareTestSuite) TestOnAcknowledgementPacket() { } ack = channeltypes.NewResultAcknowledgement([]byte{1}).Acknowledgement() - if tc.malleate != nil { tc.malleate() } @@ -443,9 +391,9 @@ func (s *MiddlewareTestSuite) TestOnAcknowledgementPacket() { s.Require().True(ok) sourceChan := s.pathAToB.EndpointA.GetChannel() - onAcknowledgementPacket := func() error { + onAck := func() error { return transferStack.OnAcknowledgementPacket( - ctx, + ctxA, sourceChan.Version, packet, ack, @@ -464,15 +412,16 @@ func (s *MiddlewareTestSuite) TestOnAcknowledgementPacket() { ) res, err := s.evmChainA.SendMsgs(msg) s.Require().NoError(err) // message committed + packet, err := ibctesting.ParsePacketFromEvents(res.Events) s.Require().NoError(err) - // relay send + // relay the sent packet err = path.RelayPacket(packet) s.Require().NoError(err) // relay committed } - err = onAcknowledgementPacket() + err = onAck() if tc.expError == "" { s.Require().NoError(err) } else { @@ -483,7 +432,7 @@ func (s *MiddlewareTestSuite) TestOnAcknowledgementPacket() { } } -// TestOnAcknowledgementPacketNativeErc20 tests the OnAcknowledgementPacket method for native ERC20 tokens. +// TestOnAcknowledgementPacketNativeErc20 tests ack logic when the packet involves a native ERC20. func (s *MiddlewareTestSuite) TestOnAcknowledgementPacketNativeErc20() { var ( packet channeltypes.Packet @@ -533,57 +482,58 @@ func (s *MiddlewareTestSuite) TestOnAcknowledgementPacketNativeErc20() { tc := tc s.Run(tc.name, func() { s.SetupTest() - nativeErc20 := s.SetupNativeErc20() + nativeErc20 := SetupNativeErc20(s.T(), s.evmChainA) evmCtx := s.evmChainA.GetContext() evmApp := s.evmChainA.App.(*evmd.EVMD) - // Scenario: Native ERC20 token transfer from evmChainA to chainB - // evmChainA sender must have `sendAmt` erc20 tokens before sending the token to chainB via IBC. - timeoutHeight := clienttypes.NewHeight(1, 110) path := s.pathAToB chainBAccount := s.chainB.SenderAccount.GetAddress() - // IBC send from evmChainA to chainB. - sendAmt := math.NewIntFromBigInt(nativeErc20.initialBal) - senderEthAddr := nativeErc20.account + + sendAmt := math.NewIntFromBigInt(nativeErc20.InitialBal) + senderEthAddr := nativeErc20.Account sender := sdk.AccAddress(senderEthAddr.Bytes()) receiver := s.chainB.SenderAccount.GetAddress() + + // Send the native erc20 token from evmChainA to chainB. msg := transfertypes.NewMsgTransfer( path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, - sdk.NewCoin(nativeErc20.denom, sendAmt), sender.String(), receiver.String(), + sdk.NewCoin(nativeErc20.Denom, sendAmt), sender.String(), receiver.String(), timeoutHeight, 0, "", ) - // escrowCheck is a function that checks the state of the evmChainA after the IBC transfer. - // sendAmt should be escrowed on evmChainA for sending to chainB. - // eerc20 balance should be decreased by sendAmt. - escrowCheck := func() { - erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.contractAbi, nativeErc20.contractAddr, senderEthAddr) - s.Require().Equal(erc20BalAfterIbcTransfer.String(), new(big.Int).Sub(nativeErc20.initialBal, sendAmt.BigInt()).String(), "erc20 balance should be equal to minted sendAmt - sendAmt after IBC transfer") - // Check native erc20 token is escrowed on evmChainA for sending to chainB. - escrowAddr := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) - escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.denom) - s.Require().Equal(escrowedBal.Amount.String(), sendAmt.String(), "escrowed balance should be equal to sendAmt after IBC transfer") + + escrowAddr := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) + // checkEscrow is a check function to ensure the native erc20 token is escrowed. + checkEscrow := func() { + erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.ContractAbi, nativeErc20.ContractAddr, senderEthAddr) + s.Require().Equal( + new(big.Int).Sub(nativeErc20.InitialBal, sendAmt.BigInt()).String(), + erc20BalAfterIbcTransfer.String(), + ) + escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.Denom) + s.Require().Equal(sendAmt.String(), escrowedBal.Amount.String()) } - refundCheck := func() { - // Check native erc20 token is un-escrowed on evmChainA after refund. - escrowAddr := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) - escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.denom) - s.Require().True(escrowedBal.IsZero(), "escrowed balance should be un-escrowed after receiving the packet") + + // checkRefund is a check function to ensure refund is processed. + checkRefund := func() { + escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.Denom) + s.Require().True(escrowedBal.IsZero()) // Check erc20 balance is same as initial balance after refund. - erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.contractAbi, nativeErc20.contractAddr, senderEthAddr) - s.Require().Equal(erc20BalAfterIbcTransfer.String(), nativeErc20.initialBal.String(), "erc20 balance should be equal to initial balance after refund") + erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.ContractAbi, nativeErc20.ContractAddr, senderEthAddr) + s.Require().Equal(nativeErc20.InitialBal.String(), erc20BalAfterIbcTransfer.String()) } + _, err := s.evmChainA.SendMsgs(msg) s.Require().NoError(err) // message committed - escrowCheck() + checkEscrow() transferStack, ok := s.evmChainA.App.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) s.Require().True(ok) packetData := transfertypes.NewFungibleTokenPacketData( - nativeErc20.denom, + nativeErc20.Denom, sendAmt.String(), sender.String(), chainBAccount.String(), @@ -599,14 +549,14 @@ func (s *MiddlewareTestSuite) TestOnAcknowledgementPacketNativeErc20() { TimeoutHeight: s.chainB.GetTimeoutHeight(), TimeoutTimestamp: 0, } - ack = channeltypes.NewResultAcknowledgement([]byte{1}).Acknowledgement() + ack = channeltypes.NewResultAcknowledgement([]byte{1}).Acknowledgement() if tc.malleate != nil { tc.malleate() } sourceChan := path.EndpointA.GetChannel() - onAcknowledgementPacket := func() error { + onAck := func() error { return transferStack.OnAcknowledgementPacket( evmCtx, sourceChan.Version, @@ -616,25 +566,26 @@ func (s *MiddlewareTestSuite) TestOnAcknowledgementPacketNativeErc20() { ) } - err = onAcknowledgementPacket() + err = onAck() if tc.expError == "" { s.Require().NoError(err) } else { s.Require().Error(err) s.Require().Contains(err.Error(), tc.expError) } + if tc.expRefund { - refundCheck() + checkRefund() } else { - escrowCheck() + checkEscrow() } }) } } +// TestOnTimeoutPacket checks the timeout handling for ICS-20. func (s *MiddlewareTestSuite) TestOnTimeoutPacket() { var ( - ctx sdk.Context packet channeltypes.Packet ) @@ -665,13 +616,15 @@ func (s *MiddlewareTestSuite) TestOnTimeoutPacket() { s.Run(tc.name, func() { s.SetupTest() - ctx = s.evmChainA.GetContext() + ctxA := s.evmChainA.GetContext() evmApp := s.evmChainA.App.(*evmd.EVMD) - bondDenom, err := evmApp.StakingKeeper.BondDenom(ctx) + bondDenom, err := evmApp.StakingKeeper.BondDenom(ctxA) s.Require().NoError(err) + sendAmt := ibctesting.DefaultCoinAmount sender := s.evmChainA.SenderAccount.GetAddress() receiver := s.chainB.SenderAccount.GetAddress() + packetData := transfertypes.NewFungibleTokenPacketData( bondDenom, sendAmt.String(), @@ -679,6 +632,7 @@ func (s *MiddlewareTestSuite) TestOnTimeoutPacket() { receiver.String(), "", ) + path := s.pathAToB packet = channeltypes.Packet{ Sequence: 1, @@ -699,9 +653,9 @@ func (s *MiddlewareTestSuite) TestOnTimeoutPacket() { s.Require().True(ok) sourceChan := s.pathAToB.EndpointA.GetChannel() - onTimeoutPacket := func() error { + onTimeout := func() error { return transferStack.OnTimeoutPacket( - ctx, + ctxA, sourceChan.Version, packet, sender, @@ -718,17 +672,18 @@ func (s *MiddlewareTestSuite) TestOnTimeoutPacket() { receiver.String(), timeoutHeight, 0, "", ) + res, err := s.evmChainA.SendMsgs(msg) s.Require().NoError(err) // message committed + packet, err := ibctesting.ParsePacketFromEvents(res.Events) s.Require().NoError(err) - // relay send err = path.RelayPacket(packet) s.Require().NoError(err) // relay committed } - err = onTimeoutPacket() + err = onTimeout() if tc.expError == "" { s.Require().NoError(err) } else { @@ -771,57 +726,56 @@ func (s *MiddlewareTestSuite) TestOnTimeoutPacketNativeErc20() { tc := tc s.Run(tc.name, func() { s.SetupTest() - nativeErc20 := s.SetupNativeErc20() + nativeErc20 := SetupNativeErc20(s.T(), s.evmChainA) evmCtx := s.evmChainA.GetContext() evmApp := s.evmChainA.App.(*evmd.EVMD) - // Scenario: Native ERC20 token transfer from evmChainA to chainB - // evmChainA sender must have `sendAmt` erc20 tokens before sending the token to chainB via IBC. - timeoutHeight := clienttypes.NewHeight(1, 110) path := s.pathAToB chainBAccount := s.chainB.SenderAccount.GetAddress() - // IBC send from evmChainA to chainB. - sendAmt := math.NewIntFromBigInt(nativeErc20.initialBal) - senderEthAddr := nativeErc20.account + + sendAmt := math.NewIntFromBigInt(nativeErc20.InitialBal) + senderEthAddr := nativeErc20.Account sender := sdk.AccAddress(senderEthAddr.Bytes()) receiver := s.chainB.SenderAccount.GetAddress() + msg := transfertypes.NewMsgTransfer( path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, - sdk.NewCoin(nativeErc20.denom, sendAmt), sender.String(), receiver.String(), + sdk.NewCoin(nativeErc20.Denom, sendAmt), sender.String(), receiver.String(), timeoutHeight, 0, "", ) - // escrowCheck is a function that checks the state of the evmChainA after the IBC transfer. - // sendAmt should be escrowed on evmChainA for sending to chainB. - // eerc20 balance should be decreased by sendAmt. - escrowCheck := func() { - erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.contractAbi, nativeErc20.contractAddr, senderEthAddr) - s.Require().Equal(erc20BalAfterIbcTransfer.String(), new(big.Int).Sub(nativeErc20.initialBal, sendAmt.BigInt()).String(), "erc20 balance should be equal to minted sendAmt - sendAmt after IBC transfer") - // Check native erc20 token is escrowed on evmChainA for sending to chainB. - escrowAddr := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) - escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.denom) - s.Require().Equal(escrowedBal.Amount.String(), sendAmt.String(), "escrowed balance should be equal to sendAmt after IBC transfer") + + escrowAddr := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) + // checkEscrow is a check function to ensure the native erc20 token is escrowed. + checkEscrow := func() { + erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.ContractAbi, nativeErc20.ContractAddr, senderEthAddr) + s.Require().Equal( + new(big.Int).Sub(nativeErc20.InitialBal, sendAmt.BigInt()).String(), + erc20BalAfterIbcTransfer.String(), + ) + escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.Denom) + s.Require().Equal(sendAmt.String(), escrowedBal.Amount.String()) } - refundCheck := func() { - // Check native erc20 token is un-escrowed on evmChainA after refund. - escrowAddr := transfertypes.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) - escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.denom) - s.Require().True(escrowedBal.IsZero(), "escrowed balance should be un-escrowed after receiving the packet") + + // checkRefund is a check function to ensure refund is processed. + checkRefund := func() { + escrowedBal := evmApp.BankKeeper.GetBalance(evmCtx, escrowAddr, nativeErc20.Denom) + s.Require().True(escrowedBal.IsZero()) // Check erc20 balance is same as initial balance after refund. - erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.contractAbi, nativeErc20.contractAddr, senderEthAddr) - s.Require().Equal(erc20BalAfterIbcTransfer.String(), nativeErc20.initialBal.String(), "erc20 balance should be equal to initial balance after refund") + erc20BalAfterIbcTransfer := evmApp.Erc20Keeper.BalanceOf(evmCtx, nativeErc20.ContractAbi, nativeErc20.ContractAddr, senderEthAddr) + s.Require().Equal(nativeErc20.InitialBal.String(), erc20BalAfterIbcTransfer.String()) } _, err := s.evmChainA.SendMsgs(msg) s.Require().NoError(err) // message committed - escrowCheck() + checkEscrow() transferStack, ok := s.evmChainA.App.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) s.Require().True(ok) packetData := transfertypes.NewFungibleTokenPacketData( - nativeErc20.denom, + nativeErc20.Denom, sendAmt.String(), sender.String(), chainBAccount.String(), @@ -843,26 +797,24 @@ func (s *MiddlewareTestSuite) TestOnTimeoutPacketNativeErc20() { } sourceChan := path.EndpointA.GetChannel() - onTimeoutPacket := func() error { - return transferStack.OnTimeoutPacket( - evmCtx, - sourceChan.Version, - packet, - receiver, - ) - } + err = transferStack.OnTimeoutPacket( + evmCtx, + sourceChan.Version, + packet, + receiver, + ) - err = onTimeoutPacket() if tc.expError == "" { s.Require().NoError(err) } else { s.Require().Error(err) s.Require().Contains(err.Error(), tc.expError) } + if tc.expRefund { - refundCheck() + checkRefund() } else { - escrowCheck() + checkEscrow() } }) }