mukan-ibc/modules/apps/transfer/keeper/relay_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

1262 lines
44 KiB
Go

package keeper_test
import (
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
sdkmath "cosmossdk.io/math"
"git.cw.tr/mukan-network/mukan-sdk/crypto/keys/secp256k1"
sdk "git.cw.tr/mukan-network/mukan-sdk/types"
sdkerrors "git.cw.tr/mukan-network/mukan-sdk/types/errors"
vestingtypes "git.cw.tr/mukan-network/mukan-sdk/x/auth/vesting/types"
banktestutil "git.cw.tr/mukan-network/mukan-sdk/x/bank/testutil"
banktypes "git.cw.tr/mukan-network/mukan-sdk/x/bank/types"
minttypes "git.cw.tr/mukan-network/mukan-sdk/x/mint/types"
transferkeeper "git.cw.tr/mukan-network/mukan-ibc/modules/apps/transfer/keeper"
"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"
ibcerrors "git.cw.tr/mukan-network/mukan-ibc/modules/core/errors"
ibctesting "git.cw.tr/mukan-network/mukan-ibc/testing"
ibcmock "git.cw.tr/mukan-network/mukan-ibc/testing/mock"
)
var (
zeroAmount = sdkmath.NewInt(0)
defaultAmount = ibctesting.DefaultCoinAmount
)
// TestSendTransfer tests sending from chainA to chainB using both coin
// that originate on chainA and coin that originate on chainB.
func (suite *KeeperTestSuite) TestSendTransfer() {
var (
coin sdk.Coin
path *ibctesting.Path
sender sdk.AccAddress
memo string
expEscrowAmount sdkmath.Int // total amounts in escrow for denom on receiving chain
)
testCases := []struct {
name string
malleate func()
expError error
}{
{
"success: transfer of native token",
func() {},
nil,
},
{
"success: transfer of native token with memo",
func() {
memo = "memo" //nolint:goconst
},
nil,
},
{
"success: transfer of IBC token",
func() {
// send IBC token back to chainB
denom := types.NewDenom(ibctesting.TestCoin.Denom, types.NewHop(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID))
coin = sdk.NewCoin(denom.IBCDenom(), ibctesting.TestCoin.Amount)
expEscrowAmount = zeroAmount
},
nil,
},
{
"success: transfer of IBC token with memo",
func() {
// send IBC token back to chainB
denom := types.NewDenom(ibctesting.TestCoin.Denom, types.NewHop(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID))
coin = sdk.NewCoin(denom.IBCDenom(), ibctesting.TestCoin.Amount)
memo = "memo"
expEscrowAmount = zeroAmount
},
nil,
},
{
"success: transfer of entire balance",
func() {
coin = sdk.NewCoin(coin.Denom, types.UnboundedSpendLimit())
var ok bool
expEscrowAmount, ok = sdkmath.NewIntFromString(ibctesting.DefaultGenesisAccBalance)
suite.Require().True(ok)
},
nil,
},
{
"success: transfer of entire spendable balance with vesting account",
func() {
// create vesting account
vestingAccPrivKey := secp256k1.GenPrivKey()
vestingAccAddress := sdk.AccAddress(vestingAccPrivKey.PubKey().Address())
vestingCoins := sdk.NewCoins(sdk.NewCoin(coin.Denom, ibctesting.DefaultCoinAmount))
_, err := suite.chainA.SendMsgs(vestingtypes.NewMsgCreateVestingAccount(
suite.chainA.SenderAccount.GetAddress(),
vestingAccAddress,
vestingCoins,
suite.chainA.GetContext().BlockTime().Add(time.Hour).Unix(),
false,
))
suite.Require().NoError(err)
sender = vestingAccAddress
// transfer some spendable coins to vesting account
transferCoin := sdk.NewCoin(coin.Denom, sdkmath.NewInt(42))
_, err = suite.chainA.SendMsgs(banktypes.NewMsgSend(suite.chainA.SenderAccount.GetAddress(), vestingAccAddress, sdk.NewCoins(transferCoin)))
suite.Require().NoError(err)
coin = sdk.NewCoin(coin.Denom, types.UnboundedSpendLimit())
expEscrowAmount = transferCoin.Amount
},
nil,
},
{
"failure: no spendable coins for vesting account",
func() {
// create vesting account
vestingAccPrivKey := secp256k1.GenPrivKey()
vestingAccAddress := sdk.AccAddress(vestingAccPrivKey.PubKey().Address())
vestingCoin := sdk.NewCoin(coin.Denom, ibctesting.DefaultCoinAmount)
_, err := suite.chainA.SendMsgs(vestingtypes.NewMsgCreateVestingAccount(
suite.chainA.SenderAccount.GetAddress(),
vestingAccAddress,
sdk.NewCoins(vestingCoin),
suite.chainA.GetContext().BlockTime().Add(time.Hour).Unix(),
false,
))
suite.Require().NoError(err)
sender = vestingAccAddress
// just to prove that the vesting account has a balance (but not spendable)
vestingAccBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), vestingAccAddress, coin.Denom)
suite.Require().Equal(vestingCoin.Amount.Int64(), vestingAccBalance.Amount.Int64())
vestinSpendableBalance := suite.chainA.GetSimApp().BankKeeper.SpendableCoins(suite.chainA.GetContext(), vestingAccAddress)
suite.Require().Zero(vestinSpendableBalance.AmountOf(coin.Denom).Int64())
coin = sdk.NewCoin(coin.Denom, types.UnboundedSpendLimit())
},
types.ErrInvalidAmount,
},
{
"failure: sender account is blocked",
func() {
sender = suite.chainA.GetSimApp().AccountKeeper.GetModuleAddress(minttypes.ModuleName)
},
ibcerrors.ErrUnauthorized,
},
{
"failure: bank send from sender account failed, insufficient balance",
func() {
coin = sdk.NewCoin("randomdenom", defaultAmount)
},
sdkerrors.ErrInsufficientFunds,
},
{
"failure: denom trace not found",
func() {
denom := types.NewDenom("randomdenom", types.NewHop(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID))
coin = sdk.NewCoin(denom.IBCDenom(), ibctesting.TestCoin.Amount)
},
types.ErrDenomNotFound,
},
{
"failure: bank send from module account failed, insufficient balance",
func() {
denom := types.NewDenom(ibctesting.TestCoin.Denom, types.NewHop(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID))
coin = sdk.NewCoin(denom.IBCDenom(), ibctesting.TestCoin.Amount.Add(sdkmath.NewInt(1)))
},
sdkerrors.ErrInsufficientFunds,
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest() // reset
path = ibctesting.NewTransferPath(suite.chainA, suite.chainB)
path.Setup()
// create IBC token on chainA
transferMsg := types.NewMsgTransfer(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, ibctesting.TestCoin, suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String(), suite.chainA.GetTimeoutHeight(), 0, "")
result, err := suite.chainB.SendMsgs(transferMsg)
suite.Require().NoError(err) // message committed
packet, err := ibctesting.ParseV1PacketFromEvents(result.Events)
suite.Require().NoError(err)
err = path.RelayPacket(packet)
suite.Require().NoError(err)
// Value that can malleated for Transfer we are testing.
coin = ibctesting.TestCoin
sender = suite.chainA.SenderAccount.GetAddress()
memo = ""
expEscrowAmount = defaultAmount
tc.malleate()
msg := types.NewMsgTransfer(
path.EndpointA.ChannelConfig.PortID,
path.EndpointA.ChannelID,
coin,
sender.String(),
suite.chainB.SenderAccount.GetAddress().String(),
suite.chainB.GetTimeoutHeight(), 0, // only use timeout height
memo,
)
res, err := suite.chainA.GetSimApp().TransferKeeper.Transfer(suite.chainA.GetContext(), msg)
if tc.expError == nil {
suite.Require().NoError(err)
suite.Require().NotNil(res)
} else {
suite.Require().Nil(res)
suite.Require().Error(err)
suite.Require().ErrorIs(err, tc.expError)
// We do not expect escrowed amounts in error cases.
expEscrowAmount = zeroAmount
}
// Assert amounts escrowed are as expected.
suite.assertEscrowEqual(suite.chainA, coin, expEscrowAmount)
})
}
}
func (suite *KeeperTestSuite) TestSendTransferSetsTotalEscrowAmountForSourceIBCToken() {
/*
Given the following flow of tokens:
chain A (channel 0) -> (channel-0) chain B (channel-1) -> (channel-1) chain A
stake transfer/channel-0/stake transfer/channel-1/transfer/channel-0/stake
^
|
SendTransfer
This test will transfer vouchers of denom "transfer/channel-0/stake" from chain B
to chain A over channel-1 to assert that total escrow amount is stored on chain B
for vouchers of denom "transfer/channel-0/stake" because chain B acts as source
Set up:
- Two transfer channels between chain A and chain B (channel-0 and channel-1).
- Tokens of native denom "stake" on chain A transferred to chain B over channel-0
and vouchers minted with denom trace "transfer/channel-0/stake".
Execute:
- Transfer vouchers of denom trace "transfer/channel-0/stake" from chain B to chain A
over channel-1.
Assert:
- The vouchers are not of a native denom (because they are of an IBC denom), but chain B
is the source, then the value for total escrow amount should still be stored for the IBC
denom that corresponds to the trace "transfer/channel-0/stake".
*/
// set up
// 2 transfer channels between chain A and chain B
path1 := ibctesting.NewTransferPath(suite.chainA, suite.chainB)
path1.Setup()
path2 := ibctesting.NewTransferPath(suite.chainA, suite.chainB)
path2.Setup()
// create IBC token on chain B with denom trace "transfer/channel-0/stake"
coin := ibctesting.TestCoin
transferMsg := types.NewMsgTransfer(
path1.EndpointA.ChannelConfig.PortID,
path1.EndpointA.ChannelID,
coin,
suite.chainA.SenderAccount.GetAddress().String(),
suite.chainB.SenderAccount.GetAddress().String(),
suite.chainB.GetTimeoutHeight(), 0, "",
)
result, err := suite.chainA.SendMsgs(transferMsg)
suite.Require().NoError(err) // message committed
packet, err := ibctesting.ParseV1PacketFromEvents(result.Events)
suite.Require().NoError(err)
err = path1.RelayPacket(packet)
suite.Require().NoError(err)
// execute
denom := types.NewDenom(sdk.DefaultBondDenom, types.NewHop(path1.EndpointB.ChannelConfig.PortID, path1.EndpointB.ChannelID))
coin = sdk.NewCoin(denom.IBCDenom(), defaultAmount)
msg := types.NewMsgTransfer(
path2.EndpointB.ChannelConfig.PortID,
path2.EndpointB.ChannelID,
coin,
suite.chainB.SenderAccount.GetAddress().String(),
suite.chainA.SenderAccount.GetAddress().String(),
suite.chainA.GetTimeoutHeight(), 0, "",
)
res, err := suite.chainB.GetSimApp().TransferKeeper.Transfer(suite.chainB.GetContext(), msg)
suite.Require().NoError(err)
suite.Require().NotNil(res)
// check total amount in escrow of sent token on sending chain
totalEscrow := suite.chainB.GetSimApp().TransferKeeper.GetTotalEscrowForDenom(suite.chainB.GetContext(), coin.GetDenom())
suite.Require().Equal(defaultAmount, totalEscrow.Amount)
}
// TestOnRecvPacket_ReceiverIsNotSource tests receiving on chainB a coin that
// originates on chainA. The bulk of the testing occurs in the test case for
// loop since setup is intensive for all cases. The malleate function allows
// for testing invalid cases.
func (suite *KeeperTestSuite) TestOnRecvPacket_ReceiverIsNotSource() {
var packetData types.InternalTransferRepresentation
testCases := []struct {
msg string
malleate func()
expError error
}{
{
"success: receive",
func() {},
nil,
},
{
"success: receive with memo",
func() {
packetData.Memo = "memo"
},
nil,
},
{
"success: receive with hex receiver address",
func() {
suite.chainB.GetSimApp().TransferKeeper.SetAddressCodec(ibcmock.TestAddressCodec{})
receiver := sdk.MustAccAddressFromBech32(packetData.Receiver)
packetData.Receiver = hex.EncodeToString(receiver.Bytes())
},
nil,
},
{
"failure: mint zero coin",
func() {
packetData.Token.Amount = zeroAmount.String()
},
types.ErrInvalidAmount,
},
{
"failure: receiver is module account",
func() {
packetData.Receiver = suite.chainB.GetSimApp().AccountKeeper.GetModuleAddress(minttypes.ModuleName).String()
},
ibcerrors.ErrUnauthorized,
},
{
"failure: receiver is invalid",
func() {
packetData.Receiver = "invalid-address"
},
ibcerrors.ErrInvalidAddress,
},
{
"failure: receive is disabled",
func() {
suite.chainB.GetSimApp().TransferKeeper.SetParams(suite.chainB.GetContext(),
types.Params{
ReceiveEnabled: false,
})
},
types.ErrReceiveDisabled,
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
path := ibctesting.NewTransferPath(suite.chainA, suite.chainB)
path.Setup()
receiver := suite.chainB.SenderAccount.GetAddress().String() // must be explicitly changed in malleate
// send coins from chainA to chainB
transferMsg := types.NewMsgTransfer(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, ibctesting.TestCoin, suite.chainA.SenderAccount.GetAddress().String(), receiver, clienttypes.NewHeight(1, 110), 0, "")
_, err := suite.chainA.SendMsgs(transferMsg)
suite.Require().NoError(err) // message committed
token := types.Token{Denom: types.NewDenom(transferMsg.Token.Denom), Amount: transferMsg.Token.Amount.String()}
packetData = types.NewInternalTransferRepresentation(token, suite.chainA.SenderAccount.GetAddress().String(), receiver, "")
sourcePort := path.EndpointA.ChannelConfig.PortID
sourceChannel := path.EndpointA.ChannelID
destinationPort := path.EndpointB.ChannelConfig.PortID
destinationChannel := path.EndpointB.ChannelID
tc.malleate()
denom := types.NewDenom(token.Denom.Base, types.NewHop(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID))
err = suite.chainB.GetSimApp().TransferKeeper.OnRecvPacket(
suite.chainB.GetContext(),
packetData,
sourcePort,
sourceChannel,
destinationPort,
destinationChannel,
)
if tc.expError == nil {
suite.Require().NoError(err)
// Check denom metadata for of tokens received on chain B.
actualMetadata, found := suite.chainB.GetSimApp().BankKeeper.GetDenomMetaData(suite.chainB.GetContext(), denom.IBCDenom())
suite.Require().True(found)
suite.Require().Equal(metadataFromDenom(denom), actualMetadata)
} else {
suite.Require().Error(err)
suite.Require().ErrorIs(err, tc.expError)
// Check denom metadata absence for cases where recv fails.
_, found := suite.chainB.GetSimApp().BankKeeper.GetDenomMetaData(suite.chainB.GetContext(), denom.IBCDenom())
suite.Require().False(found)
}
})
}
}
// TestOnRecvPacket_ReceiverIsSource tests receiving on chainB a coin that
// originated on chainB, but was previously transferred to chainA. The bulk
// of the testing occurs in the test case for loop since setup is intensive
// for all cases. The malleate function allows for testing invalid cases.
func (suite *KeeperTestSuite) TestOnRecvPacket_ReceiverIsSource() {
var (
packetData types.InternalTransferRepresentation
expEscrowAmount sdkmath.Int // total amount in escrow for denom on receiving chain
)
testCases := []struct {
msg string
malleate func()
expError error
}{
{
"successful receive",
func() {},
nil,
},
{
"successful receive with memo",
func() {
packetData.Memo = "memo"
},
nil,
},
{
"successful receive of half the amount",
func() {
packetData.Token.Amount = sdkmath.NewInt(50).String()
// expect 50 remaining
expEscrowAmount = sdkmath.NewInt(50)
},
nil,
},
{
"failure: empty coin",
func() {
packetData.Token.Amount = zeroAmount.String()
},
types.ErrInvalidAmount,
},
{
"failure: tries to unescrow more tokens than allowed",
func() {
packetData.Token.Amount = sdkmath.NewInt(1000000).String()
},
sdkerrors.ErrInsufficientFunds,
},
{
"failure: empty denom",
func() {
packetData.Token.Denom = types.Denom{}
},
types.ErrInvalidDenomForTransfer,
},
{
"failure: invalid receiver address",
func() {
packetData.Receiver = "gaia1scqhwpgsmr6vmztaa7suurfl52my6nd2kmrudl"
},
errors.New("failed to decode receiver address"),
},
{
"failure: receiver is module account",
func() {
packetData.Receiver = suite.chainB.GetSimApp().AccountKeeper.GetModuleAddress(minttypes.ModuleName).String()
},
ibcerrors.ErrUnauthorized,
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
path := ibctesting.NewTransferPath(suite.chainA, suite.chainB)
path.Setup()
receiver := suite.chainB.SenderAccount.GetAddress().String() // must be explicitly changed in malleate
expEscrowAmount = zeroAmount // total amount in escrow of voucher denom on receiving chain
// send coins from chainA to chainB, receive them, acknowledge them
transferMsg := types.NewMsgTransfer(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, ibctesting.TestCoin, suite.chainA.SenderAccount.GetAddress().String(), receiver, clienttypes.NewHeight(1, 110), 0, "")
_, err := suite.chainA.SendMsgs(transferMsg)
suite.Require().NoError(err) // message committed
token := types.Token{Denom: types.NewDenom(transferMsg.Token.Denom, types.NewHop(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID)), Amount: transferMsg.Token.Amount.String()}
packetData = types.NewInternalTransferRepresentation(token, suite.chainA.SenderAccount.GetAddress().String(), receiver, "")
sourcePort := path.EndpointB.ChannelConfig.PortID
sourceChannel := path.EndpointB.ChannelID
destinationPort := path.EndpointA.ChannelConfig.PortID
destinationChannel := path.EndpointA.ChannelID
tc.malleate()
err = suite.chainA.GetSimApp().TransferKeeper.OnRecvPacket(
suite.chainA.GetContext(),
packetData,
sourcePort,
sourceChannel,
destinationPort,
destinationChannel,
)
if tc.expError == nil {
suite.Require().NoError(err)
_, found := suite.chainA.GetSimApp().BankKeeper.GetDenomMetaData(suite.chainA.GetContext(), sdk.DefaultBondDenom)
suite.Require().False(found)
} else {
suite.Require().Error(err)
suite.Require().ErrorContains(err, tc.expError.Error())
// Expect escrowed amount to stay same on failure.
expEscrowAmount = defaultAmount
}
// Assert amounts escrowed are as expected, we do not malleate amount escrowed in initial transfer.
suite.assertEscrowEqual(suite.chainA, ibctesting.TestCoin, expEscrowAmount)
})
}
}
func (suite *KeeperTestSuite) TestOnRecvPacketSetsTotalEscrowAmountForSourceIBCToken() {
/*
Given the following flow of tokens:
chain A (channel 0) -> (channel-0) chain B (channel-1) -> (channel-1) chain A (channel-1) -> (channel-1) chain B
stake transfer/channel-0/stake transfer/channel-1/transfer/channel-0/stake transfer/channel-0/stake
^
|
OnRecvPacket
This test will assert that on receiving vouchers of denom "transfer/channel-0/stake"
on chain B the total escrow amount is updated on because chain B acted as source
when vouchers were transferred to chain A over channel-1.
Setup:
- Two transfer channels between chain A and chain B.
- Vouchers of denom trace "transfer/channel-0/stake" on chain B are in escrow
account for port ID transfer and channel ID channel-1.
Execute:
- Receive vouchers of denom trace "transfer/channel-0/stake" from chain A to chain B
over channel-1.
Assert:
- The vouchers are not of a native denom (because they are of an IBC denom), but chain B
is the source, then the value for total escrow amount should still be updated for the IBC
denom that corresponds to the trace "transfer/channel-0/stake" when the vouchers are
received back on chain B.
*/
amount := defaultAmount
// setup
// 2 transfer channels between chain A and chain B
path1 := ibctesting.NewTransferPath(suite.chainA, suite.chainB)
path1.Setup()
path2 := ibctesting.NewTransferPath(suite.chainA, suite.chainB)
path2.Setup()
// denom path: {transfer/channel-1/transfer/channel-0}
denom := types.NewDenom(
sdk.DefaultBondDenom,
types.NewHop(path2.EndpointA.ChannelConfig.PortID, path2.EndpointA.ChannelID),
types.NewHop(path1.EndpointB.ChannelConfig.PortID, path1.EndpointB.ChannelID),
)
data := types.NewInternalTransferRepresentation(
types.Token{
Denom: denom,
Amount: amount.String(),
}, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), "")
sourcePort := path2.EndpointA.ChannelConfig.PortID
sourceChannel := path2.EndpointA.ChannelID
destinationPort := path2.EndpointB.ChannelConfig.PortID
destinationChannel := path2.EndpointB.ChannelID
// fund escrow account for transfer and channel-1 on chain B
// denom path: transfer/channel-0
denom = types.NewDenom(
sdk.DefaultBondDenom,
types.NewHop(path1.EndpointB.ChannelConfig.PortID, path1.EndpointB.ChannelID),
)
escrowAddress := types.GetEscrowAddress(path2.EndpointB.ChannelConfig.PortID, path2.EndpointB.ChannelID)
coin := sdk.NewCoin(denom.IBCDenom(), amount)
suite.Require().NoError(
banktestutil.FundAccount(
suite.chainB.GetContext(),
suite.chainB.GetSimApp().BankKeeper,
escrowAddress,
sdk.NewCoins(coin),
),
)
suite.chainB.GetSimApp().TransferKeeper.SetTotalEscrowForDenom(suite.chainB.GetContext(), coin)
totalEscrowChainB := suite.chainB.GetSimApp().TransferKeeper.GetTotalEscrowForDenom(suite.chainB.GetContext(), coin.GetDenom())
suite.Require().Equal(defaultAmount, totalEscrowChainB.Amount)
// execute onRecvPacket, when chaninB receives the source token the escrow amount should decrease
err := suite.chainB.GetSimApp().TransferKeeper.OnRecvPacket(
suite.chainB.GetContext(),
data,
sourcePort,
sourceChannel,
destinationPort,
destinationChannel,
)
suite.Require().NoError(err)
// check total amount in escrow of sent token on receiving chain
totalEscrowChainB = suite.chainB.GetSimApp().TransferKeeper.GetTotalEscrowForDenom(suite.chainB.GetContext(), coin.GetDenom())
suite.Require().Equal(zeroAmount, totalEscrowChainB.Amount)
}
// TestOnAcknowledgementPacket tests that successful acknowledgement is a no-op
// and failure acknowledment leads to refund when attempting to send from chainA
// to chainB. If sender is source then the denomination being refunded has no
// trace.
func (suite *KeeperTestSuite) TestOnAcknowledgementPacket() {
var (
successAck = channeltypes.NewResultAcknowledgement([]byte{byte(1)})
failedAck = channeltypes.NewErrorAcknowledgement(errors.New("failed packet transfer"))
denom types.Denom
amount sdkmath.Int
path *ibctesting.Path
expEscrowAmount sdkmath.Int
)
testCases := []struct {
msg string
ack channeltypes.Acknowledgement
malleate func()
expError error
}{
{
"success ack: no-op",
successAck,
func() {
denom = types.NewDenom(sdk.DefaultBondDenom, types.NewHop(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID))
},
nil,
},
{
"failed ack: successful refund of native coin",
failedAck,
func() {
escrow := types.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)
denom = types.NewDenom(sdk.DefaultBondDenom)
coin := sdk.NewCoin(sdk.DefaultBondDenom, amount)
suite.Require().NoError(banktestutil.FundAccount(suite.chainA.GetContext(), suite.chainA.GetSimApp().BankKeeper, escrow, sdk.NewCoins(coin)))
// set escrow amount that would have been stored after successful execution of MsgTransfer
suite.chainA.GetSimApp().TransferKeeper.SetTotalEscrowForDenom(suite.chainA.GetContext(), sdk.NewCoin(sdk.DefaultBondDenom, amount))
},
nil,
},
{
"failed ack: successful refund of IBC voucher",
failedAck,
func() {
escrow := types.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)
denom = types.NewDenom(sdk.DefaultBondDenom, types.NewHop(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID))
coin := sdk.NewCoin(denom.IBCDenom(), amount)
suite.Require().NoError(banktestutil.FundAccount(suite.chainA.GetContext(), suite.chainA.GetSimApp().BankKeeper, escrow, sdk.NewCoins(coin)))
},
nil,
},
{
"failed ack: funds cannot be refunded because escrow account has zero balance",
failedAck,
func() {
denom = types.NewDenom(sdk.DefaultBondDenom)
// set escrow amount that would have been stored after successful execution of MsgTransfer
suite.chainA.GetSimApp().TransferKeeper.SetTotalEscrowForDenom(suite.chainA.GetContext(), sdk.NewCoin(sdk.DefaultBondDenom, amount))
expEscrowAmount = defaultAmount
},
sdkerrors.ErrInsufficientFunds,
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
path = ibctesting.NewTransferPath(suite.chainA, suite.chainB)
path.Setup()
amount = defaultAmount // must be explicitly changed
expEscrowAmount = zeroAmount
tc.malleate()
data := types.NewInternalTransferRepresentation(
types.Token{
Denom: denom,
Amount: amount.String(),
}, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), "")
sourcePort := path.EndpointA.ChannelConfig.PortID
sourceChannel := path.EndpointA.ChannelID
preAcknowledgementBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), denom.IBCDenom())
err := suite.chainA.GetSimApp().TransferKeeper.OnAcknowledgementPacket(suite.chainA.GetContext(), sourcePort, sourceChannel, data, tc.ack)
// check total amount in escrow of sent token denom on sending chain
totalEscrow := suite.chainA.GetSimApp().TransferKeeper.GetTotalEscrowForDenom(suite.chainA.GetContext(), denom.IBCDenom())
suite.Require().Equal(expEscrowAmount, totalEscrow.Amount)
if tc.expError == nil {
suite.Require().NoError(err)
postAcknowledgementBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), denom.IBCDenom())
deltaAmount := postAcknowledgementBalance.Amount.Sub(preAcknowledgementBalance.Amount)
if tc.ack.Success() {
suite.Require().Equal(int64(0), deltaAmount.Int64(), "successful ack changed balance")
} else {
suite.Require().Equal(amount, deltaAmount, "failed ack did not trigger refund")
}
} else {
suite.Require().Error(err)
suite.Require().ErrorIs(err, tc.expError)
}
})
}
}
func (suite *KeeperTestSuite) TestOnAcknowledgementPacketSetsTotalEscrowAmountForSourceIBCToken() {
/*
This test is testing the following scenario. Given tokens travelling like this:
chain A (channel 0) -> (channel-0) chain B (channel-1) -> (channel-1) chain A (channel-1)
stake transfer/channel-0/stake transfer/channel-1/transfer/channel-0/stake
^
|
OnAcknowledgePacket
We want to assert that on failed acknowledgment of vouchers sent with denom trace
"transfer/channel-0/stake" on chain B the total escrow amount is updated.
Set up:
- Two transfer channels between chain A and chain B.
- Vouckers of denom "transfer/channel-0/stake" on chain B are in escrow
account for port ID transfer and channel ID channel-1.
Execute:
- Acknowledge vouchers of denom trace "transfer/channel-0/stake" sent from chain B
to chain B over channel-1.
Assert:
- The vouchers are not of a native denom (because they are of an IBC denom), but chain B
is the source, then the value for total escrow amount should still be updated for the IBC
denom that corresponds to the trace "transfer/channel-0/stake" when processing the failed
acknowledgement.
*/
amount := defaultAmount
ack := channeltypes.NewErrorAcknowledgement(errors.New("failed packet transfer"))
// set up
// 2 transfer channels between chain A and chain B
path1 := ibctesting.NewTransferPath(suite.chainA, suite.chainB)
path1.Setup()
path2 := ibctesting.NewTransferPath(suite.chainA, suite.chainB)
path2.Setup()
// fund escrow account for transfer and channel-1 on chain B
// denom path: transfer/channel-0
denom := types.NewDenom(sdk.DefaultBondDenom, types.NewHop(path1.EndpointB.ChannelConfig.PortID, path1.EndpointB.ChannelID))
escrowAddress := types.GetEscrowAddress(path2.EndpointB.ChannelConfig.PortID, path2.EndpointB.ChannelID)
coin := sdk.NewCoin(denom.IBCDenom(), amount)
suite.Require().NoError(
banktestutil.FundAccount(
suite.chainB.GetContext(),
suite.chainB.GetSimApp().BankKeeper,
escrowAddress,
sdk.NewCoins(coin),
),
)
data := types.NewInternalTransferRepresentation(
types.Token{
Denom: denom,
Amount: amount.String(),
},
suite.chainB.SenderAccount.GetAddress().String(),
suite.chainA.SenderAccount.GetAddress().String(),
"",
)
sourcePort := path2.EndpointB.ChannelConfig.PortID
sourceChannel := path2.EndpointB.ChannelID
suite.chainB.GetSimApp().TransferKeeper.SetTotalEscrowForDenom(suite.chainB.GetContext(), coin)
totalEscrowChainB := suite.chainB.GetSimApp().TransferKeeper.GetTotalEscrowForDenom(suite.chainB.GetContext(), coin.GetDenom())
suite.Require().Equal(defaultAmount, totalEscrowChainB.Amount)
err := suite.chainB.GetSimApp().TransferKeeper.OnAcknowledgementPacket(suite.chainB.GetContext(), sourcePort, sourceChannel, data, ack)
suite.Require().NoError(err)
// check total amount in escrow of sent token on sending chain
totalEscrowChainB = suite.chainB.GetSimApp().TransferKeeper.GetTotalEscrowForDenom(suite.chainB.GetContext(), coin.GetDenom())
suite.Require().Equal(zeroAmount, totalEscrowChainB.Amount)
}
// TestOnTimeoutPacket tests private refundPacket function since it is a simple
// wrapper over it. The actual timeout does not matter since IBC core logic
// is not being tested. The test is timing out a send from chainA to chainB
// so the refunds are occurring on chainA.
func (suite *KeeperTestSuite) TestOnTimeoutPacket() {
var (
path *ibctesting.Path
amount string
sender string
denom types.Denom
expEscrowAmount sdkmath.Int
)
testCases := []struct {
msg string
malleate func()
expError error
}{
{
"successful timeout: sender is source of coin",
func() {
escrow := types.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)
denom = types.NewDenom(sdk.DefaultBondDenom)
coinAmount, ok := sdkmath.NewIntFromString(amount)
suite.Require().True(ok)
coin := sdk.NewCoin(denom.IBCDenom(), coinAmount)
expEscrowAmount = zeroAmount
// funds the escrow account to have balance
suite.Require().NoError(banktestutil.FundAccount(suite.chainA.GetContext(), suite.chainA.GetSimApp().BankKeeper, escrow, sdk.NewCoins(coin)))
// set escrow amount that would have been stored after successful execution of MsgTransfer
suite.chainA.GetSimApp().TransferKeeper.SetTotalEscrowForDenom(suite.chainA.GetContext(), coin)
},
nil,
},
{
"successful timeout: sender is not source of coin",
func() {
escrow := types.GetEscrowAddress(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)
denom = types.NewDenom(sdk.DefaultBondDenom, types.NewHop(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID))
coinAmount, ok := sdkmath.NewIntFromString(amount)
suite.Require().True(ok)
coin := sdk.NewCoin(denom.IBCDenom(), coinAmount)
expEscrowAmount = zeroAmount
// funds the escrow account to have balance
suite.Require().NoError(banktestutil.FundAccount(suite.chainA.GetContext(), suite.chainA.GetSimApp().BankKeeper, escrow, sdk.NewCoins(coin)))
},
nil,
},
{
"failure: funds cannot be refunded because escrow account has no balance for non-native coin",
func() {
denom = types.NewDenom("bitcoin")
var ok bool
expEscrowAmount, ok = sdkmath.NewIntFromString(amount)
suite.Require().True(ok)
// set escrow amount that would have been stored after successful execution of MsgTransfer
suite.chainA.GetSimApp().TransferKeeper.SetTotalEscrowForDenom(suite.chainA.GetContext(), sdk.NewCoin(denom.IBCDenom(), expEscrowAmount))
},
sdkerrors.ErrInsufficientFunds,
},
{
"failure: funds cannot be refunded because escrow account has no balance for native coin",
func() {
denom = types.NewDenom(sdk.DefaultBondDenom)
var ok bool
expEscrowAmount, ok = sdkmath.NewIntFromString(amount)
suite.Require().True(ok)
// set escrow amount that would have been stored after successful execution of MsgTransfer
suite.chainA.GetSimApp().TransferKeeper.SetTotalEscrowForDenom(suite.chainA.GetContext(), sdk.NewCoin(denom.IBCDenom(), expEscrowAmount))
},
sdkerrors.ErrInsufficientFunds,
},
{
"failure: cannot mint because sender address is invalid",
func() {
denom = types.NewDenom(sdk.DefaultBondDenom)
amount = sdkmath.OneInt().String()
sender = "invalid address"
},
errors.New("decoding bech32 failed"),
},
{
"failure: invalid amount",
func() {
denom = types.NewDenom(sdk.DefaultBondDenom)
amount = "invalid"
},
types.ErrInvalidAmount,
},
}
for _, tc := range testCases {
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() {
suite.SetupTest() // reset
path = ibctesting.NewTransferPath(suite.chainA, suite.chainB)
path.Setup()
amount = defaultAmount.String() // must be explicitly changed
sender = suite.chainA.SenderAccount.GetAddress().String()
expEscrowAmount = zeroAmount
tc.malleate()
data := types.NewInternalTransferRepresentation(
types.Token{
Denom: denom,
Amount: amount,
}, sender, suite.chainB.SenderAccount.GetAddress().String(), "")
sourcePort := path.EndpointA.ChannelConfig.PortID
sourceChannel := path.EndpointA.ChannelID
preTimeoutBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), denom.IBCDenom())
err := suite.chainA.GetSimApp().TransferKeeper.OnTimeoutPacket(suite.chainA.GetContext(), sourcePort, sourceChannel, data)
postTimeoutBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), denom.IBCDenom())
deltaAmount := postTimeoutBalance.Amount.Sub(preTimeoutBalance.Amount)
// check total amount in escrow of sent token denom on sending chain
totalEscrow := suite.chainA.GetSimApp().TransferKeeper.GetTotalEscrowForDenom(suite.chainA.GetContext(), denom.IBCDenom())
suite.Require().Equal(expEscrowAmount, totalEscrow.Amount)
if tc.expError == nil {
suite.Require().NoError(err)
amountParsed, ok := sdkmath.NewIntFromString(amount)
suite.Require().True(ok)
suite.Require().Equal(amountParsed, deltaAmount, "successful timeout did not trigger refund")
} else {
suite.Require().Error(err)
suite.Require().ErrorContains(err, tc.expError.Error())
}
})
}
}
func (suite *KeeperTestSuite) TestOnTimeoutPacketSetsTotalEscrowAmountForSourceIBCToken() {
/*
Given the following flow of tokens:
chain A (channel 0) -> (channel-0) chain B (channel-1) -> (channel-1) chain A (channel-1)
stake transfer/channel-0/stake transfer/channel-1/transfer/channel-0/stake
^
|
OnTimeoutPacket
We want to assert that on timeout of vouchers sent with denom trace
"transfer/channel-0/stake" on chain B the total escrow amount is updated.
Set up:
- Two transfer channels between chain A and chain B.
- Vouckers of denom "transfer/channel-0/stake" on chain B are in escrow
account for port ID transfer and channel ID channel-1.
Execute:
- Timeout vouchers of denom trace "transfer/channel-0/stake" sent from chain B
to chain B over channel-1.
Assert:
- The vouchers are not of a native denom (because they are of an IBC denom), but chain B
is the source, then the value for total escrow amount should still be updated for the IBC
denom that corresponds to the trace "transfer/channel-0/stake" when processing the timeout.
*/
amount := defaultAmount
// set up
// 2 transfer channels between chain A and chain B
path1 := ibctesting.NewTransferPath(suite.chainA, suite.chainB)
path1.Setup()
path2 := ibctesting.NewTransferPath(suite.chainA, suite.chainB)
path2.Setup()
// fund escrow account for transfer and channel-1 on chain B
denom := types.NewDenom(sdk.DefaultBondDenom, types.NewHop(path1.EndpointB.ChannelConfig.PortID, path1.EndpointB.ChannelID))
escrowAddress := types.GetEscrowAddress(path2.EndpointB.ChannelConfig.PortID, path2.EndpointB.ChannelID)
coin := sdk.NewCoin(denom.IBCDenom(), amount)
suite.Require().NoError(
banktestutil.FundAccount(
suite.chainB.GetContext(),
suite.chainB.GetSimApp().BankKeeper,
escrowAddress,
sdk.NewCoins(coin),
),
)
data := types.NewInternalTransferRepresentation(
types.Token{
Denom: denom,
Amount: amount.String(),
}, suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String(), "")
sourcePort := path2.EndpointB.ChannelConfig.PortID
sourceChannel := path2.EndpointB.ChannelID
suite.chainB.GetSimApp().TransferKeeper.SetTotalEscrowForDenom(suite.chainB.GetContext(), coin)
totalEscrowChainB := suite.chainB.GetSimApp().TransferKeeper.GetTotalEscrowForDenom(suite.chainB.GetContext(), coin.GetDenom())
suite.Require().Equal(defaultAmount, totalEscrowChainB.Amount)
err := suite.chainB.GetSimApp().TransferKeeper.OnTimeoutPacket(suite.chainB.GetContext(), sourcePort, sourceChannel, data)
suite.Require().NoError(err)
// check total amount in escrow of sent token on sending chain
totalEscrowChainB = suite.chainB.GetSimApp().TransferKeeper.GetTotalEscrowForDenom(suite.chainB.GetContext(), coin.GetDenom())
suite.Require().Equal(zeroAmount, totalEscrowChainB.Amount)
}
func (suite *KeeperTestSuite) TestPacketForwardsCompatibility() {
// We are testing a scenario where a packet in the future has a new populated
// field called "new_field". And this packet is being sent to this module which
// doesn't have this field in the packet data. The module should be able to handle
// this packet without any issues.
// the test also ensures that an ack is written for any malformed or bad packet data.
var packetData []byte
var path *ibctesting.Path
testCases := []struct {
msg string
malleate func()
expError error
expAckError error
}{
{
"success: no new field with memo",
func() {
jsonString := fmt.Sprintf(`{"denom":"denom","amount":"100","sender":"%s","receiver":"%s","memo":"memo"}`, suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String())
packetData = []byte(jsonString)
},
nil,
nil,
},
{
"success: no new field without memo",
func() {
jsonString := fmt.Sprintf(`{"denom":"denom","amount":"100","sender":"%s","receiver":"%s"}`, suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String())
packetData = []byte(jsonString)
},
nil,
nil,
},
{
"failure: invalid packet data",
func() {
packetData = []byte("invalid packet data")
},
ibcerrors.ErrInvalidType,
ibcerrors.ErrInvalidType,
},
{
"failure: new field",
func() {
jsonString := fmt.Sprintf(`{"denom":"denom","amount":"100","sender":"%s","receiver":"%s","memo":"memo","new_field":"value"}`, suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String())
packetData = []byte(jsonString)
},
ibcerrors.ErrInvalidType,
ibcerrors.ErrInvalidType,
},
{
"failure: missing field",
func() {
jsonString := fmt.Sprintf(`{"amount":"100","sender":%s","receiver":"%s"}`, suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String())
packetData = []byte(jsonString)
},
ibcerrors.ErrInvalidType,
ibcerrors.ErrInvalidType,
},
}
for _, tc := range testCases {
suite.Run(tc.msg, func() {
suite.SetupTest() // reset
packetData = nil
path = ibctesting.NewTransferPath(suite.chainA, suite.chainB)
path.EndpointA.ChannelConfig.Version = types.V1
path.EndpointB.ChannelConfig.Version = types.V1
tc.malleate()
path.Setup()
timeoutHeight := suite.chainB.GetTimeoutHeight()
seq, err := path.EndpointB.SendPacket(timeoutHeight, 0, packetData)
suite.Require().NoError(err)
packet := channeltypes.NewPacket(packetData, seq, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, timeoutHeight, 0)
// receive packet on chainA
err = path.RelayPacket(packet)
if tc.expError == nil {
suite.Require().NoError(err)
} else {
suite.Require().ErrorContains(err, tc.expError.Error())
ackBz, ok := path.EndpointA.Chain.GetSimApp().IBCKeeper.ChannelKeeper.GetPacketAcknowledgement(path.EndpointA.Chain.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, seq)
suite.Require().True(ok)
// an ack should be written for the malformed / bad packet data.
expectedAck := channeltypes.NewErrorAcknowledgement(tc.expAckError)
expBz := channeltypes.CommitAcknowledgement(expectedAck.Acknowledgement())
suite.Require().Equal(expBz, ackBz)
}
})
}
}
func (suite *KeeperTestSuite) TestCreatePacketDataBytesFromVersion() {
var (
token types.Token
sender, receiver string
)
testCases := []struct {
name string
appVersion string
malleate func()
expResult func(bz []byte, err error)
}{
{
"success",
types.V1,
func() {},
func(bz []byte, err error) {
expPacketData := types.NewFungibleTokenPacketData(ibctesting.TestCoin.Denom, ibctesting.TestCoin.Amount.String(), sender, receiver, "")
suite.Require().Equal(bz, expPacketData.GetBytes())
suite.Require().NoError(err)
},
},
{
"failure: version 2",
"ics20-2",
func() {},
func(bz []byte, err error) {
suite.Require().Nil(bz)
suite.Require().Error(err, ibcerrors.ErrInvalidVersion)
},
},
{
"failure: fails v1 validation",
types.V1,
func() {
sender = ""
},
func(bz []byte, err error) {
suite.Require().Nil(bz)
suite.Require().ErrorIs(err, ibcerrors.ErrInvalidAddress)
},
},
{
"failure: invalid version",
ibcmock.Version,
func() {},
func(bz []byte, err error) {
suite.Require().Nil(bz)
suite.Require().ErrorIs(err, types.ErrInvalidVersion)
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
path := ibctesting.NewTransferPath(suite.chainA, suite.chainB)
path.Setup()
token = types.Token{
Amount: ibctesting.TestCoin.Amount.String(),
Denom: types.NewDenom(ibctesting.TestCoin.Denom),
}
sender = suite.chainA.SenderAccount.GetAddress().String()
receiver = suite.chainB.SenderAccount.GetAddress().String()
tc.malleate()
bz, err := transferkeeper.CreatePacketDataBytesFromVersion(tc.appVersion, sender, receiver, "", token)
tc.expResult(bz, err)
})
}
}
// metadataFromDenom creates a banktypes.Metadata from a given types.Denom
func metadataFromDenom(denom types.Denom) banktypes.Metadata {
return banktypes.Metadata{
Description: fmt.Sprintf("IBC token from %s", denom.Path()),
DenomUnits: []*banktypes.DenomUnit{
{
Denom: denom.Base,
Exponent: 0,
},
},
Base: denom.IBCDenom(),
Display: denom.Path(),
Name: fmt.Sprintf("%s IBC token", denom.Path()),
Symbol: strings.ToUpper(denom.Base),
}
}
// assertEscrowEqual asserts that the amounts escrowed for each of the coins on chain matches the expectedAmounts
func (suite *KeeperTestSuite) assertEscrowEqual(chain *ibctesting.TestChain, coin sdk.Coin, expectedAmount sdkmath.Int) {
amount := chain.GetSimApp().TransferKeeper.GetTotalEscrowForDenom(chain.GetContext(), coin.GetDenom())
suite.Require().Equal(expectedAmount, amount.Amount)
}