From 809fa089783e7a9f3fe8967be0fff9c87873f3e7 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Mon, 4 Dec 2023 18:44:30 +0100 Subject: [PATCH] chore(uibc): refactore to make the uibc stack more modular (#2343) * refactore x/uibc * fix build * rename * move ibc quota logic to quota keeper * update middleware implementation * fixes * lint * review * lint * fix test --- app/app.go | 10 +- x/uibc/module/abci.go | 9 +- x/uibc/module/module.go | 30 ++-- x/uibc/quota/{keeper => }/genesis.go | 6 +- x/uibc/quota/{keeper => }/grpc_query.go | 8 +- x/uibc/quota/ibc_middleware.go | 82 +++++++++++ x/uibc/quota/ibc_module.go | 137 ------------------ x/uibc/quota/ics4_wrapper.go | 56 ------- .../quota/{keeper => }/intest/genesis_test.go | 0 .../{keeper => }/intest/grpc_query_test.go | 0 .../{keeper => }/intest/msg_server_test.go | 0 .../quota/{keeper => }/intest/quota_test.go | 0 .../quota/{keeper => }/intest/suite_test.go | 10 +- x/uibc/quota/{keeper => }/keeper.go | 12 +- x/uibc/quota/{keeper => }/keys.go | 2 +- x/uibc/quota/{keeper => }/migrations.go | 2 +- x/uibc/quota/{keeper => }/mocks_test.go | 2 +- x/uibc/quota/{keeper => }/msg_server.go | 6 +- x/uibc/quota/{keeper => }/params.go | 2 +- x/uibc/quota/{keeper => }/params_test.go | 2 +- x/uibc/quota/{keeper => }/quota.go | 6 +- x/uibc/quota/{keeper => }/quota_test.go | 2 +- x/uibc/quota/{keeper => }/unit_test.go | 2 +- x/uibc/uics20/ibc_module.go | 89 ++++++++++++ x/uibc/uics20/ics4_wrapper.go | 39 +++++ x/uibc/{quota => uics20}/ics4_wrapper_test.go | 9 +- 26 files changed, 269 insertions(+), 254 deletions(-) rename x/uibc/quota/{keeper => }/genesis.go (84%) rename x/uibc/quota/{keeper => }/grpc_query.go (92%) create mode 100644 x/uibc/quota/ibc_middleware.go delete mode 100644 x/uibc/quota/ibc_module.go delete mode 100644 x/uibc/quota/ics4_wrapper.go rename x/uibc/quota/{keeper => }/intest/genesis_test.go (100%) rename x/uibc/quota/{keeper => }/intest/grpc_query_test.go (100%) rename x/uibc/quota/{keeper => }/intest/msg_server_test.go (100%) rename x/uibc/quota/{keeper => }/intest/quota_test.go (100%) rename x/uibc/quota/{keeper => }/intest/suite_test.go (91%) rename x/uibc/quota/{keeper => }/keeper.go (89%) rename x/uibc/quota/{keeper => }/keys.go (97%) rename x/uibc/quota/{keeper => }/migrations.go (97%) rename x/uibc/quota/{keeper => }/mocks_test.go (98%) rename x/uibc/quota/{keeper => }/msg_server.go (94%) rename x/uibc/quota/{keeper => }/params.go (99%) rename x/uibc/quota/{keeper => }/params_test.go (99%) rename x/uibc/quota/{keeper => }/quota.go (98%) rename x/uibc/quota/{keeper => }/quota_test.go (99%) rename x/uibc/quota/{keeper => }/unit_test.go (99%) create mode 100644 x/uibc/uics20/ibc_module.go create mode 100644 x/uibc/uics20/ics4_wrapper.go rename x/uibc/{quota => uics20}/ics4_wrapper_test.go (94%) diff --git a/app/app.go b/app/app.go index c6d8fcd217..a10da828d1 100644 --- a/app/app.go +++ b/app/app.go @@ -147,7 +147,7 @@ import ( uibcmodule "github.com/umee-network/umee/v6/x/uibc/module" uibcoracle "github.com/umee-network/umee/v6/x/uibc/oracle" uibcquota "github.com/umee-network/umee/v6/x/uibc/quota" - uibcquotakeeper "github.com/umee-network/umee/v6/x/uibc/quota/keeper" + "github.com/umee-network/umee/v6/x/uibc/uics20" "github.com/umee-network/umee/v6/x/metoken" metokenkeeper "github.com/umee-network/umee/v6/x/metoken/keeper" @@ -275,7 +275,7 @@ type UmeeApp struct { LeverageKeeper leveragekeeper.Keeper IncentiveKeeper incentivekeeper.Keeper OracleKeeper oraclekeeper.Keeper - UIbcQuotaKeeperB uibcquotakeeper.Builder + UIbcQuotaKeeperB uibcquota.KeeperBuilder UGovKeeperB ugovkeeper.Builder MetokenKeeperB metokenkeeper.Builder @@ -549,7 +549,7 @@ func New( ) // UIbcQuotaKeeper implements ibcporttypes.ICS4Wrapper - app.UIbcQuotaKeeperB = uibcquotakeeper.NewKeeperBuilder( + app.UIbcQuotaKeeperB = uibcquota.NewKeeperBuilder( appCodec, keys[uibc.StoreKey], app.LeverageKeeper, uibcoracle.FromUmeeAvgPriceOracle(app.OracleKeeper), app.UGovKeeperB.EmergencyGroup, ) @@ -566,7 +566,7 @@ func New( - IBC Rate Limit Middleware **********/ - quotaICS4 := uibcquota.NewICS4(app.IBCKeeper.ChannelKeeper, app.UIbcQuotaKeeperB) + quotaICS4 := uics20.NewICS4(app.IBCKeeper.ChannelKeeper, app.UIbcQuotaKeeperB) // Create Transfer Keeper and pass IBCFeeKeeper as expected Channel and PortKeeper // since fee middleware will wrap the IBCKeeper for underlying application. @@ -581,7 +581,7 @@ func New( var transferStack ibcporttypes.IBCModule transferStack = ibctransfer.NewIBCModule(app.IBCTransferKeeper) // transferStack = ibcfee.NewIBCMiddleware(transferStack, app.IBCFeeKeeper) - transferStack = uibcquota.NewICS20Module(transferStack, app.UIbcQuotaKeeperB, appCodec) + transferStack = uics20.NewICS20Module(transferStack, app.UIbcQuotaKeeperB, appCodec) // Create Interchain Accounts Controller Stack // SendPacket, since it is originating from the application to core IBC: diff --git a/x/uibc/module/abci.go b/x/uibc/module/abci.go index e5479e3a0c..5db34063fa 100644 --- a/x/uibc/module/abci.go +++ b/x/uibc/module/abci.go @@ -6,23 +6,22 @@ import ( abci "github.com/cometbft/cometbft/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/umee-network/umee/v6/x/uibc/quota/keeper" + "github.com/umee-network/umee/v6/x/uibc/quota" ) // BeginBlock implements BeginBlock for the x/uibc module. -func BeginBlock(ctx sdk.Context, k keeper.Keeper) { +func BeginBlock(ctx sdk.Context, k quota.Keeper) { logger := ctx.Logger().With("module", "uibc") quotaExpires, err := k.GetExpire() if err != nil { - // TODO, use logger as argument - logger.Error("can't get quota exipre", "error", err) + logger.Error("can't get quota expire", "error", err) return } // reset quotas if quotaExpires == nil || quotaExpires.Before(ctx.BlockTime()) { if err = k.ResetAllQuotas(); err != nil { - logger.Error("can't get quota exipre", "error", err) + logger.Error("can't get quota expire", "error", err) } else { logger.Info("IBC Quota Reset") ctx.EventManager().EmitEvent( diff --git a/x/uibc/module/module.go b/x/uibc/module/module.go index eb2063ff81..712c546dbb 100644 --- a/x/uibc/module/module.go +++ b/x/uibc/module/module.go @@ -14,9 +14,9 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" "github.com/umee-network/umee/v6/util" - ibctransfer "github.com/umee-network/umee/v6/x/uibc" + "github.com/umee-network/umee/v6/x/uibc" "github.com/umee-network/umee/v6/x/uibc/client/cli" - "github.com/umee-network/umee/v6/x/uibc/quota/keeper" + "github.com/umee-network/umee/v6/x/uibc/quota" ) var ( @@ -35,7 +35,7 @@ func NewAppModuleBasic(cdc codec.Codec) AppModuleBasic { // DefaultGenesis implements module.AppModuleBasic func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { - return cdc.MustMarshalJSON(ibctransfer.DefaultGenesisState()) + return cdc.MustMarshalJSON(uibc.DefaultGenesisState()) } // GetQueryCmd implements module.AppModuleBasic @@ -50,31 +50,31 @@ func (AppModuleBasic) GetTxCmd() *cobra.Command { // Name implements module.AppModuleBasic func (AppModuleBasic) Name() string { - return ibctransfer.ModuleName + return uibc.ModuleName } // RegisterGRPCGatewayRoutes implements module.AppModuleBasic func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { - err := ibctransfer.RegisterQueryHandlerClient( - context.Background(), mux, ibctransfer.NewQueryClient(clientCtx)) + err := uibc.RegisterQueryHandlerClient( + context.Background(), mux, uibc.NewQueryClient(clientCtx)) util.Panic(err) } // RegisterInterfaces implements module.AppModuleBasic func (AppModuleBasic) RegisterInterfaces(registry codectypes.InterfaceRegistry) { - ibctransfer.RegisterInterfaces(registry) + uibc.RegisterInterfaces(registry) } // RegisterLegacyAminoCodec implements module.AppModuleBasic func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { - ibctransfer.RegisterLegacyAminoCodec(cdc) + uibc.RegisterLegacyAminoCodec(cdc) } // ValidateGenesis implements module.AppModuleBasic func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, _ client.TxEncodingConfig, bz json.RawMessage) error { - var gs ibctransfer.GenesisState + var gs uibc.GenesisState if err := cdc.UnmarshalJSON(bz, &gs); err != nil { - return fmt.Errorf("failed to unmarshal %s genesis state: %w", ibctransfer.ModuleName, err) + return fmt.Errorf("failed to unmarshal %s genesis state: %w", uibc.ModuleName, err) } return gs.Validate() @@ -83,10 +83,10 @@ func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, _ client.TxEncodingCo // AppModule represents the AppModule for this module type AppModule struct { AppModuleBasic - kb keeper.Builder + kb quota.KeeperBuilder } -func NewAppModule(cdc codec.Codec, kb keeper.Builder) AppModule { +func NewAppModule(cdc codec.Codec, kb quota.KeeperBuilder) AppModule { return AppModule{ AppModuleBasic: NewAppModuleBasic(cdc), kb: kb, @@ -101,7 +101,7 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw // InitGenesis implements module.AppModule func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate { - var genState ibctransfer.GenesisState + var genState uibc.GenesisState cdc.MustUnmarshalJSON(data, &genState) am.kb.InitGenesis(ctx, genState) @@ -118,8 +118,8 @@ func (AppModule) RegisterInvariants(sdk.InvariantRegistry) {} // RegisterServices implements module.AppModule func (am AppModule) RegisterServices(cfg module.Configurator) { - ibctransfer.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.kb)) - ibctransfer.RegisterQueryServer(cfg.QueryServer(), keeper.NewQuerier(am.kb)) + uibc.RegisterMsgServer(cfg.MsgServer(), quota.NewMsgServerImpl(am.kb)) + uibc.RegisterQueryServer(cfg.QueryServer(), quota.NewQuerier(am.kb)) } // BeginBlock executes all ABCI BeginBlock logic respective to the x/uibc module. diff --git a/x/uibc/quota/keeper/genesis.go b/x/uibc/quota/genesis.go similarity index 84% rename from x/uibc/quota/keeper/genesis.go rename to x/uibc/quota/genesis.go index da55d7f231..8b2370e98e 100644 --- a/x/uibc/quota/keeper/genesis.go +++ b/x/uibc/quota/genesis.go @@ -1,4 +1,4 @@ -package keeper +package quota import ( sdk "github.com/cosmos/cosmos-sdk/types" @@ -8,7 +8,7 @@ import ( // InitGenesis initializes the x/uibc module's state from a provided genesis // state. -func (kb Builder) InitGenesis(ctx sdk.Context, genState uibc.GenesisState) { +func (kb KeeperBuilder) InitGenesis(ctx sdk.Context, genState uibc.GenesisState) { k := kb.Keeper(&ctx) err := k.SetParams(genState.Params) util.Panic(err) @@ -23,7 +23,7 @@ func (kb Builder) InitGenesis(ctx sdk.Context, genState uibc.GenesisState) { } // ExportGenesis returns the x/uibc module's exported genesis state. -func (kb Builder) ExportGenesis(ctx sdk.Context) *uibc.GenesisState { +func (kb KeeperBuilder) ExportGenesis(ctx sdk.Context) *uibc.GenesisState { k := kb.Keeper(&ctx) outflows, err := k.GetAllOutflows() util.Panic(err) diff --git a/x/uibc/quota/keeper/grpc_query.go b/x/uibc/quota/grpc_query.go similarity index 92% rename from x/uibc/quota/keeper/grpc_query.go rename to x/uibc/quota/grpc_query.go index bcc5e891b6..62984e58cc 100644 --- a/x/uibc/quota/keeper/grpc_query.go +++ b/x/uibc/quota/grpc_query.go @@ -1,4 +1,4 @@ -package keeper +package quota import ( context "context" @@ -11,11 +11,11 @@ var _ uibc.QueryServer = Querier{} // Querier implements a QueryServer for the x/uibc module. type Querier struct { - Builder + KeeperBuilder } -func NewQuerier(kb Builder) Querier { - return Querier{Builder: kb} +func NewQuerier(kb KeeperBuilder) Querier { + return Querier{KeeperBuilder: kb} } // Params returns params of the x/uibc module. diff --git a/x/uibc/quota/ibc_middleware.go b/x/uibc/quota/ibc_middleware.go new file mode 100644 index 0000000000..57e2bb1ef5 --- /dev/null +++ b/x/uibc/quota/ibc_middleware.go @@ -0,0 +1,82 @@ +package quota + +import ( + "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + ics20types "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + "github.com/cosmos/ibc-go/v7/modules/core/exported" + + ibcutil "github.com/umee-network/umee/v6/util/ibc" + "github.com/umee-network/umee/v6/util/sdkutil" + "github.com/umee-network/umee/v6/x/uibc" +) + +func (k Keeper) IBCOnSendPacket(packet []byte) error { + params := k.GetParams() + + if !params.IbcStatus.IBCTransferEnabled() { + return ics20types.ErrSendDisabled + } + + funds, denom, err := ibcutil.GetFundsFromPacket(packet) + if err != nil { + return errors.Wrap(err, "bad packet in rate limit's SendPacket") + } + + if params.IbcStatus.OutflowQuotaEnabled() { + if err := k.CheckAndUpdateQuota(denom, funds); err != nil { + return errors.Wrap(err, "sendPacket over the IBC Quota") + } + } + + return nil +} + +func (k Keeper) IBCOnRecvPacket(packet channeltypes.Packet) exported.Acknowledgement { + params := k.GetParams() + if !params.IbcStatus.IBCTransferEnabled() { + return channeltypes.NewErrorAcknowledgement(ics20types.ErrReceiveDisabled) + } + + if params.IbcStatus.OutflowQuotaEnabled() { + var data ics20types.FungibleTokenPacketData + if err := ics20types.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil { + ackErr := sdkerrors.ErrInvalidType.Wrap("cannot unmarshal ICS-20 transfer packet data") + return channeltypes.NewErrorAcknowledgement(ackErr) + } + + isSourceChain := ics20types.SenderChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data.Denom) + ackErr := k.RecordIBCInflow(packet, data.Denom, data.Amount, isSourceChain) + if ackErr != nil { + return ackErr + } + } + + return nil +} + +// IBCRevertQuotaUpdate must be called on packet acknnowledgemenet error or timeout to revert +// necessary changes. +func (k Keeper) IBCRevertQuotaUpdate(amount, denom string) { + params := k.GetParams() + if !params.IbcStatus.OutflowQuotaEnabled() { + return + } + if err := k.revertQuotaUpdateStr(amount, denom); err != nil { + k.ctx.Logger().Error("revert quota update error", "err", err) + sdkutil.Emit(k.ctx, &uibc.EventBadRevert{ + FailureType: "ibc-ack", + Packet: amount + denom, + }) + } +} + +func (k Keeper) revertQuotaUpdateStr(amount, denom string) error { + amountInt, ok := sdkmath.NewIntFromString(amount) + if !ok { + return sdkerrors.ErrInvalidRequest.Wrapf("invalid transfer amount %s", amount) + } + return k.UndoUpdateQuota(denom, amountInt) +} diff --git a/x/uibc/quota/ibc_module.go b/x/uibc/quota/ibc_module.go deleted file mode 100644 index bf87008e6a..0000000000 --- a/x/uibc/quota/ibc_module.go +++ /dev/null @@ -1,137 +0,0 @@ -package quota - -import ( - "encoding/json" - - "cosmossdk.io/errors" - sdkmath "cosmossdk.io/math" - transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" - channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" - porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types" - "github.com/cosmos/ibc-go/v7/modules/core/exported" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - - "github.com/umee-network/umee/v6/util/sdkutil" - "github.com/umee-network/umee/v6/x/uibc" - "github.com/umee-network/umee/v6/x/uibc/quota/keeper" -) - -var _ porttypes.IBCModule = ICS20Module{} - -// ICS20Module overwrites OnAcknowledgementPacket and OnTimeoutPacket to revert -// quota update on acknowledgement error or timeout. -type ICS20Module struct { - porttypes.IBCModule - kb keeper.Builder - cdc codec.JSONCodec -} - -// NewICS20Module is an IBCMiddlware constructor. -// `app` must be an ICS20 app. -func NewICS20Module(app porttypes.IBCModule, k keeper.Builder, cdc codec.JSONCodec) ICS20Module { - return ICS20Module{ - IBCModule: app, - kb: k, - cdc: cdc, - } -} - -// OnRecvPacket implements types.Middleware -func (im ICS20Module) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress, -) exported.Acknowledgement { - params := im.kb.Keeper(&ctx).GetParams() - if !params.IbcStatus.IBCTransferEnabled() { - return channeltypes.NewErrorAcknowledgement(transfertypes.ErrReceiveDisabled) - } - - if params.IbcStatus.OutflowQuotaEnabled() { - var data transfertypes.FungibleTokenPacketData - if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil { - ackErr := sdkerrors.ErrInvalidType.Wrap("cannot unmarshal ICS-20 transfer packet data") - return channeltypes.NewErrorAcknowledgement(ackErr) - } - - isSourceChain := transfertypes.SenderChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data.Denom) - ackErr := im.kb.Keeper(&ctx).RecordIBCInflow(ctx, packet, data.Denom, data.Amount, isSourceChain) - if ackErr != nil { - return ackErr - } - } - - return im.IBCModule.OnRecvPacket(ctx, packet, relayer) -} - -// OnAcknowledgementPacket implements types.Middleware -func (im ICS20Module) OnAcknowledgementPacket(ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, - relayer sdk.AccAddress, -) error { - var ack channeltypes.Acknowledgement - if err := im.cdc.UnmarshalJSON(acknowledgement, &ack); err != nil { - return errors.Wrap(err, "cannot unmarshal ICS-20 transfer packet acknowledgement") - } - if _, ok := ack.Response.(*channeltypes.Acknowledgement_Error); ok { - params := im.kb.Keeper(&ctx).GetParams() - if params.IbcStatus.OutflowQuotaEnabled() { - err := im.revertQuotaUpdate(ctx, packet.Data) - emitOnRevertQuota(&ctx, "acknowledgement", packet.Data, err) - } - } - - return im.IBCModule.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer) -} - -// OnTimeoutPacket implements types.Middleware -func (im ICS20Module) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error { - err := im.revertQuotaUpdate(ctx, packet.Data) - emitOnRevertQuota(&ctx, "timeout", packet.Data, err) - - return im.IBCModule.OnTimeoutPacket(ctx, packet, relayer) -} - -// revertQuotaUpdate must be called on packet acknnowledgemenet error to revert necessary changes. -func (im ICS20Module) revertQuotaUpdate( - ctx sdk.Context, - packetData []byte, -) error { - var data transfertypes.FungibleTokenPacketData - if err := im.cdc.UnmarshalJSON(packetData, &data); err != nil { - return errors.Wrap(err, - "cannot unmarshal ICS-20 transfer packet data") - } - - amount, ok := sdkmath.NewIntFromString(data.Amount) - if !ok { - return sdkerrors.ErrInvalidRequest.Wrapf("invalid transfer amount %s", data.Amount) - } - - return im.kb.Keeper(&ctx).UndoUpdateQuota(data.Denom, amount) -} - -func ValidateReceiverAddress(packet channeltypes.Packet) error { - var packetData transfertypes.FungibleTokenPacketData - if err := json.Unmarshal(packet.GetData(), &packetData); err != nil { - return err - } - if len(packetData.Receiver) >= 4096 { - return sdkerrors.ErrInvalidAddress.Wrapf( - "IBC Receiver address too long. Max supported length is %d", 4096, - ) - } - return nil -} - -// emitOnRevertQuota emits events related to quota update revert. -// packetData is ICS 20 packet data bytes. -func emitOnRevertQuota(ctx *sdk.Context, failureType string, packetData []byte, err error) { - if err == nil { - return - } - ctx.Logger().Error("revert quota update error", "err", err) - sdkutil.Emit(ctx, &uibc.EventBadRevert{ - FailureType: failureType, - Packet: string(packetData), - }) -} diff --git a/x/uibc/quota/ics4_wrapper.go b/x/uibc/quota/ics4_wrapper.go deleted file mode 100644 index f87649faa6..0000000000 --- a/x/uibc/quota/ics4_wrapper.go +++ /dev/null @@ -1,56 +0,0 @@ -package quota - -import ( - "cosmossdk.io/errors" - ics20types "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" - clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" - porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types" - - sdk "github.com/cosmos/cosmos-sdk/types" - capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" - - ibcutil "github.com/umee-network/umee/v6/util/ibc" - "github.com/umee-network/umee/v6/x/uibc/quota/keeper" -) - -// ICS4 wraps SendPacket to check IBC quota. -type ICS4 struct { - porttypes.ICS4Wrapper - - kb keeper.Builder -} - -func NewICS4(parent porttypes.ICS4Wrapper, kb keeper.Builder) ICS4 { - return ICS4{parent, kb} -} - -// SendPacket implements types.Middleware -func (q ICS4) SendPacket( - ctx sdk.Context, - chanCap *capabilitytypes.Capability, - sourcePort string, - sourceChannel string, - timeoutHeight clienttypes.Height, - timeoutTimestamp uint64, - data []byte, -) (uint64, error) { - k := q.kb.Keeper(&ctx) - params := k.GetParams() - - if !params.IbcStatus.IBCTransferEnabled() { - return 0, ics20types.ErrSendDisabled - } - - funds, denom, err := ibcutil.GetFundsFromPacket(data) - if err != nil { - return 0, errors.Wrap(err, "bad packet in rate limit's SendPacket") - } - - if params.IbcStatus.OutflowQuotaEnabled() { - if err := k.CheckAndUpdateQuota(denom, funds); err != nil { - return 0, errors.Wrap(err, "sendPacket over the IBC Quota") - } - } - - return q.ICS4Wrapper.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) -} diff --git a/x/uibc/quota/keeper/intest/genesis_test.go b/x/uibc/quota/intest/genesis_test.go similarity index 100% rename from x/uibc/quota/keeper/intest/genesis_test.go rename to x/uibc/quota/intest/genesis_test.go diff --git a/x/uibc/quota/keeper/intest/grpc_query_test.go b/x/uibc/quota/intest/grpc_query_test.go similarity index 100% rename from x/uibc/quota/keeper/intest/grpc_query_test.go rename to x/uibc/quota/intest/grpc_query_test.go diff --git a/x/uibc/quota/keeper/intest/msg_server_test.go b/x/uibc/quota/intest/msg_server_test.go similarity index 100% rename from x/uibc/quota/keeper/intest/msg_server_test.go rename to x/uibc/quota/intest/msg_server_test.go diff --git a/x/uibc/quota/keeper/intest/quota_test.go b/x/uibc/quota/intest/quota_test.go similarity index 100% rename from x/uibc/quota/keeper/intest/quota_test.go rename to x/uibc/quota/intest/quota_test.go diff --git a/x/uibc/quota/keeper/intest/suite_test.go b/x/uibc/quota/intest/suite_test.go similarity index 91% rename from x/uibc/quota/keeper/intest/suite_test.go rename to x/uibc/quota/intest/suite_test.go index a9612ac3d1..c8369845bd 100644 --- a/x/uibc/quota/keeper/intest/suite_test.go +++ b/x/uibc/quota/intest/suite_test.go @@ -24,7 +24,7 @@ import ( "github.com/umee-network/umee/v6/tests/tsdk" ugovmocks "github.com/umee-network/umee/v6/x/ugov/mocks" "github.com/umee-network/umee/v6/x/uibc" - "github.com/umee-network/umee/v6/x/uibc/quota/keeper" + "github.com/umee-network/umee/v6/x/uibc/quota" ) const ( @@ -71,7 +71,7 @@ func initTestSuite(t *testing.T) *IntTestSuite { }) queryHelper := baseapp.NewQueryServerTestHelper(ctx, app.InterfaceRegistry()) - uibc.RegisterQueryServer(queryHelper, keeper.NewQuerier(app.UIbcQuotaKeeperB)) + uibc.RegisterQueryServer(queryHelper, quota.NewQuerier(app.UIbcQuotaKeeperB)) sh := testutil.NewHelper(t, ctx, app.StakingKeeper) sh.Denom = bondDenom @@ -93,7 +93,7 @@ func initTestSuite(t *testing.T) *IntTestSuite { s.app = app s.ctx = ctx s.queryClient = uibc.NewQueryClient(queryHelper) - s.msgServer = keeper.NewMsgServerImpl(app.UIbcQuotaKeeperB) + s.msgServer = quota.NewMsgServerImpl(app.UIbcQuotaKeeperB) return s } @@ -104,10 +104,10 @@ func initKeeper( cdc codec.BinaryCodec, leverage uibc.Leverage, oracle uibc.Oracle, -) (sdk.Context, keeper.Keeper) { +) (sdk.Context, quota.Keeper) { storeKey := storetypes.NewMemoryStoreKey("quota") ctx, _ := tsdk.NewCtxOneStore(t, storeKey) eg := ugovmocks.NewSimpleEmergencyGroupBuilder() - kb := keeper.NewKeeperBuilder(cdc, storeKey, leverage, oracle, eg) + kb := quota.NewKeeperBuilder(cdc, storeKey, leverage, oracle, eg) return ctx, kb.Keeper(&ctx) } diff --git a/x/uibc/quota/keeper/keeper.go b/x/uibc/quota/keeper.go similarity index 89% rename from x/uibc/quota/keeper/keeper.go rename to x/uibc/quota/keeper.go index 23802ea030..05982d871e 100644 --- a/x/uibc/quota/keeper/keeper.go +++ b/x/uibc/quota/keeper.go @@ -1,4 +1,4 @@ -package keeper +package quota import ( "time" @@ -13,8 +13,8 @@ import ( "github.com/umee-network/umee/v6/x/uibc" ) -// Builder constructs Keeper by perparing all related dependencies (notably the store). -type Builder struct { +// KeeperBuilder constructs Keeper by perparing all related dependencies (notably the store). +type KeeperBuilder struct { storeKey storetypes.StoreKey cdc codec.BinaryCodec leverage uibc.Leverage @@ -25,8 +25,8 @@ type Builder struct { func NewKeeperBuilder( cdc codec.BinaryCodec, key storetypes.StoreKey, leverage uibc.Leverage, oracle uibc.Oracle, ugov ugov.EmergencyGroupBuilder, -) Builder { - return Builder{ +) KeeperBuilder { + return KeeperBuilder{ cdc: cdc, storeKey: key, leverage: leverage, @@ -35,7 +35,7 @@ func NewKeeperBuilder( } } -func (kb Builder) Keeper(ctx *sdk.Context) Keeper { +func (kb KeeperBuilder) Keeper(ctx *sdk.Context) Keeper { return Keeper{ store: ctx.KVStore(kb.storeKey), leverage: kb.leverage, diff --git a/x/uibc/quota/keeper/keys.go b/x/uibc/quota/keys.go similarity index 97% rename from x/uibc/quota/keeper/keys.go rename to x/uibc/quota/keys.go index b27cfc795a..c0adbf6720 100644 --- a/x/uibc/quota/keeper/keys.go +++ b/x/uibc/quota/keys.go @@ -1,4 +1,4 @@ -package keeper +package quota import ( "github.com/umee-network/umee/v6/util" diff --git a/x/uibc/quota/keeper/migrations.go b/x/uibc/quota/migrations.go similarity index 97% rename from x/uibc/quota/keeper/migrations.go rename to x/uibc/quota/migrations.go index 81450fa80e..9a00758da3 100644 --- a/x/uibc/quota/keeper/migrations.go +++ b/x/uibc/quota/migrations.go @@ -1,4 +1,4 @@ -package keeper +package quota import sdk "github.com/cosmos/cosmos-sdk/types" diff --git a/x/uibc/quota/keeper/mocks_test.go b/x/uibc/quota/mocks_test.go similarity index 98% rename from x/uibc/quota/keeper/mocks_test.go rename to x/uibc/quota/mocks_test.go index bf16448878..ce8c4cb0e9 100644 --- a/x/uibc/quota/keeper/mocks_test.go +++ b/x/uibc/quota/mocks_test.go @@ -1,6 +1,6 @@ // Simple mocks for unit tests -package keeper +package quota import ( "errors" diff --git a/x/uibc/quota/keeper/msg_server.go b/x/uibc/quota/msg_server.go similarity index 94% rename from x/uibc/quota/keeper/msg_server.go rename to x/uibc/quota/msg_server.go index c199995999..ec1dc5006a 100644 --- a/x/uibc/quota/keeper/msg_server.go +++ b/x/uibc/quota/msg_server.go @@ -1,4 +1,4 @@ -package keeper +package quota import ( "context" @@ -11,11 +11,11 @@ import ( var _ uibc.MsgServer = msgServer{} type msgServer struct { - kb Builder + kb KeeperBuilder } // NewMsgServerImpl returns an implementation of uibc.MsgServer -func NewMsgServerImpl(kb Builder) uibc.MsgServer { +func NewMsgServerImpl(kb KeeperBuilder) uibc.MsgServer { return &msgServer{kb: kb} } diff --git a/x/uibc/quota/keeper/params.go b/x/uibc/quota/params.go similarity index 99% rename from x/uibc/quota/keeper/params.go rename to x/uibc/quota/params.go index e3f3e44516..160298f79c 100644 --- a/x/uibc/quota/keeper/params.go +++ b/x/uibc/quota/params.go @@ -1,4 +1,4 @@ -package keeper +package quota import ( "errors" diff --git a/x/uibc/quota/keeper/params_test.go b/x/uibc/quota/params_test.go similarity index 99% rename from x/uibc/quota/keeper/params_test.go rename to x/uibc/quota/params_test.go index ffde415ac4..3e53a93c82 100644 --- a/x/uibc/quota/keeper/params_test.go +++ b/x/uibc/quota/params_test.go @@ -1,4 +1,4 @@ -package keeper +package quota import ( "testing" diff --git a/x/uibc/quota/keeper/quota.go b/x/uibc/quota/quota.go similarity index 98% rename from x/uibc/quota/keeper/quota.go rename to x/uibc/quota/quota.go index 09980d985d..520b224faa 100644 --- a/x/uibc/quota/keeper/quota.go +++ b/x/uibc/quota/quota.go @@ -1,4 +1,4 @@ -package keeper +package quota import ( "strings" @@ -229,7 +229,7 @@ func (k Keeper) UndoUpdateQuota(denom string, amount sdkmath.Int) error { } // RecordIBCInflow will save the inflow amount if token is registered otherwise it will skip -func (k Keeper) RecordIBCInflow(ctx sdk.Context, +func (k Keeper) RecordIBCInflow( packet channeltypes.Packet, dataDenom, dataAmount string, isSourceChain bool, ) exported.Acknowledgement { // if chain is recevier and sender chain is source then we need create ibc_denom (ibc/hash(channel,denom)) to @@ -241,7 +241,7 @@ func (k Keeper) RecordIBCInflow(ctx sdk.Context, prefixedDenom := sourcePrefix + dataDenom // construct the denomination trace from the full raw denomination and get the ibc_denom ibcDenom := transfertypes.ParseDenomTrace(prefixedDenom).IBCDenom() - ts, err := k.leverage.GetTokenSettings(ctx, ibcDenom) + ts, err := k.leverage.GetTokenSettings(*k.ctx, ibcDenom) if err != nil { // skip if token is not a registered token on leverage if ltypes.ErrNotRegisteredToken.Is(err) { diff --git a/x/uibc/quota/keeper/quota_test.go b/x/uibc/quota/quota_test.go similarity index 99% rename from x/uibc/quota/keeper/quota_test.go rename to x/uibc/quota/quota_test.go index e6fce7096f..2337173072 100644 --- a/x/uibc/quota/keeper/quota_test.go +++ b/x/uibc/quota/quota_test.go @@ -1,4 +1,4 @@ -package keeper +package quota import ( "testing" diff --git a/x/uibc/quota/keeper/unit_test.go b/x/uibc/quota/unit_test.go similarity index 99% rename from x/uibc/quota/keeper/unit_test.go rename to x/uibc/quota/unit_test.go index 7f766bbc1c..4c84969392 100644 --- a/x/uibc/quota/keeper/unit_test.go +++ b/x/uibc/quota/unit_test.go @@ -1,4 +1,4 @@ -package keeper +package quota import ( "testing" diff --git a/x/uibc/uics20/ibc_module.go b/x/uibc/uics20/ibc_module.go new file mode 100644 index 0000000000..7ef20c8ee1 --- /dev/null +++ b/x/uibc/uics20/ibc_module.go @@ -0,0 +1,89 @@ +package uics20 + +import ( + "cosmossdk.io/errors" + transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types" + "github.com/cosmos/ibc-go/v7/modules/core/exported" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/umee-network/umee/v6/x/uibc/quota" +) + +var _ porttypes.IBCModule = ICS20Module{} + +// ICS20Module implements ibcporttypes.IBCModule for ICS20 transfer middleware. +// It overwrites OnAcknowledgementPacket and OnTimeoutPacket to revert +// quota update on acknowledgement error or timeout. +type ICS20Module struct { + porttypes.IBCModule + kb quota.KeeperBuilder + cdc codec.JSONCodec +} + +// NewICS20Module is an IBCMiddlware constructor. +// `app` must be an ICS20 app. +func NewICS20Module(app porttypes.IBCModule, k quota.KeeperBuilder, cdc codec.JSONCodec) ICS20Module { + return ICS20Module{ + IBCModule: app, + kb: k, + cdc: cdc, + } +} + +// OnRecvPacket is called when a receiver chain receives a packet from SendPacket. +func (im ICS20Module) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress, +) exported.Acknowledgement { + qk := im.kb.Keeper(&ctx) + if ackResp := qk.IBCOnRecvPacket(packet); ackResp != nil && !ackResp.Success() { + return ackResp + } + + return im.IBCModule.OnRecvPacket(ctx, packet, relayer) +} + +// OnAcknowledgementPacket is called on the packet sender chain, once the receiver acknowledged +// the packet reception. +func (im ICS20Module) OnAcknowledgementPacket( + ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress, +) error { + var ack channeltypes.Acknowledgement + if err := im.cdc.UnmarshalJSON(acknowledgement, &ack); err != nil { + return errors.Wrap(err, "cannot unmarshal ICS-20 transfer packet acknowledgement") + } + if _, isErr := ack.Response.(*channeltypes.Acknowledgement_Error); isErr { + // we don't return to propagate the ack error to the other layers + im.onAckErr(&ctx, packet) + } + + return im.IBCModule.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer) +} + +// OnTimeoutPacket implements types.Middleware +func (im ICS20Module) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error { + im.onAckErr(&ctx, packet) + return im.IBCModule.OnTimeoutPacket(ctx, packet, relayer) +} + +func (im ICS20Module) onAckErr(ctx *sdk.Context, packet channeltypes.Packet) { + ftData, err := im.deserializeFTData(packet) + if err != nil { + // we only log error, because we want to propagate the ack to other layers. + ctx.Logger().Error("can't revert quota update", "err", err) + } + qk := im.kb.Keeper(ctx) + qk.IBCRevertQuotaUpdate(ftData.Amount, ftData.Denom) +} + +func (im ICS20Module) deserializeFTData( + packet channeltypes.Packet, +) (d transfertypes.FungibleTokenPacketData, err error) { + if err = im.cdc.UnmarshalJSON(packet.GetData(), &d); err != nil { + err = errors.Wrap(err, + "cannot unmarshal ICS-20 transfer packet data") + } + return +} diff --git a/x/uibc/uics20/ics4_wrapper.go b/x/uibc/uics20/ics4_wrapper.go new file mode 100644 index 0000000000..7ffd043926 --- /dev/null +++ b/x/uibc/uics20/ics4_wrapper.go @@ -0,0 +1,39 @@ +package uics20 + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types" + + "github.com/umee-network/umee/v6/x/uibc/quota" +) + +// ICS4 implements porttypes.ICS4Wrapper (middleware to send packets and acknowledgements) and +// overwrites SendPacket to check IBC quota. +type ICS4 struct { + porttypes.ICS4Wrapper + + quotaKB quota.KeeperBuilder +} + +func NewICS4(parent porttypes.ICS4Wrapper, kb quota.KeeperBuilder) ICS4 { + return ICS4{parent, kb} +} + +// SendPacket implements types.Middleware +func (q ICS4) SendPacket( + ctx sdk.Context, + chanCap *capabilitytypes.Capability, + sourcePort string, + sourceChannel string, + timeoutHeight clienttypes.Height, + timeoutTimestamp uint64, + data []byte, +) (uint64, error) { + k := q.quotaKB.Keeper(&ctx) + if err := k.IBCOnSendPacket(data); err != nil { + return 0, err + } + return q.ICS4Wrapper.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) +} diff --git a/x/uibc/quota/ics4_wrapper_test.go b/x/uibc/uics20/ics4_wrapper_test.go similarity index 94% rename from x/uibc/quota/ics4_wrapper_test.go rename to x/uibc/uics20/ics4_wrapper_test.go index 141f85a16e..82c5195901 100644 --- a/x/uibc/quota/ics4_wrapper_test.go +++ b/x/uibc/uics20/ics4_wrapper_test.go @@ -1,4 +1,4 @@ -package quota_test +package uics20_test import ( "testing" @@ -23,7 +23,7 @@ import ( "github.com/umee-network/umee/v6/x/uibc" "github.com/umee-network/umee/v6/x/uibc/mocks" "github.com/umee-network/umee/v6/x/uibc/quota" - "github.com/umee-network/umee/v6/x/uibc/quota/keeper" + "github.com/umee-network/umee/v6/x/uibc/uics20" ) type MockICS4Wrapper struct { @@ -53,7 +53,7 @@ func TestSendPacket(t *testing.T) { storeKey := storetypes.NewMemoryStoreKey("quota") ctx, _ := tsdk.NewCtxOneStore(t, storeKey) - kb := keeper.NewKeeperBuilder(codec.NewProtoCodec(nil), storeKey, leverageMock, oracleMock, eg) + kb := quota.NewKeeperBuilder(codec.NewProtoCodec(nil), storeKey, leverageMock, oracleMock, eg) dp := uibc.DefaultParams() keeper := kb.Keeper(&ctx) keeper.SetParams(dp) @@ -62,8 +62,7 @@ func TestSendPacket(t *testing.T) { leverageMock.EXPECT().GetTokenSettings(ctx, "umee").Return(ltypes.Token{}, ltypes.ErrNotRegisteredToken).AnyTimes() oracleMock.EXPECT().Price(ctx, "TEST").Return(sdk.Dec{}, types.ErrMalformedLatestAvgPrice) - // quota ics5 - ics4 := quota.NewICS4(mock, kb) + ics4 := uics20.NewICS4(mock, kb) // error test cases _, err := ics4.SendPacket(ctx, nil, "", "", clienttypes.NewHeight(1, 1), 1, nil)