mukan-ibc/modules/apps/transfer/v2/ibc_module_test.go
Mukan Erkin Törük 88dd97a9f8
Some checks failed
CodeQL / Analyze (push) Waiting to run
golangci-lint / lint (push) Waiting to run
Tests / Code Coverage / build (amd64) (push) Waiting to run
Tests / Code Coverage / build (arm64) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[additional-args:-tags="test_e2e" name:e2e path:./e2e]) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[name:08-wasm path:./modules/light-clients/08-wasm]) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[name:ibc-go path:.]) (push) Waiting to run
Docker Build & Push Simapp (main) / docker-build (push) Has been cancelled
refactor: replace all github.com upstream refs with git.cw.tr/mukan-network
2026-05-11 03:36:22 +03:00

421 lines
16 KiB
Go

package v2_test
import (
"crypto/sha256"
"fmt"
"testing"
"time"
testifysuite "github.com/stretchr/testify/suite"
sdkmath "cosmossdk.io/math"
sdk "git.cw.tr/mukan-network/mukan-sdk/types"
"git.cw.tr/mukan-network/mukan-ibc/modules/apps/transfer/types"
clienttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client/types"
channeltypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/04-channel/types"
channeltypesv2 "git.cw.tr/mukan-network/mukan-ibc/modules/core/04-channel/v2/types"
ibctesting "git.cw.tr/mukan-network/mukan-ibc/testing"
)
const testclientid = "testclientid"
type TransferTestSuite struct {
testifysuite.Suite
coordinator *ibctesting.Coordinator
// testing chains used for convenience and readability
chainA *ibctesting.TestChain
chainB *ibctesting.TestChain
chainC *ibctesting.TestChain
pathAToB *ibctesting.Path
pathBToC *ibctesting.Path
}
const invalidPortID = "invalidportid"
func (suite *TransferTestSuite) SetupTest() {
suite.coordinator = ibctesting.NewCoordinator(suite.T(), 3)
suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(1))
suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(2))
suite.chainC = suite.coordinator.GetChain(ibctesting.GetChainID(3))
// setup between chainA and chainB
// NOTE:
// pathAToB.EndpointA = endpoint on chainA
// pathAToB.EndpointB = endpoint on chainB
suite.pathAToB = ibctesting.NewPath(suite.chainA, suite.chainB)
// setup between chainB and chainC
// pathBToC.EndpointA = endpoint on chainB
// pathBToC.EndpointB = endpoint on chainC
suite.pathBToC = ibctesting.NewPath(suite.chainB, suite.chainC)
// setup IBC v2 paths between the chains
suite.pathAToB.SetupV2()
suite.pathBToC.SetupV2()
}
func TestTransferTestSuite(t *testing.T) {
testifysuite.Run(t, new(TransferTestSuite))
}
func (suite *TransferTestSuite) TestOnSendPacket() {
var payload channeltypesv2.Payload
testCases := []struct {
name string
sourceDenomToTransfer string
malleate func()
expError error
}{
{
"transfer single denom",
sdk.DefaultBondDenom,
func() {},
nil,
},
{
"transfer with invalid source port",
sdk.DefaultBondDenom,
func() {
payload.SourcePort = invalidPortID
},
channeltypesv2.ErrInvalidPacket,
},
{
"transfer with invalid destination port",
sdk.DefaultBondDenom,
func() {
payload.DestinationPort = invalidPortID
},
channeltypesv2.ErrInvalidPacket,
},
{
"transfer with invalid source client",
sdk.DefaultBondDenom,
func() {
suite.pathAToB.EndpointA.ClientID = testclientid
},
channeltypesv2.ErrInvalidPacket,
},
{
"transfer with invalid destination client",
sdk.DefaultBondDenom,
func() {
suite.pathAToB.EndpointB.ClientID = testclientid
},
channeltypesv2.ErrInvalidPacket,
},
{
"transfer with slashes in base denom",
"base/coin",
func() {},
types.ErrInvalidDenomForTransfer,
},
{
"transfer with slashes in ibc denom",
fmt.Sprintf("ibc/%x", sha256.Sum256([]byte("coin"))),
func() {},
types.ErrInvalidDenomForTransfer,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest() // reset
originalBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), tc.sourceDenomToTransfer)
amount, ok := sdkmath.NewIntFromString("9223372036854775808") // 2^63 (one above int64)
suite.Require().True(ok)
originalCoin := sdk.NewCoin(tc.sourceDenomToTransfer, amount)
token := types.Token{
Denom: types.Denom{Base: originalCoin.Denom},
Amount: originalCoin.Amount.String(),
}
transferData := types.NewFungibleTokenPacketData(token.Denom.Path(), token.Amount, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), "")
bz := suite.chainA.Codec.MustMarshal(&transferData)
payload = channeltypesv2.NewPayload(types.PortID, types.PortID, types.V1, types.EncodingProtobuf, bz)
// malleate payload
tc.malleate()
ctx := suite.chainA.GetContext()
cbs := suite.chainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort)
err := cbs.OnSendPacket(ctx, suite.pathAToB.EndpointA.ClientID, suite.pathAToB.EndpointB.ClientID, 1, payload, suite.chainA.SenderAccount.GetAddress())
if tc.expError != nil {
suite.Require().Contains(err.Error(), tc.expError.Error())
return
}
suite.Require().NoError(err)
escrowAddress := types.GetEscrowAddress(types.PortID, suite.pathAToB.EndpointA.ClientID)
// check that the balance for chainA is updated
chainABalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), originalCoin.Denom)
suite.Require().Equal(originalBalance.Amount.Sub(amount).Int64(), chainABalance.Amount.Int64())
// check that module account escrow address has locked the tokens
chainAEscrowBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), escrowAddress, originalCoin.Denom)
suite.Require().Equal(originalCoin, chainAEscrowBalance)
})
}
}
func (suite *TransferTestSuite) TestOnRecvPacket() {
var payload channeltypesv2.Payload
testCases := []struct {
name string
sourceDenomToTransfer string
malleate func()
expErr bool
}{
{
"transfer single denom",
sdk.DefaultBondDenom,
func() {},
false,
},
{
"transfer with invalid source port",
sdk.DefaultBondDenom,
func() {
payload.SourcePort = invalidPortID
},
true,
},
{
"transfer with invalid dest port",
sdk.DefaultBondDenom,
func() {
payload.DestinationPort = invalidPortID
},
true,
},
{
"transfer with invalid source client",
sdk.DefaultBondDenom,
func() {
suite.pathAToB.EndpointA.ClientID = testclientid
},
true,
},
{
"transfer with invalid destination client",
sdk.DefaultBondDenom,
func() {
suite.pathAToB.EndpointB.ClientID = testclientid
},
true,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest() // reset
originalBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), tc.sourceDenomToTransfer)
timeoutTimestamp := uint64(suite.chainB.GetContext().BlockTime().Add(time.Hour).Unix())
amount, ok := sdkmath.NewIntFromString("9223372036854775808") // 2^63 (one above int64)
suite.Require().True(ok)
originalCoin := sdk.NewCoin(tc.sourceDenomToTransfer, amount)
msg := types.NewMsgTransferWithEncoding(types.PortID, suite.pathAToB.EndpointA.ClientID, originalCoin, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), clienttypes.Height{}, timeoutTimestamp, "", types.EncodingProtobuf)
resp, err := suite.chainA.SendMsgs(msg)
suite.Require().NoError(err) // message committed
packets, err := ibctesting.ParseIBCV2Packets(channeltypes.EventTypeSendPacket, resp.Events)
suite.Require().NoError(err)
suite.Require().Len(packets, 1)
suite.Require().Len(packets[0].Payloads, 1)
payload = packets[0].Payloads[0]
ctx := suite.chainB.GetContext()
cbs := suite.chainB.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort)
// malleate payload after it has been sent but before OnRecvPacket callback is called
tc.malleate()
recvResult := cbs.OnRecvPacket(ctx, suite.pathAToB.EndpointA.ClientID, suite.pathAToB.EndpointB.ClientID, packets[0].Sequence, payload, suite.chainB.SenderAccount.GetAddress())
if tc.expErr {
suite.Require().Equal(channeltypesv2.PacketStatus_Failure, recvResult.Status)
return
}
suite.Require().Equal(channeltypesv2.PacketStatus_Success, recvResult.Status)
suite.Require().Equal(channeltypes.NewResultAcknowledgement([]byte{byte(1)}).Acknowledgement(), recvResult.Acknowledgement)
escrowAddress := types.GetEscrowAddress(types.PortID, suite.pathAToB.EndpointA.ClientID)
// check that the balance for chainA is updated
chainABalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), originalCoin.Denom)
suite.Require().Equal(originalBalance.Amount.Sub(amount).Int64(), chainABalance.Amount.Int64())
// check that module account escrow address has locked the tokens
chainAEscrowBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), escrowAddress, originalCoin.Denom)
suite.Require().Equal(originalCoin, chainAEscrowBalance)
traceAToB := types.NewHop(types.PortID, suite.pathAToB.EndpointB.ClientID)
// check that voucher exists on chain B
chainBDenom := types.NewDenom(originalCoin.Denom, traceAToB)
chainBBalance := suite.chainB.GetSimApp().BankKeeper.GetBalance(suite.chainB.GetContext(), suite.chainB.SenderAccount.GetAddress(), chainBDenom.IBCDenom())
coinSentFromAToB := sdk.NewCoin(chainBDenom.IBCDenom(), amount)
suite.Require().Equal(coinSentFromAToB, chainBBalance)
})
}
}
func (suite *TransferTestSuite) TestOnAckPacket() {
testCases := []struct {
name string
sourceDenomToTransfer string
}{
{
"transfer single denom",
sdk.DefaultBondDenom,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest() // reset
originalBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), tc.sourceDenomToTransfer)
timeoutTimestamp := uint64(suite.chainB.GetContext().BlockTime().Add(time.Hour).Unix())
amount, ok := sdkmath.NewIntFromString("9223372036854775808") // 2^63 (one above int64)
suite.Require().True(ok)
originalCoin := sdk.NewCoin(tc.sourceDenomToTransfer, amount)
msg := types.NewMsgTransferWithEncoding(types.PortID, suite.pathAToB.EndpointA.ClientID, originalCoin, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), clienttypes.Height{}, timeoutTimestamp, "", types.EncodingProtobuf)
resp, err := suite.chainA.SendMsgs(msg)
suite.Require().NoError(err) // message committed
packets, err := ibctesting.ParseIBCV2Packets(channeltypes.EventTypeSendPacket, resp.Events)
suite.Require().NoError(err)
suite.Require().Len(packets, 1)
suite.Require().Len(packets[0].Payloads, 1)
payload := packets[0].Payloads[0]
ctx := suite.chainA.GetContext()
cbs := suite.chainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort)
ack := channeltypes.NewResultAcknowledgement([]byte{byte(1)})
err = cbs.OnAcknowledgementPacket(
ctx, suite.pathAToB.EndpointA.ClientID, suite.pathAToB.EndpointB.ClientID,
packets[0].Sequence, ack.Acknowledgement(), payload, suite.chainA.SenderAccount.GetAddress(),
)
suite.Require().NoError(err)
// on successful ack, the tokens sent in packets should still be in escrow
escrowAddress := types.GetEscrowAddress(types.PortID, suite.pathAToB.EndpointA.ClientID)
// check that the balance for chainA is updated
chainABalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), originalCoin.Denom)
suite.Require().Equal(originalBalance.Amount.Sub(amount).Int64(), chainABalance.Amount.Int64())
// check that module account escrow address has locked the tokens
chainAEscrowBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), escrowAddress, originalCoin.Denom)
suite.Require().Equal(originalCoin, chainAEscrowBalance)
// create a custom error ack and replay the callback to ensure it fails with IBC v2 callbacks
errAck := channeltypes.NewErrorAcknowledgement(types.ErrInvalidAmount)
err = cbs.OnAcknowledgementPacket(
ctx, suite.pathAToB.EndpointA.ClientID, suite.pathAToB.EndpointB.ClientID,
1, errAck.Acknowledgement(), payload, suite.chainA.SenderAccount.GetAddress(),
)
suite.Require().Error(err)
// create the sentinel error ack and replay the callback to ensure the tokens are correctly refunded
// we can replay the callback here because the replay protection is handled in the IBC handler
err = cbs.OnAcknowledgementPacket(
ctx, suite.pathAToB.EndpointA.ClientID, suite.pathAToB.EndpointB.ClientID,
1, channeltypesv2.ErrorAcknowledgement[:], payload, suite.chainA.SenderAccount.GetAddress(),
)
suite.Require().NoError(err)
// on error ack, the tokens sent in packets should be returned to sender
// check that the balance for chainA is refunded
chainABalance = suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), originalCoin.Denom)
suite.Require().Equal(originalBalance.Amount, chainABalance.Amount)
// check that module account escrow address has no tokens
chainAEscrowBalance = suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), escrowAddress, originalCoin.Denom)
suite.Require().Equal(sdk.NewCoin(originalCoin.Denom, sdkmath.ZeroInt()), chainAEscrowBalance)
})
}
}
func (suite *TransferTestSuite) TestOnTimeoutPacket() {
testCases := []struct {
name string
sourceDenomToTransfer string
}{
{
"transfer single denom",
sdk.DefaultBondDenom,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest() // reset
originalBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), tc.sourceDenomToTransfer)
timeoutTimestamp := uint64(suite.chainB.GetContext().BlockTime().Add(time.Hour).Unix())
amount, ok := sdkmath.NewIntFromString("9223372036854775808") // 2^63 (one above int64)
suite.Require().True(ok)
originalCoin := sdk.NewCoin(tc.sourceDenomToTransfer, amount)
msg := types.NewMsgTransferWithEncoding(types.PortID, suite.pathAToB.EndpointA.ClientID, originalCoin, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), clienttypes.Height{}, timeoutTimestamp, "", types.EncodingProtobuf)
resp, err := suite.chainA.SendMsgs(msg)
suite.Require().NoError(err) // message committed
packets, err := ibctesting.ParseIBCV2Packets(channeltypes.EventTypeSendPacket, resp.Events)
suite.Require().NoError(err)
suite.Require().Len(packets, 1)
suite.Require().Len(packets[0].Payloads, 1)
payload := packets[0].Payloads[0]
// on successful send, the tokens sent in packets should be in escrow
escrowAddress := types.GetEscrowAddress(types.PortID, suite.pathAToB.EndpointA.ClientID)
// check that the balance for chainA is updated
chainABalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), originalCoin.Denom)
suite.Require().Equal(originalBalance.Amount.Sub(amount).Int64(), chainABalance.Amount.Int64())
// check that module account escrow address has locked the tokens
chainAEscrowBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), escrowAddress, originalCoin.Denom)
suite.Require().Equal(originalCoin, chainAEscrowBalance)
ctx := suite.chainA.GetContext()
cbs := suite.chainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort)
err = cbs.OnTimeoutPacket(ctx, suite.pathAToB.EndpointA.ClientID, suite.pathAToB.EndpointB.ClientID, packets[0].Sequence, payload, suite.chainA.SenderAccount.GetAddress())
suite.Require().NoError(err)
// on timeout, the tokens sent in packets should be returned to sender
// check that the balance for chainA is refunded
chainABalance = suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), originalCoin.Denom)
suite.Require().Equal(originalBalance.Amount, chainABalance.Amount)
// check that module account escrow address has no tokens
chainAEscrowBalance = suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), escrowAddress, originalCoin.Denom)
suite.Require().Equal(sdk.NewCoin(originalCoin.Denom, sdkmath.ZeroInt()), chainAEscrowBalance)
})
}
}