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())) }