From a74ffb0cec00768bbb8dbe3fd6413e66388010d3 Mon Sep 17 00:00:00 2001 From: Michael Tsitrin <114929630+mtsitrin@users.noreply.github.com> Date: Thu, 6 Jun 2024 00:22:03 +0300 Subject: [PATCH] feat: bridging fee middleware (#899) --- app/app.go | 6 +- app/apptesting/events.go | 2 +- ibctesting/bridging_fee_test.go | 105 +++++++++++++++++++++++ ibctesting/eibc_test.go | 1 + proto/dymension/delayedack/params.proto | 6 ++ x/bridging_fee/events.go | 6 ++ x/bridging_fee/ibc_middleware.go | 109 ++++++++++++++++++++++++ x/delayedack/eibc.go | 11 ++- x/delayedack/genesis_test.go | 13 +++ x/delayedack/ibc_middleware.go | 37 +------- x/delayedack/keeper/finalize.go | 1 + x/delayedack/keeper/fraud_test.go | 2 +- x/delayedack/keeper/hooks_test.go | 2 +- x/delayedack/keeper/keeper.go | 31 +++++++ x/delayedack/keeper/params.go | 11 ++- x/delayedack/types/params.go | 60 +++++++++++-- x/delayedack/types/params.pb.go | 75 +++++++++++++--- x/eibc/genesis_test.go | 4 +- x/eibc/keeper/hooks_test.go | 4 +- x/eibc/keeper/msg_server.go | 10 +++ x/eibc/keeper/msg_server_test.go | 55 +++++++----- x/eibc/types/errors.go | 1 + x/eibc/types/expected_keepers.go | 1 + 23 files changed, 469 insertions(+), 84 deletions(-) create mode 100644 ibctesting/bridging_fee_test.go create mode 100644 x/bridging_fee/events.go create mode 100644 x/bridging_fee/ibc_middleware.go diff --git a/app/app.go b/app/app.go index ddd2996db..0d5c1ad83 100644 --- a/app/app.go +++ b/app/app.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" + "github.com/dymensionxyz/dymension/v3/x/bridging_fee" vfchooks "github.com/dymensionxyz/dymension/v3/x/vfc/hooks" "github.com/gorilla/mux" @@ -739,9 +740,11 @@ func New( ) transferModule := ibctransfer.NewAppModule(app.TransferKeeper) + transferMiddleware := ibctransfer.NewIBCModule(app.TransferKeeper) var transferStack ibcporttypes.IBCModule - transferStack = ibctransfer.NewIBCModule(app.TransferKeeper) + transferStack = bridging_fee.NewIBCMiddleware(transferMiddleware, app.IBCKeeper.ChannelKeeper, app.DelayedAckKeeper, app.TransferKeeper, app.AccountKeeper.GetModuleAddress(txfeestypes.ModuleName)) + transferStack = packetforwardmiddleware.NewIBCMiddleware( transferStack, app.PacketForwardMiddlewareKeeper, @@ -1025,6 +1028,7 @@ func (app *App) ModuleAccountAddrs() map[string]bool { // exclude the streamer as we want him to be able to get external incentives modAccAddrs[authtypes.NewModuleAddress(streamermoduletypes.ModuleName).String()] = false + modAccAddrs[authtypes.NewModuleAddress(txfeestypes.ModuleName).String()] = false return modAccAddrs } diff --git a/app/apptesting/events.go b/app/apptesting/events.go index cefa71734..91f3bf577 100644 --- a/app/apptesting/events.go +++ b/app/apptesting/events.go @@ -51,6 +51,6 @@ func (s *KeeperTestHelper) ExtractAttributes(event sdk.Event) map[string]string func (s *KeeperTestHelper) AssertAttributes(event sdk.Event, eventAttributes []sdk.Attribute) { attrs := s.ExtractAttributes(event) for _, attr := range eventAttributes { - s.Require().Equal(attr.Value, attrs[attr.Key]) + s.Assert().Equal(attr.Value, attrs[attr.Key]) } } diff --git a/ibctesting/bridging_fee_test.go b/ibctesting/bridging_fee_test.go new file mode 100644 index 000000000..b9b9294a2 --- /dev/null +++ b/ibctesting/bridging_fee_test.go @@ -0,0 +1,105 @@ +package ibctesting_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" + ibctesting "github.com/cosmos/ibc-go/v6/testing" + "github.com/osmosis-labs/osmosis/v15/x/txfees" + "github.com/stretchr/testify/suite" +) + +type BridgingFeeTestSuite struct { + IBCTestUtilSuite +} + +func TestBridgingFeeTestSuite(t *testing.T) { + suite.Run(t, new(BridgingFeeTestSuite)) +} + +func (suite *BridgingFeeTestSuite) SetupTest() { + suite.IBCTestUtilSuite.SetupTest() +} + +func (suite *BridgingFeeTestSuite) TestNotRollappNoBridgingFee() { + // setup between cosmosChain and hubChain + path := suite.NewTransferPath(suite.hubChain, suite.cosmosChain) + suite.coordinator.Setup(path) + hubEndpoint := path.EndpointA + cosmosEndpoint := path.EndpointB + + timeoutHeight := clienttypes.NewHeight(100, 110) + amount, ok := sdk.NewIntFromString("10000000000000000000") // 10DYM + suite.Require().True(ok) + coinToSendToB := sdk.NewCoin(sdk.DefaultBondDenom, amount) + + // send from cosmosChain to hubChain + msg := types.NewMsgTransfer(cosmosEndpoint.ChannelConfig.PortID, cosmosEndpoint.ChannelID, coinToSendToB, cosmosEndpoint.Chain.SenderAccount.GetAddress().String(), hubEndpoint.Chain.SenderAccount.GetAddress().String(), timeoutHeight, 0, "") + res, err := cosmosEndpoint.Chain.SendMsgs(msg) + suite.Require().NoError(err) // message committed + packet, err := ibctesting.ParsePacketFromEvents(res.GetEvents()) + suite.Require().NoError(err) + err = path.RelayPacket(packet) + suite.Require().NoError(err) // relay committed + + denom := suite.GetRollappToHubIBCDenomFromPacket(packet) + finalBalance := ConvertToApp(suite.hubChain).BankKeeper.GetBalance(suite.hubChain.GetContext(), suite.hubChain.SenderAccount.GetAddress(), denom) + suite.Assert().Equal(sdk.NewCoin(denom, coinToSendToB.Amount), finalBalance) +} + +func (suite *BridgingFeeTestSuite) TestBridgingFee() { + path := suite.NewTransferPath(suite.hubChain, suite.rollappChain) + suite.coordinator.Setup(path) + + rollappEndpoint := path.EndpointB + rollappIBCKeeper := suite.rollappChain.App.GetIBCKeeper() + + suite.CreateRollapp() + suite.RegisterSequencer() + suite.GenesisEvent(path.EndpointA.ChannelID) + + // Update rollapp state + currentRollappBlockHeight := uint64(suite.rollappChain.GetContext().BlockHeight()) + suite.UpdateRollappState(currentRollappBlockHeight) + + timeoutHeight := clienttypes.NewHeight(100, 110) + amount, ok := sdk.NewIntFromString("10000000000000000000") // 10DYM + suite.Require().True(ok) + coinToSendToB := sdk.NewCoin(sdk.DefaultBondDenom, amount) + + /* --------------------- initiating transfer on rollapp --------------------- */ + msg := types.NewMsgTransfer(rollappEndpoint.ChannelConfig.PortID, rollappEndpoint.ChannelID, coinToSendToB, suite.rollappChain.SenderAccount.GetAddress().String(), suite.hubChain.SenderAccount.GetAddress().String(), timeoutHeight, 0, "") + res, err := suite.rollappChain.SendMsgs(msg) + suite.Require().NoError(err) // message committed + packet, err := ibctesting.ParsePacketFromEvents(res.GetEvents()) + suite.Require().NoError(err) + found := rollappIBCKeeper.ChannelKeeper.HasPacketCommitment(rollappEndpoint.Chain.GetContext(), packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence()) + suite.Require().True(found) + err = path.RelayPacket(packet) + suite.Require().Error(err) // expecting error as no AcknowledgePacket expected to return + + // check balance before finalization + denom := suite.GetRollappToHubIBCDenomFromPacket(packet) + transferredCoins := sdk.NewCoin(denom, coinToSendToB.Amount) + recipient := suite.hubChain.SenderAccount.GetAddress() + initialBalance := ConvertToApp(suite.hubChain).BankKeeper.SpendableCoins(suite.hubChain.GetContext(), recipient) + suite.Require().Equal(initialBalance.AmountOf(denom), sdk.ZeroInt()) + + // Finalize the rollapp state + currentRollappBlockHeight = uint64(suite.rollappChain.GetContext().BlockHeight()) + _, err = suite.FinalizeRollappState(1, currentRollappBlockHeight) + suite.Require().NoError(err) + + // check balance after finalization + expectedFee := ConvertToApp(suite.hubChain).DelayedAckKeeper.BridgingFeeFromAmt(suite.hubChain.GetContext(), transferredCoins.Amount) + expectedBalance := initialBalance.Add(transferredCoins).Sub(sdk.NewCoin(denom, expectedFee)) + finalBalance := ConvertToApp(suite.hubChain).BankKeeper.SpendableCoins(suite.hubChain.GetContext(), recipient) + suite.Assert().Equal(expectedBalance, finalBalance) + + // check fees + addr := ConvertToApp(suite.hubChain).AccountKeeper.GetModuleAccount(suite.hubChain.GetContext(), txfees.ModuleName) + txFeesBalance := ConvertToApp(suite.hubChain).BankKeeper.GetBalance(suite.hubChain.GetContext(), addr.GetAddress(), denom) + suite.Assert().Equal(expectedFee, txFeesBalance.Amount) +} diff --git a/ibctesting/eibc_test.go b/ibctesting/eibc_test.go index 5760c09e9..7e5f3cfec 100644 --- a/ibctesting/eibc_test.go +++ b/ibctesting/eibc_test.go @@ -40,6 +40,7 @@ func (suite *EIBCTestSuite) SetupTest() { delayedAckKeeper := ConvertToApp(suite.hubChain).DelayedAckKeeper params := delayedAckKeeper.GetParams(suite.hubChain.GetContext()) params.EpochIdentifier = "month" + params.BridgingFee = sdk.ZeroDec() delayedAckKeeper.SetParams(suite.hubChain.GetContext(), params) } diff --git a/proto/dymension/delayedack/params.proto b/proto/dymension/delayedack/params.proto index c23603e14..4128834f6 100644 --- a/proto/dymension/delayedack/params.proto +++ b/proto/dymension/delayedack/params.proto @@ -10,4 +10,10 @@ message Params { option (gogoproto.goproto_stringer) = false; string epoch_identifier = 1 [ (gogoproto.moretags) = "yaml:\"epoch_identifier\"" ]; + + string bridging_fee = 2 [ + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", + (gogoproto.moretags) = "yaml:\"bridging_fee\"", + (gogoproto.nullable) = false + ]; } diff --git a/x/bridging_fee/events.go b/x/bridging_fee/events.go new file mode 100644 index 000000000..ea2a2231d --- /dev/null +++ b/x/bridging_fee/events.go @@ -0,0 +1,6 @@ +package bridging_fee + +const ( + EventTypeBridgingFee = "bridging_fee" + AttributeKeyFee = "fee" +) diff --git a/x/bridging_fee/ibc_middleware.go b/x/bridging_fee/ibc_middleware.go new file mode 100644 index 000000000..c747da42b --- /dev/null +++ b/x/bridging_fee/ibc_middleware.go @@ -0,0 +1,109 @@ +package bridging_fee + +import ( + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + transfer "github.com/cosmos/ibc-go/v6/modules/apps/transfer" + transferkeeper "github.com/cosmos/ibc-go/v6/modules/apps/transfer/keeper" + channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v6/modules/core/05-port/types" + "github.com/cosmos/ibc-go/v6/modules/core/exported" + + delayedaackkeeper "github.com/dymensionxyz/dymension/v3/x/delayedack/keeper" +) + +const ( + ModuleName = "bridging_fee" +) + +var _ porttypes.Middleware = &BridgingFeeMiddleware{} + +// BridgingFeeMiddleware implements the ICS26 callbacks +// The middleware is responsible for charging a bridging fee on transfers coming from rollapps +// The actual charge happens on the packet finalization +// based on ADR: https://www.notion.so/dymension/ADR-x-Bridging-Fee-Middleware-7ba8c191373f43ce81782fc759913299?pvs=4 +type BridgingFeeMiddleware struct { + transfer.IBCModule + porttypes.ICS4Wrapper + + delayedAckKeeper delayedaackkeeper.Keeper + transferKeeper transferkeeper.Keeper + feeModuleAddr sdk.AccAddress +} + +// NewIBCMiddleware creates a new IBCMiddleware given the keeper and underlying application +func NewIBCMiddleware(transfer transfer.IBCModule, channelKeeper porttypes.ICS4Wrapper, keeper delayedaackkeeper.Keeper, transferKeeper transferkeeper.Keeper, feeModuleAddr sdk.AccAddress) *BridgingFeeMiddleware { + return &BridgingFeeMiddleware{ + IBCModule: transfer, + ICS4Wrapper: channelKeeper, + delayedAckKeeper: keeper, + transferKeeper: transferKeeper, + feeModuleAddr: feeModuleAddr, + } +} + +// GetFeeRecipient returns the address that will receive the bridging fee +func (im BridgingFeeMiddleware) GetFeeRecipient() sdk.AccAddress { + return im.feeModuleAddr +} + +func (im *BridgingFeeMiddleware) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) exported.Acknowledgement { + if !im.delayedAckKeeper.IsRollappsEnabled(ctx) { + return im.IBCModule.OnRecvPacket(ctx, packet, relayer) + } + logger := ctx.Logger().With( + "module", ModuleName, + "packet_source", packet.SourcePort, + "packet_destination", packet.DestinationPort, + "packet_sequence", packet.Sequence) + + rollappPortOnHub, rollappChannelOnHub := packet.DestinationPort, packet.DestinationChannel + rollappID, transferPacketData, err := im.delayedAckKeeper.ExtractRollappIDAndTransferPacket(ctx, packet, rollappPortOnHub, rollappChannelOnHub) + if err != nil { + logger.Error("Failed to extract rollapp id from packet", "err", err) + return channeltypes.NewErrorAcknowledgement(err) + } + + if rollappID == "" { + logger.Debug("Skipping IBC transfer OnRecvPacket for non-rollapp chain") + return im.IBCModule.OnRecvPacket(ctx, packet, relayer) + } + + // parse the transfer amount + transferAmount, ok := sdk.NewIntFromString(transferPacketData.Amount) + if !ok { + err = errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "parse transfer amount into math.Int") + return channeltypes.NewErrorAcknowledgement(err) + } + + // get fee + fee := im.delayedAckKeeper.BridgingFeeFromAmt(ctx, transferAmount) + + // update packet data for the fee charge + feePacket := *transferPacketData + feePacket.Amount = fee.String() + feePacket.Receiver = im.GetFeeRecipient().String() + + // No event emitted, as we called the transfer keeper directly (vs the transfer middleware) + err = im.transferKeeper.OnRecvPacket(ctx, packet, feePacket) + if err != nil { + logger.Error("Failed to charge bridging fee", "err", err) + // we continue as we don't want the fee charge to fail the transfer in any case + fee = sdk.ZeroInt() + } else { + logger.Debug("Charged bridging fee", "fee", fee) + ctx.EventManager().EmitEvent( + sdk.NewEvent( + EventTypeBridgingFee, + sdk.NewAttribute(AttributeKeyFee, fee.String()), + sdk.NewAttribute(sdk.AttributeKeySender, transferPacketData.Sender), + ), + ) + } + + // transfer the remaining amount + transferPacketData.Amount = transferAmount.Sub(fee).String() + packet.Data = transferPacketData.GetBytes() + return im.IBCModule.OnRecvPacket(ctx, packet, relayer) +} diff --git a/x/delayedack/eibc.go b/x/delayedack/eibc.go index fd63c7459..b28dc607c 100644 --- a/x/delayedack/eibc.go +++ b/x/delayedack/eibc.go @@ -59,7 +59,7 @@ func (im IBCMiddleware) eIBCDemandOrderHandler(ctx sdk.Context, rollappPacket co } } - eibcDemandOrder, err := im.createDemandOrderFromIBCPacket(data, &rollappPacket, *packetMetaData.EIBC) + eibcDemandOrder, err := im.createDemandOrderFromIBCPacket(ctx, data, &rollappPacket, *packetMetaData.EIBC) if err != nil { return fmt.Errorf("create eibc demand order: %w", err) } @@ -75,7 +75,7 @@ func (im IBCMiddleware) eIBCDemandOrderHandler(ctx sdk.Context, rollappPacket co // It validates the fungible token packet data, extracts the fee from the memo, // calculates the demand order price, and creates a new demand order. // It returns the created demand order or an error if there is any. -func (im IBCMiddleware) createDemandOrderFromIBCPacket(fungibleTokenPacketData transfertypes.FungibleTokenPacketData, +func (im IBCMiddleware) createDemandOrderFromIBCPacket(ctx sdk.Context, fungibleTokenPacketData transfertypes.FungibleTokenPacketData, rollappPacket *commontypes.RollappPacket, eibcMetaData types.EIBCMetadata, ) (*eibctypes.DemandOrder, error) { // Validate the fungible token packet data as we're going to use it to create the demand order @@ -98,6 +98,7 @@ func (im IBCMiddleware) createDemandOrderFromIBCPacket(fungibleTokenPacketData t if amt.LT(fee) { return nil, fmt.Errorf("fee cannot be larger than amount: fee: %s: amt :%s", fee, fungibleTokenPacketData.Amount) } + /* In case of timeout/errack: fee = fee_multiplier*transfer_amount @@ -125,6 +126,12 @@ func (im IBCMiddleware) createDemandOrderFromIBCPacket(fungibleTokenPacketData t demandOrderDenom = trace.IBCDenom() demandOrderRecipient = fungibleTokenPacketData.Sender // and who tried to send it (refund because it failed) case commontypes.RollappPacket_ON_RECV: + bridgingFee := im.keeper.BridgingFeeFromAmt(ctx, amt) + if bridgingFee.GT(fee) { + // We check that the fee the fulfiller makes is at least as big as the bridging fee they will have to pay later + // this is to improve UX and help fulfillers not lose money. + return nil, fmt.Errorf("eibc fee cannot be smaller than bridging fee: eibc fee: %s: bridging fee: %s", fee, bridgingFee) + } demandOrderDenom = im.getEIBCTransferDenom(*rollappPacket.Packet, fungibleTokenPacketData) demandOrderRecipient = fungibleTokenPacketData.Receiver // who we tried to send to } diff --git a/x/delayedack/genesis_test.go b/x/delayedack/genesis_test.go index 056e7cdbb..93ba066bd 100644 --- a/x/delayedack/genesis_test.go +++ b/x/delayedack/genesis_test.go @@ -11,6 +11,8 @@ import ( "github.com/stretchr/testify/require" ) +var defBridgingFee = types.DefaultParams().BridgingFee + func TestInitGenesis(t *testing.T) { tests := []struct { name string @@ -22,14 +24,24 @@ func TestInitGenesis(t *testing.T) { name: "only params - success", params: types.Params{ EpochIdentifier: "week", + BridgingFee: defBridgingFee, }, rollappPackets: []commontypes.RollappPacket{}, expPanic: false, }, + { + name: "only params - missing bridging fee - fail", + params: types.Params{ + EpochIdentifier: "week", + }, + rollappPackets: []commontypes.RollappPacket{}, + expPanic: true, + }, { name: "params and rollapp packets - panic", params: types.Params{ EpochIdentifier: "week", + BridgingFee: defBridgingFee, }, rollappPackets: []commontypes.RollappPacket{{RollappId: "0"}}, expPanic: true, @@ -58,6 +70,7 @@ func TestExportGenesis(t *testing.T) { // Set params params := types.Params{ EpochIdentifier: "week", + BridgingFee: defBridgingFee, } k.SetParams(ctx, params) // Set some demand orders diff --git a/x/delayedack/ibc_middleware.go b/x/delayedack/ibc_middleware.go index 78addefba..566cca383 100644 --- a/x/delayedack/ibc_middleware.go +++ b/x/delayedack/ibc_middleware.go @@ -7,7 +7,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" - transfertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types" porttypes "github.com/cosmos/ibc-go/v6/modules/core/05-port/types" @@ -53,7 +52,7 @@ func (im IBCMiddleware) OnRecvPacket( rollappPortOnHub, rollappChannelOnHub := packet.DestinationPort, packet.DestinationChannel - rollappID, transferPacketData, err := im.ExtractRollappIDAndTransferPacket(ctx, packet, rollappPortOnHub, rollappChannelOnHub) + rollappID, transferPacketData, err := im.keeper.ExtractRollappIDAndTransferPacket(ctx, packet, rollappPortOnHub, rollappChannelOnHub) if err != nil { logger.Error("Failed to extract rollapp id from packet", "err", err) return channeltypes.NewErrorAcknowledgement(err) @@ -135,7 +134,7 @@ func (im IBCMiddleware) OnAcknowledgementPacket( return errorsmod.Wrapf(types.ErrUnknownRequest, "unmarshal ICS-20 transfer packet acknowledgement: %v", err) } - rollappID, transferPacketData, err := im.ExtractRollappIDAndTransferPacket(ctx, packet, rollappPortOnHub, rollappChannelOnHub) + rollappID, transferPacketData, err := im.keeper.ExtractRollappIDAndTransferPacket(ctx, packet, rollappPortOnHub, rollappChannelOnHub) if err != nil { logger.Error("Failed to extract rollapp id from channel", "err", err) return err @@ -221,7 +220,7 @@ func (im IBCMiddleware) OnTimeoutPacket( rollappPortOnHub, rollappChannelOnHub := packet.SourcePort, packet.SourceChannel - rollappID, transferPacketData, err := im.ExtractRollappIDAndTransferPacket(ctx, packet, rollappPortOnHub, rollappChannelOnHub) + rollappID, transferPacketData, err := im.keeper.ExtractRollappIDAndTransferPacket(ctx, packet, rollappPortOnHub, rollappChannelOnHub) if err != nil { logger.Error("Failed to extract rollapp id from channel", "err", err) return err @@ -316,36 +315,6 @@ func (im IBCMiddleware) GetAppVersion(ctx sdk.Context, portID, channelID string) return im.keeper.GetAppVersion(ctx, portID, channelID) } -// ExtractRollappIDAndTransferPacket extracts the rollapp ID from the packet -func (im IBCMiddleware) ExtractRollappIDAndTransferPacket(ctx sdk.Context, packet channeltypes.Packet, rollappPortOnHub string, rollappChannelOnHub string) (string, *transfertypes.FungibleTokenPacketData, error) { - // no-op if the packet is not a fungible token packet - var data transfertypes.FungibleTokenPacketData - if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil { - return "", nil, err - } - // Check if the packet is destined for a rollapp - chainID, err := im.keeper.ExtractChainIDFromChannel(ctx, rollappPortOnHub, rollappChannelOnHub) - if err != nil { - return "", &data, err - } - rollapp, found := im.keeper.GetRollapp(ctx, chainID) - if !found { - return "", &data, nil - } - if rollapp.ChannelId == "" { - return "", &data, errorsmod.Wrapf(rollapptypes.ErrGenesisEventNotTriggered, "empty channel id: rollap id: %s", chainID) - } - // check if the channelID matches the rollappID's channelID - if rollapp.ChannelId != rollappChannelOnHub { - return "", &data, errorsmod.Wrapf( - rollapptypes.ErrMismatchedChannelID, - "channel id mismatch: expect: %s: got: %s", rollapp.ChannelId, rollappChannelOnHub, - ) - } - - return chainID, &data, nil -} - // GetProofHeight returns the proof height of the packet func (im IBCMiddleware) GetProofHeight(ctx sdk.Context, packetType commontypes.RollappPacket_Type, rollappPortOnHub string, rollappChannelOnHub string, sequence uint64, diff --git a/x/delayedack/keeper/finalize.go b/x/delayedack/keeper/finalize.go index f8cea1080..37d43b056 100644 --- a/x/delayedack/keeper/finalize.go +++ b/x/delayedack/keeper/finalize.go @@ -56,6 +56,7 @@ func (k Keeper) finalizeRollappPacket( switch rollappPacket.Type { case commontypes.RollappPacket_ON_RECV: + // TODO: makes more sense to modify the packet when calling the handler, instead storing in db "wrong" packet ack := ibc.OnRecvPacket(ctx, *rollappPacket.Packet, rollappPacket.Relayer) /* We only write the ack if writing it succeeds: diff --git a/x/delayedack/keeper/fraud_test.go b/x/delayedack/keeper/fraud_test.go index 875d2d918..d5bfe0e43 100644 --- a/x/delayedack/keeper/fraud_test.go +++ b/x/delayedack/keeper/fraud_test.go @@ -63,7 +63,7 @@ func (suite *DelayedAckTestSuite) TestDeletionOfRevertedPackets() { suite.Require().Equal(10, len(keeper.GetAllRollappPackets(ctx))) - keeper.SetParams(ctx, types.Params{EpochIdentifier: "minute"}) + keeper.SetParams(ctx, types.Params{EpochIdentifier: "minute", BridgingFee: keeper.BridgingFee(ctx)}) epochHooks := keeper.GetEpochHooks() err = epochHooks.AfterEpochEnd(ctx, "minute", 1) suite.Require().NoError(err) diff --git a/x/delayedack/keeper/hooks_test.go b/x/delayedack/keeper/hooks_test.go index e1768fc0b..9b2fea121 100644 --- a/x/delayedack/keeper/hooks_test.go +++ b/x/delayedack/keeper/hooks_test.go @@ -70,7 +70,7 @@ func (suite *DelayedAckTestSuite) TestAfterEpochEnd() { finalizedRollappPackets := keeper.ListRollappPackets(ctx, types.ByRollappIDByStatus(rollappID, commontypes.Status_FINALIZED)) suite.Require().Equal(tc.finalizePacketsNum, len(finalizedRollappPackets)) - keeper.SetParams(ctx, types.Params{EpochIdentifier: tc.epochIdentifierParam}) + keeper.SetParams(ctx, types.Params{EpochIdentifier: tc.epochIdentifierParam, BridgingFee: keeper.BridgingFee(ctx)}) epochHooks := keeper.GetEpochHooks() err := epochHooks.AfterEpochEnd(ctx, tc.epochIdentifier, 1) suite.Require().NoError(err) diff --git a/x/delayedack/keeper/keeper.go b/x/delayedack/keeper/keeper.go index d10b47ab7..6983a5dc3 100644 --- a/x/delayedack/keeper/keeper.go +++ b/x/delayedack/keeper/keeper.go @@ -11,6 +11,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + transfertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" connectiontypes "github.com/cosmos/ibc-go/v6/modules/core/03-connection/types" channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types" @@ -78,6 +79,36 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) } +// ExtractRollappIDAndTransferPacket extracts the rollapp ID from the packet +func (k Keeper) ExtractRollappIDAndTransferPacket(ctx sdk.Context, packet channeltypes.Packet, rollappPortOnHub string, rollappChannelOnHub string) (string, *transfertypes.FungibleTokenPacketData, error) { + // no-op if the packet is not a fungible token packet + var data transfertypes.FungibleTokenPacketData + if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil { + return "", nil, err + } + // Check if the packet is destined for a rollapp + chainID, err := k.ExtractChainIDFromChannel(ctx, rollappPortOnHub, rollappChannelOnHub) + if err != nil { + return "", &data, err + } + rollapp, found := k.GetRollapp(ctx, chainID) + if !found { + return "", &data, nil + } + if rollapp.ChannelId == "" { + return "", &data, errorsmod.Wrapf(rollapptypes.ErrGenesisEventNotTriggered, "empty channel id: rollap id: %s", chainID) + } + // check if the channelID matches the rollappID's channelID + if rollapp.ChannelId != rollappChannelOnHub { + return "", &data, errorsmod.Wrapf( + rollapptypes.ErrMismatchedChannelID, + "channel id mismatch: expect: %s: got: %s", rollapp.ChannelId, rollappChannelOnHub, + ) + } + + return chainID, &data, nil +} + func (k Keeper) ExtractChainIDFromChannel(ctx sdk.Context, portID string, channelID string) (string, error) { _, clientState, err := k.channelKeeper.GetChannelClientState(ctx, portID, channelID) if err != nil { diff --git a/x/delayedack/keeper/params.go b/x/delayedack/keeper/params.go index f5dc90e19..633aa3f3b 100644 --- a/x/delayedack/keeper/params.go +++ b/x/delayedack/keeper/params.go @@ -7,7 +7,7 @@ import ( // GetParams get all parameters as types.Params func (k Keeper) GetParams(ctx sdk.Context) types.Params { - return types.NewParams(k.EpochIdentifier(ctx)) + return types.NewParams(k.EpochIdentifier(ctx), k.BridgingFee(ctx)) } // SetParams set the params @@ -19,3 +19,12 @@ func (k Keeper) EpochIdentifier(ctx sdk.Context) (res string) { k.paramstore.Get(ctx, types.KeyEpochIdentifier, &res) return } + +func (k Keeper) BridgingFee(ctx sdk.Context) (res sdk.Dec) { + k.paramstore.Get(ctx, types.KeyBridgeFee, &res) + return +} + +func (k Keeper) BridgingFeeFromAmt(ctx sdk.Context, amt sdk.Int) (res sdk.Int) { + return k.BridgingFee(ctx).MulInt(amt).TruncateInt() +} diff --git a/x/delayedack/types/params.go b/x/delayedack/types/params.go index 0e5b55220..aaf6cb3f6 100644 --- a/x/delayedack/types/params.go +++ b/x/delayedack/types/params.go @@ -1,14 +1,22 @@ package types import ( + fmt "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" "gopkg.in/yaml.v2" ) var _ paramtypes.ParamSet = (*Params)(nil) -// KeyEpochIdentifier is the key for the epoch identifier -var KeyEpochIdentifier = []byte("EpochIdentifier") +var ( + // KeyEpochIdentifier is the key for the epoch identifier + KeyEpochIdentifier = []byte("EpochIdentifier") + + // KeyBridgeFee is the key for the bridge fee + KeyBridgeFee = []byte("BridgeFee") +) const ( defaultEpochIdentifier = "hour" @@ -20,28 +28,66 @@ func ParamKeyTable() paramtypes.KeyTable { } // NewParams creates a new Params instance -func NewParams(epochIdentifier string) Params { +func NewParams(epochIdentifier string, bridgingFee sdk.Dec) Params { return Params{ EpochIdentifier: epochIdentifier, + BridgingFee: bridgingFee, } } // DefaultParams returns a default set of parameters func DefaultParams() Params { - return NewParams(defaultEpochIdentifier) + return NewParams( + defaultEpochIdentifier, + sdk.NewDecWithPrec(1, 3), // 0.1% + ) } // ParamSetPairs get the params.ParamSet func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { return paramtypes.ParamSetPairs{ - paramtypes.NewParamSetPair(KeyEpochIdentifier, &p.EpochIdentifier, func(_ interface{}) error { return nil }), + paramtypes.NewParamSetPair(KeyEpochIdentifier, &p.EpochIdentifier, validateEpochIdentifier), + paramtypes.NewParamSetPair(KeyBridgeFee, &p.BridgingFee, validateBridgingFee), + } +} + +func validateBridgingFee(i interface{}) error { + v, ok := i.(sdk.Dec) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + if v.IsNil() { + return fmt.Errorf("invalid global pool params: %+v", i) + } + if v.IsNegative() { + return fmt.Errorf("bridging fee must be positive: %s", v) + } + + if v.GTE(sdk.OneDec()) { + return fmt.Errorf("bridging fee too large: %s", v) } + + return nil +} + +func validateEpochIdentifier(i interface{}) error { + v, ok := i.(string) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + if len(v) == 0 { + return fmt.Errorf("epoch identifier cannot be empty") + } + return nil } // Validate validates the set of params func (p Params) Validate() error { - if p.EpochIdentifier == "" { - return ErrEmptyEpochIdentifier + if err := validateBridgingFee(p.BridgingFee); err != nil { + return err + } + if err := validateEpochIdentifier(p.EpochIdentifier); err != nil { + return err } return nil } diff --git a/x/delayedack/types/params.pb.go b/x/delayedack/types/params.pb.go index e19862db3..b5f98530f 100644 --- a/x/delayedack/types/params.pb.go +++ b/x/delayedack/types/params.pb.go @@ -5,6 +5,7 @@ package types import ( fmt "fmt" + github_com_cosmos_cosmos_sdk_types "github.com/cosmos/cosmos-sdk/types" _ "github.com/gogo/protobuf/gogoproto" proto "github.com/gogo/protobuf/proto" io "io" @@ -25,7 +26,8 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // Params defines the parameters for the module. type Params struct { - EpochIdentifier string `protobuf:"bytes,1,opt,name=epoch_identifier,json=epochIdentifier,proto3" json:"epoch_identifier,omitempty" yaml:"epoch_identifier"` + EpochIdentifier string `protobuf:"bytes,1,opt,name=epoch_identifier,json=epochIdentifier,proto3" json:"epoch_identifier,omitempty" yaml:"epoch_identifier"` + BridgingFee github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,2,opt,name=bridging_fee,json=bridgingFee,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"bridging_fee" yaml:"bridging_fee"` } func (m *Params) Reset() { *m = Params{} } @@ -74,20 +76,25 @@ func init() { func init() { proto.RegisterFile("dymension/delayedack/params.proto", fileDescriptor_4a023ab5715cd34b) } var fileDescriptor_4a023ab5715cd34b = []byte{ - // 207 bytes of a gzipped FileDescriptorProto + // 276 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x4c, 0xa9, 0xcc, 0x4d, 0xcd, 0x2b, 0xce, 0xcc, 0xcf, 0xd3, 0x4f, 0x49, 0xcd, 0x49, 0xac, 0x4c, 0x4d, 0x49, 0x4c, 0xce, 0xd6, 0x2f, 0x48, 0x2c, 0x4a, 0xcc, 0x2d, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x42, 0x28, 0xa9, 0xa8, 0xac, 0xd2, 0x83, 0x73, 0xf4, 0x10, 0xea, 0xa5, 0x44, 0xd2, 0xf3, 0xd3, 0xf3, 0xc1, - 0xaa, 0xf5, 0x41, 0x2c, 0x88, 0x46, 0xa5, 0x30, 0x2e, 0xb6, 0x00, 0xb0, 0x41, 0x42, 0x6e, 0x5c, - 0x02, 0xa9, 0x05, 0xf9, 0xc9, 0x19, 0xf1, 0x99, 0x29, 0xa9, 0x79, 0x25, 0x99, 0x69, 0x99, 0xa9, - 0x45, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x4e, 0xd2, 0x9f, 0xee, 0xc9, 0x8b, 0x57, 0x26, 0xe6, - 0xe6, 0x58, 0x29, 0xa1, 0xab, 0x50, 0x0a, 0xe2, 0x07, 0x0b, 0x79, 0xc2, 0x45, 0xac, 0x58, 0x66, - 0x2c, 0x90, 0x67, 0x70, 0x0a, 0x3c, 0xf1, 0x48, 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, - 0xe4, 0x18, 0x27, 0x3c, 0x96, 0x63, 0xb8, 0xf0, 0x58, 0x8e, 0xe1, 0xc6, 0x63, 0x39, 0x86, 0x28, - 0xf3, 0xf4, 0xcc, 0x92, 0x8c, 0xd2, 0x24, 0xbd, 0xe4, 0xfc, 0x5c, 0x7d, 0x64, 0x57, 0x23, 0x38, - 0xfa, 0x65, 0xc6, 0xfa, 0x15, 0xc8, 0x5e, 0x2d, 0xa9, 0x2c, 0x48, 0x2d, 0x4e, 0x62, 0x03, 0xbb, - 0xd8, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x18, 0x5d, 0x4d, 0xd5, 0x0f, 0x01, 0x00, 0x00, + 0xaa, 0xf5, 0x41, 0x2c, 0x88, 0x46, 0xa5, 0x03, 0x8c, 0x5c, 0x6c, 0x01, 0x60, 0x93, 0x84, 0xdc, + 0xb8, 0x04, 0x52, 0x0b, 0xf2, 0x93, 0x33, 0xe2, 0x33, 0x53, 0x52, 0xf3, 0x4a, 0x32, 0xd3, 0x32, + 0x53, 0x8b, 0x24, 0x18, 0x15, 0x18, 0x35, 0x38, 0x9d, 0xa4, 0x3f, 0xdd, 0x93, 0x17, 0xaf, 0x4c, + 0xcc, 0xcd, 0xb1, 0x52, 0x42, 0x57, 0xa1, 0x14, 0xc4, 0x0f, 0x16, 0xf2, 0x84, 0x8b, 0x08, 0x65, + 0x70, 0xf1, 0x24, 0x15, 0x65, 0xa6, 0xa4, 0x67, 0xe6, 0xa5, 0xc7, 0xa7, 0xa5, 0xa6, 0x4a, 0x30, + 0x81, 0xcd, 0x70, 0x3d, 0x71, 0x4f, 0x9e, 0xe1, 0xd6, 0x3d, 0x79, 0xb5, 0xf4, 0xcc, 0x92, 0x8c, + 0xd2, 0x24, 0xbd, 0xe4, 0xfc, 0x5c, 0xfd, 0xe4, 0xfc, 0xe2, 0xdc, 0xfc, 0x62, 0x28, 0xa5, 0x5b, + 0x9c, 0x92, 0xad, 0x5f, 0x52, 0x59, 0x90, 0x5a, 0xac, 0xe7, 0x92, 0x9a, 0xfc, 0xe9, 0x9e, 0xbc, + 0x30, 0xc4, 0x46, 0x64, 0xb3, 0x94, 0x82, 0xb8, 0x61, 0x5c, 0xb7, 0xd4, 0x54, 0x2b, 0x96, 0x19, + 0x0b, 0xe4, 0x19, 0x9c, 0x02, 0x4f, 0x3c, 0x92, 0x63, 0xbc, 0xf0, 0x48, 0x8e, 0xf1, 0xc1, 0x23, + 0x39, 0xc6, 0x09, 0x8f, 0xe5, 0x18, 0x2e, 0x3c, 0x96, 0x63, 0xb8, 0xf1, 0x58, 0x8e, 0x21, 0xca, + 0x1c, 0xc9, 0x2e, 0xe4, 0x00, 0x42, 0x70, 0xf4, 0xcb, 0x8c, 0xf5, 0x2b, 0x90, 0x43, 0x15, 0xec, + 0x80, 0x24, 0x36, 0x70, 0xe0, 0x18, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xf7, 0x45, 0x26, 0xe5, + 0x7a, 0x01, 0x00, 0x00, } func (m *Params) Marshal() (dAtA []byte, err error) { @@ -110,6 +117,16 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + { + size := m.BridgingFee.Size() + i -= size + if _, err := m.BridgingFee.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintParams(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 if len(m.EpochIdentifier) > 0 { i -= len(m.EpochIdentifier) copy(dAtA[i:], m.EpochIdentifier) @@ -141,6 +158,8 @@ func (m *Params) Size() (n int) { if l > 0 { n += 1 + l + sovParams(uint64(l)) } + l = m.BridgingFee.Size() + n += 1 + l + sovParams(uint64(l)) return n } @@ -211,6 +230,40 @@ func (m *Params) Unmarshal(dAtA []byte) error { } m.EpochIdentifier = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field BridgingFee", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthParams + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthParams + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.BridgingFee.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipParams(dAtA[iNdEx:]) diff --git a/x/eibc/genesis_test.go b/x/eibc/genesis_test.go index 98b2b8d2b..ff0b9fa86 100644 --- a/x/eibc/genesis_test.go +++ b/x/eibc/genesis_test.go @@ -23,7 +23,7 @@ func TestInitGenesis(t *testing.T) { Price: sdk.Coins{sdk.Coin{Denom: "adym", Amount: math.NewInt(150)}}, Fee: sdk.Coins{sdk.Coin{Denom: "adym", Amount: math.NewInt(50)}}, Recipient: "dym17g9cn4ss0h0dz5qhg2cg4zfnee6z3ftg3q6v58", - IsFullfilled: false, + IsFulfilled: false, TrackingPacketStatus: commontypes.Status_PENDING, }, { @@ -32,7 +32,7 @@ func TestInitGenesis(t *testing.T) { Price: sdk.Coins{sdk.Coin{Denom: "adym", Amount: math.NewInt(250)}}, Fee: sdk.Coins{sdk.Coin{Denom: "adym", Amount: math.NewInt(150)}}, Recipient: "dym15saxgqw6kvhv6k5sg6r45kmdf4sf88kfw2adcw", - IsFullfilled: true, + IsFulfilled: true, TrackingPacketStatus: commontypes.Status_REVERTED, }, }, diff --git a/x/eibc/keeper/hooks_test.go b/x/eibc/keeper/hooks_test.go index cb7c6ab61..09fb3369d 100644 --- a/x/eibc/keeper/hooks_test.go +++ b/x/eibc/keeper/hooks_test.go @@ -65,7 +65,9 @@ func (suite *KeeperTestSuite) TestAfterRollappPacketDeleted() { // Trigger the delayed ack hook which should delete the rollapp packet and the demand order epochIdentifier := "minute" - suite.App.DelayedAckKeeper.SetParams(suite.Ctx, delayedacktypes.Params{EpochIdentifier: epochIdentifier}) + defParams := delayedacktypes.DefaultParams() + defParams.EpochIdentifier = epochIdentifier + suite.App.DelayedAckKeeper.SetParams(suite.Ctx, defParams) hooks := suite.App.DelayedAckKeeper.GetEpochHooks() err = hooks.AfterEpochEnd(suite.Ctx, epochIdentifier, 1) suite.Require().NoError(err) diff --git a/x/eibc/keeper/msg_server.go b/x/eibc/keeper/msg_server.go index bde51c4eb..aedf87d41 100644 --- a/x/eibc/keeper/msg_server.go +++ b/x/eibc/keeper/msg_server.go @@ -37,6 +37,16 @@ func (m msgServer) FulfillOrder(goCtx context.Context, msg *types.MsgFulfillOrde if demandOrder.IsFulfilled { return nil, types.ErrDemandAlreadyFulfilled } + + // Check the order is profitable in regards to the bridging fee + denom := demandOrder.Price[0].Denom + originalAmt := demandOrder.Price.AmountOf(denom).Add(demandOrder.Fee.AmountOf(denom)) + bridgingFee := m.DelayedAckKeeper.BridgingFeeFromAmt(ctx, originalAmt) + + if demandOrder.Fee.AmountOf(denom).LT(bridgingFee) { + return nil, types.ErrDemandOrderNotProfitable + } + // Check the underlying packet is still relevant (i.e not expired, rejected, reverted) if demandOrder.TrackingPacketStatus != commontypes.Status_PENDING { return nil, types.ErrDemandOrderInactive diff --git a/x/eibc/keeper/msg_server_test.go b/x/eibc/keeper/msg_server_test.go index f9c6cb6ff..eb1668df4 100644 --- a/x/eibc/keeper/msg_server_test.go +++ b/x/eibc/keeper/msg_server_test.go @@ -136,15 +136,9 @@ func (suite *KeeperTestSuite) TestMsgFulfillOrder() { sdk.NewAttribute(types.AttributeKeyIsFulfilled, "true"), sdk.NewAttribute(types.AttributeKeyPacketStatus, commontypes.Status_PENDING.String()), }, - expectedPostFulfillmentEventsType: eibcEventType, - expectedPostFulfillmentEventsCount: 0, - expectedPostFulfillmentEventsAttributes: []sdk.Attribute{ - sdk.NewAttribute(types.AttributeKeyId, types.BuildDemandIDFromPacketKey(string(rollappPacketKey))), - sdk.NewAttribute(types.AttributeKeyPrice, "150"+sdk.DefaultBondDenom), - sdk.NewAttribute(types.AttributeKeyFee, "50"+sdk.DefaultBondDenom), - sdk.NewAttribute(types.AttributeKeyIsFulfilled, "true"), - sdk.NewAttribute(types.AttributeKeyPacketStatus, commontypes.Status_PENDING.String()), - }, + expectedPostFulfillmentEventsType: eibcEventType, + expectedPostFulfillmentEventsCount: 0, + expectedPostFulfillmentEventsAttributes: []sdk.Attribute{}, }, { name: "Test demand order fulfillment - status not pending", @@ -165,15 +159,33 @@ func (suite *KeeperTestSuite) TestMsgFulfillOrder() { sdk.NewAttribute(types.AttributeKeyIsFulfilled, "false"), sdk.NewAttribute(types.AttributeKeyPacketStatus, commontypes.Status_FINALIZED.String()), }, - expectedPostFulfillmentEventsType: eibcEventType, - expectedPostFulfillmentEventsCount: 0, - expectedPostFulfillmentEventsAttributes: []sdk.Attribute{ + expectedPostFulfillmentEventsType: eibcEventType, + expectedPostFulfillmentEventsCount: 0, + expectedPostFulfillmentEventsAttributes: []sdk.Attribute{}, + }, + { + name: "Test demand order fulfillment - non profitable order", + demandOrderPrice: 2000, + demandOrderFee: 1, + demandOrderFulfillmentStatus: false, + demandOrderUnderlyingPacketStatus: commontypes.Status_PENDING, + demandOrderDenom: sdk.DefaultBondDenom, + underlyingRollappPacket: rollappPacket, + expectedFulfillmentError: types.ErrDemandOrderNotProfitable, + eIBCdemandAddrBalance: math.NewInt(1000), + expectedDemandOrdefFulfillmentStatus: false, + expectedPostCreationEventsType: eibcEventType, + expectedPostCreationEventsCount: 1, + expectedPostCreationEventsAttributes: []sdk.Attribute{ sdk.NewAttribute(types.AttributeKeyId, types.BuildDemandIDFromPacketKey(string(rollappPacketKey))), - sdk.NewAttribute(types.AttributeKeyPrice, "150"+sdk.DefaultBondDenom), - sdk.NewAttribute(types.AttributeKeyFee, "50"+sdk.DefaultBondDenom), + sdk.NewAttribute(types.AttributeKeyPrice, "2000"+sdk.DefaultBondDenom), + sdk.NewAttribute(types.AttributeKeyFee, "1"+sdk.DefaultBondDenom), sdk.NewAttribute(types.AttributeKeyIsFulfilled, "false"), - sdk.NewAttribute(types.AttributeKeyPacketStatus, commontypes.Status_FINALIZED.String()), + sdk.NewAttribute(types.AttributeKeyPacketStatus, commontypes.Status_PENDING.String()), }, + expectedPostFulfillmentEventsType: eibcEventType, + expectedPostFulfillmentEventsCount: 0, + expectedPostFulfillmentEventsAttributes: []sdk.Attribute{}, }, } totalEventsEmitted := 0 @@ -195,31 +207,30 @@ func (suite *KeeperTestSuite) TestMsgFulfillOrder() { // Update rollapp status if needed if rollappPacket.Status != tc.demandOrderUnderlyingPacketStatus { _, err = suite.App.DelayedAckKeeper.UpdateRollappPacketWithStatus(suite.Ctx, *rollappPacket, tc.demandOrderUnderlyingPacketStatus) - suite.Require().NoError(err) + suite.Require().NoError(err, tc.name) } // Validate creation events emitted suite.AssertEventEmitted(suite.Ctx, tc.expectedPostCreationEventsType, tc.expectedPostCreationEventsCount+totalEventsEmitted) totalEventsEmitted += tc.expectedPostCreationEventsCount lastEvent, ok := suite.FindLastEventOfType(suite.Ctx.EventManager().Events(), tc.expectedPostCreationEventsType) - suite.Require().True(ok) + suite.Require().True(ok, tc.name) suite.AssertAttributes(lastEvent, tc.expectedPostCreationEventsAttributes) // try to fulfill the demand order demandOrder, err = suite.App.EIBCKeeper.GetDemandOrder(suite.Ctx, tc.demandOrderUnderlyingPacketStatus, demandOrder.Id) - suite.Require().NoError(err) + suite.Require().NoError(err, tc.name) _, err = suite.msgServer.FulfillOrder(suite.Ctx, &types.MsgFulfillOrder{ FulfillerAddress: eibcDemandAddr.String(), OrderId: demandOrder.Id, }) if tc.expectedFulfillmentError != nil { - suite.Require().Error(err) - suite.Require().ErrorIs(err, tc.expectedFulfillmentError) + suite.Require().ErrorIs(err, tc.expectedFulfillmentError, tc.name) } else { - suite.Require().NoError(err) + suite.Require().NoError(err, tc.name) } // Check that the demand fulfillment demandOrder, err = suite.App.EIBCKeeper.GetDemandOrder(suite.Ctx, tc.demandOrderUnderlyingPacketStatus, demandOrder.Id) suite.Require().NoError(err) - suite.Assert().Equal(tc.expectedDemandOrdefFulfillmentStatus, demandOrder.IsFulfilled) + suite.Assert().Equal(tc.expectedDemandOrdefFulfillmentStatus, demandOrder.IsFulfilled, tc.name) // Check balances updates in case of success if tc.expectedFulfillmentError == nil { afterFulfillmentSupplyAddrBalance := suite.App.BankKeeper.GetBalance(suite.Ctx, eibcSupplyAddr, sdk.DefaultBondDenom) diff --git a/x/eibc/types/errors.go b/x/eibc/types/errors.go index 5fa188fa9..1ff8c63d8 100644 --- a/x/eibc/types/errors.go +++ b/x/eibc/types/errors.go @@ -19,4 +19,5 @@ var ( ErrTooMuchTimeoutFee = errorsmod.Register(ModuleName, 12, "Timeout fee must be less than or equal to the total amount") ErrNegativeErrAckFee = errorsmod.Register(ModuleName, 13, "Error acknowledgement fee must be greater than or equal to 0") ErrTooMuchErrAckFee = errorsmod.Register(ModuleName, 14, "Error acknowledgement fee must be less than or equal to the total amount") + ErrDemandOrderNotProfitable = errorsmod.Register(ModuleName, 15, "Demand order not profitable") ) diff --git a/x/eibc/types/expected_keepers.go b/x/eibc/types/expected_keepers.go index 11eb2a295..37167c76c 100644 --- a/x/eibc/types/expected_keepers.go +++ b/x/eibc/types/expected_keepers.go @@ -20,4 +20,5 @@ type BankKeeper interface { type DelayedAckKeeper interface { GetRollappPacket(ctx sdk.Context, rollappPacketKey string) (*commontypes.RollappPacket, error) + BridgingFeeFromAmt(ctx sdk.Context, amt sdk.Int) (res sdk.Int) }