mukan-ibc/modules/apps/callbacks/replay_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

344 lines
13 KiB
Go

package ibccallbacks_test
import (
"fmt"
sdk "git.cw.tr/mukan-network/mukan-sdk/types"
"git.cw.tr/mukan-network/mukan-ibc/modules/apps/callbacks/testing/simapp"
transfertypes "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"
host "git.cw.tr/mukan-network/mukan-ibc/modules/core/24-host"
ibcexported "git.cw.tr/mukan-network/mukan-ibc/modules/core/exported"
ibctesting "git.cw.tr/mukan-network/mukan-ibc/testing"
)
func (s *CallbacksTestSuite) TestTransferTimeoutReplayProtection() {
testCases := []struct {
name string
transferMemo string
}{
{
"success: REPLAY TIMEOUT",
fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, simapp.SuccessContract),
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
s.SetupTransferTest()
callbackCount := 0 // used to count the number of times the timeout callback is called.
// This simulates a contract which submits a replay timeout packet:
GetSimApp(s.chainA).MockContractKeeper.IBCOnTimeoutPacketCallbackFn = func(
cachedCtx sdk.Context,
packet channeltypes.Packet,
_ sdk.AccAddress,
_, _, _ string,
) error {
// only replay the timeout packet twice. We could replay it more times
callbackCount++
if callbackCount == 2 {
return nil
}
// construct the timeoutMsg
packetKey := host.PacketReceiptKey(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence())
counterparty := s.path.EndpointA.Counterparty
proof, proofHeight := counterparty.QueryProof(packetKey)
nextSeqRecv, found := counterparty.Chain.App.GetIBCKeeper().ChannelKeeper.GetNextSequenceRecv(counterparty.Chain.GetContext(), counterparty.ChannelConfig.PortID, counterparty.ChannelID)
s.Require().True(found)
timeoutMsg := channeltypes.NewMsgTimeout(
packet, nextSeqRecv,
proof, proofHeight, s.chainA.SenderAccount.GetAddress().String(),
)
// in a real scenario, this should be s.chainA.SendMsg
// but I couldn't get it to work due to the way our testsuite is setup
// (we increment the account sequence after full block execution)
// (we also don't support sending messages from other accounts)
res, err := GetSimApp(s.chainA).IBCKeeper.Timeout(cachedCtx, timeoutMsg)
s.Require().NoError(err)
s.Require().Equal(channeltypes.NOOP, res.Result)
return nil
}
// fund escrow account
fund := ibctesting.TestCoins.Add(ibctesting.TestCoin).Add(ibctesting.TestCoin)
escrowAddress := transfertypes.GetEscrowAddress(s.path.EndpointA.ChannelConfig.PortID, s.path.EndpointA.ChannelID)
err := GetSimApp(s.chainA).BankKeeper.SendCoins(s.chainA.GetContext(), s.chainA.SenderAccount.GetAddress(), escrowAddress, fund)
s.Require().NoError(err)
// set total escrow for denom
GetSimApp(s.chainA).TransferKeeper.SetTotalEscrowForDenom(s.chainA.GetContext(), fund[0])
// save initial balance of sender
initialBalance := GetSimApp(s.chainA).BankKeeper.GetBalance(s.chainA.GetContext(), s.chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom)
// amountTransferred := ibctesting.TestCoin
s.ExecuteTransferTimeout(tc.transferMemo)
// check that the callback is executed 1 times
s.Require().Equal(1, callbackCount)
afterBalance := GetSimApp(s.chainA).BankKeeper.GetBalance(s.chainA.GetContext(), s.chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom)
// expected is not a malicious amount
s.Require().Equal(initialBalance.Amount, afterBalance.Amount)
})
}
}
func (s *CallbacksTestSuite) TestTransferErrorAcknowledgementReplayProtection() {
testCases := []struct {
name string
transferMemo string
}{
{
"success: REPLAY ERROR ACK",
fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, simapp.SuccessContract),
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
s.SetupTransferTest()
callbackCount := 0 // used to count the number of times the ack callback is called.
// This simulates a contract which submits a replay ack packet:
GetSimApp(s.chainA).MockContractKeeper.IBCOnAcknowledgementPacketCallbackFn = func(
cachedCtx sdk.Context,
packet channeltypes.Packet,
ack []byte,
_ sdk.AccAddress,
_, _, _ string,
) error {
// only replay the ack packet twice. We could replay it more times
callbackCount++
if callbackCount == 2 {
return nil
}
// construct the ackMsg
packetKey := host.PacketAcknowledgementKey(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence())
proof, proofHeight := s.path.EndpointA.Counterparty.QueryProof(packetKey)
ackMsg := channeltypes.NewMsgAcknowledgement(packet, ack, proof, proofHeight, s.chainA.SenderAccount.GetAddress().String())
// in a real scenario, this should be s.chainA.SendMsg
// but I couldn't get it to work due to the way our testsuite is setup
// (we increment the account sequence after full block execution)
// (we also don't support sending messages from other accounts)
res, err := GetSimApp(s.chainA).IBCKeeper.Acknowledgement(cachedCtx, ackMsg)
s.Require().NoError(err)
s.Require().Equal(channeltypes.NOOP, res.Result) // no-op because this is a redundant replay
return nil
}
// fund escrow account
fund := ibctesting.TestCoins.Add(ibctesting.TestCoin).Add(ibctesting.TestCoin)
escrowAddress := transfertypes.GetEscrowAddress(s.path.EndpointA.ChannelConfig.PortID, s.path.EndpointA.ChannelID)
err := GetSimApp(s.chainA).BankKeeper.SendCoins(s.chainA.GetContext(), s.chainA.SenderAccount.GetAddress(), escrowAddress, fund)
s.Require().NoError(err)
// set total escrow for denom
GetSimApp(s.chainA).TransferKeeper.SetTotalEscrowForDenom(s.chainA.GetContext(), fund[0])
// save initial balance of sender
initialBalance := GetSimApp(s.chainA).BankKeeper.GetBalance(s.chainA.GetContext(), s.chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom)
// amountTransferred := ibctesting.TestCoin
s.ExecuteFailedTransfer(tc.transferMemo)
// check that the callback is executed 1 times
s.Require().Equal(1, callbackCount)
expBalance := initialBalance
afterBalance := GetSimApp(s.chainA).BankKeeper.GetBalance(s.chainA.GetContext(), s.chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom)
// expected is not a malicious amount
s.Require().Equal(expBalance.Amount, afterBalance.Amount)
})
}
}
func (s *CallbacksTestSuite) TestTransferSuccessAcknowledgementReplayProtection() {
testCases := []struct {
name string
transferMemo string
}{
{
"success: REPLAY SUCCESS ACK",
fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, simapp.SuccessContract),
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
s.SetupTransferTest()
callbackCount := 0 // used to count the number of times the ack callback is called.
// This simulates a contract which submits a replay ack packet:
GetSimApp(s.chainA).MockContractKeeper.IBCOnAcknowledgementPacketCallbackFn = func(
cachedCtx sdk.Context,
packet channeltypes.Packet,
ack []byte,
_ sdk.AccAddress,
_, _, _ string,
) error {
// only replay the ack packet twice. We could replay it more times
callbackCount++
if callbackCount == 2 {
return nil
}
// construct the ackMsg
packetKey := host.PacketAcknowledgementKey(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence())
proof, proofHeight := s.path.EndpointA.Counterparty.QueryProof(packetKey)
ackMsg := channeltypes.NewMsgAcknowledgement(packet, ack, proof, proofHeight, s.chainA.SenderAccount.GetAddress().String())
// in a real scenario, this should be s.chainA.SendMsg
// but I couldn't get it to work due to the way our testsuite is setup
// (we increment the account sequence after full block execution)
// (we also don't support sending messages from other accounts)
res, err := GetSimApp(s.chainA).IBCKeeper.Acknowledgement(cachedCtx, ackMsg)
s.Require().NoError(err)
s.Require().Equal(channeltypes.NOOP, res.Result) // no-op because this is a redundant replay
return nil
}
// save initial balance of sender
initialBalance := GetSimApp(s.chainA).BankKeeper.GetBalance(s.chainA.GetContext(), s.chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom)
// amountTransferred := ibctesting.TestCoin
s.ExecuteTransfer(tc.transferMemo, true)
// check that the callback is executed 1 times
s.Require().Equal(1, callbackCount)
expBalance := initialBalance.Sub(ibctesting.TestCoin)
afterBalance := GetSimApp(s.chainA).BankKeeper.GetBalance(s.chainA.GetContext(), s.chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom)
// expected is not a malicious amount
s.Require().Equal(expBalance.Amount, afterBalance.Amount)
})
}
}
func (s *CallbacksTestSuite) TestTransferRecvPacketReplayProtection() {
testCases := []struct {
name string
transferMemo string
}{
{
"success: REPLAY RECV PACKET",
fmt.Sprintf(`{"dest_callback": {"address": "%s"}}`, simapp.SuccessContract),
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
s.SetupTransferTest()
callbackCount := 0 // used to count the number of times the RecvPacket callback is called.
// Write a contract in Chain B that tries to execute receive packet 2 times!
GetSimApp(s.chainB).MockContractKeeper.IBCReceivePacketCallbackFn = func(
cachedCtx sdk.Context,
packet ibcexported.PacketI,
_ ibcexported.Acknowledgement,
_, _ string,
) error {
callbackCount++
if callbackCount == 2 {
return nil
}
packetKey := host.PacketCommitmentKey(packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())
proof, proofHeight := s.path.EndpointB.Counterparty.Chain.QueryProof(packetKey)
recvMsg := channeltypes.NewMsgRecvPacket(packet.(channeltypes.Packet), proof, proofHeight, s.chainB.SenderAccount.GetAddress().String())
// send again
res, err := GetSimApp(s.chainB).IBCKeeper.RecvPacket(cachedCtx, recvMsg)
s.Require().NoError(err)
s.Require().Equal(channeltypes.NOOP, res.Result) // no-op because this is a redundant replay
return nil
}
// save initial balance of receiver
denom := transfertypes.NewDenom(sdk.DefaultBondDenom, transfertypes.NewHop(s.path.EndpointB.ChannelConfig.PortID, s.path.EndpointB.ChannelID))
initialBalance := GetSimApp(s.chainB).BankKeeper.GetBalance(s.chainB.GetContext(), s.chainB.SenderAccount.GetAddress(), denom.IBCDenom())
// execute the transfer
s.ExecuteTransfer(tc.transferMemo, true)
// check that the callback is executed 1 times
s.Require().Equal(1, callbackCount)
// expected is not a malicious amount
expBalance := initialBalance.Add(sdk.NewCoin(denom.IBCDenom(), ibctesting.TestCoin.Amount))
afterBalance := GetSimApp(s.chainB).BankKeeper.GetBalance(s.chainB.GetContext(), s.chainB.SenderAccount.GetAddress(), denom.IBCDenom())
s.Require().Equal(expBalance.Amount, afterBalance.Amount)
})
}
}
// ExecuteFailedTransfer executes a transfer message on chainA for ibctesting.TestCoin (100 "stake").
// The transfer will fail on RecvPacket and an error acknowledgement will be sent back to chainA.
func (s *CallbacksTestSuite) ExecuteFailedTransfer(memo string) {
GetSimApp(s.chainB).TransferKeeper.SetParams(s.chainB.GetContext(), transfertypes.Params{
ReceiveEnabled: false,
SendEnabled: true,
})
defer GetSimApp(s.chainB).TransferKeeper.SetParams(s.chainB.GetContext(), transfertypes.DefaultParams())
escrowAddress := transfertypes.GetEscrowAddress(s.path.EndpointA.ChannelConfig.PortID, s.path.EndpointA.ChannelID)
// record the balance of the escrow address before the transfer
escrowBalance := GetSimApp(s.chainA).BankKeeper.GetBalance(s.chainA.GetContext(), escrowAddress, sdk.DefaultBondDenom)
// record the balance of the receiving address before the transfer
denom := transfertypes.NewDenom(sdk.DefaultBondDenom, transfertypes.NewHop(s.path.EndpointB.ChannelConfig.PortID, s.path.EndpointB.ChannelID))
receiverBalance := GetSimApp(s.chainB).BankKeeper.GetBalance(s.chainB.GetContext(), s.chainB.SenderAccount.GetAddress(), denom.IBCDenom())
amount := ibctesting.TestCoin
msg := transfertypes.NewMsgTransfer(
s.path.EndpointA.ChannelConfig.PortID,
s.path.EndpointA.ChannelID,
amount,
s.chainA.SenderAccount.GetAddress().String(),
s.chainB.SenderAccount.GetAddress().String(),
clienttypes.NewHeight(1, 100), 0, memo,
)
res, err := s.chainA.SendMsgs(msg)
if err != nil {
return // we return if send packet is rejected
}
packet, err := ibctesting.ParseV1PacketFromEvents(res.GetEvents())
s.Require().NoError(err)
// relay send
err = s.path.RelayPacket(packet)
s.Require().NoError(err) // relay committed
// check that the escrow address balance hasn't changed
s.Require().Equal(escrowBalance, GetSimApp(s.chainA).BankKeeper.GetBalance(s.chainA.GetContext(), escrowAddress, sdk.DefaultBondDenom))
// check that the receiving address balance hasn't changed
s.Require().Equal(receiverBalance, GetSimApp(s.chainB).BankKeeper.GetBalance(s.chainB.GetContext(), s.chainB.SenderAccount.GetAddress(), denom.IBCDenom()))
}