From 6a57d4d30faa8c79346ff402c1134941cbefabe1 Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Tue, 17 Jan 2023 10:31:07 +0100 Subject: [PATCH] feat! implement token filter IBC middleware (#1219) Closes: https://github.com/celestiaorg/celestia-app/issues/235 This PR creates and wires up a new IBC middleware that acts as a firewall, rejecting all `FungibleTokenPacketData` (i.e. transfer packets) that have a denom which did not originally came from the celestia chain. This simple implemenation will mean that the chain state only consists of the native celestia token. --- app/app.go | 14 ++- x/tokenfilter/ibc_middleware.go | 77 ++++++++++++ x/tokenfilter/ibc_middleware_test.go | 170 +++++++++++++++++++++++++++ x/tokenfilter/keeper.go | 18 +++ 4 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 x/tokenfilter/ibc_middleware.go create mode 100644 x/tokenfilter/ibc_middleware_test.go create mode 100644 x/tokenfilter/keeper.go diff --git a/app/app.go b/app/app.go index 8900cb8750..8e5e5fa0a0 100644 --- a/app/app.go +++ b/app/app.go @@ -92,6 +92,7 @@ import ( blobmodule "github.com/celestiaorg/celestia-app/x/blob" blobmodulekeeper "github.com/celestiaorg/celestia-app/x/blob/keeper" blobmoduletypes "github.com/celestiaorg/celestia-app/x/blob/types" + "github.com/celestiaorg/celestia-app/x/tokenfilter" qgbmodule "github.com/celestiaorg/celestia-app/x/qgb" qgbmodulekeeper "github.com/celestiaorg/celestia-app/x/qgb/keeper" @@ -348,13 +349,20 @@ func New( AddRoute(ibcclienttypes.RouterKey, ibcclient.NewClientProposalHandler(app.IBCKeeper.ClientKeeper)) // Create Transfer Keepers + tokenFilterKeeper := tokenfilter.NewKeeper(app.IBCKeeper.ChannelKeeper) app.TransferKeeper = ibctransferkeeper.NewKeeper( appCodec, keys[ibctransfertypes.StoreKey], app.GetSubspace(ibctransfertypes.ModuleName), - app.IBCKeeper.ChannelKeeper, app.IBCKeeper.ChannelKeeper, &app.IBCKeeper.PortKeeper, + tokenFilterKeeper, app.IBCKeeper.ChannelKeeper, &app.IBCKeeper.PortKeeper, app.AccountKeeper, app.BankKeeper, scopedTransferKeeper, ) transferModule := transfer.NewAppModule(app.TransferKeeper) - transferIBCModule := transfer.NewIBCModule(app.TransferKeeper) + + // transfer stack contains (from top to bottom): + // - Token Filter + // - Transfer + var transferStack ibcporttypes.IBCModule + transferStack = transfer.NewIBCModule(app.TransferKeeper) + transferStack = tokenfilter.NewIBCMiddleware(transferStack) // Create evidence Keeper for to register the IBC light client misbehaviour evidence route evidenceKeeper := evidencekeeper.NewKeeper( @@ -380,7 +388,7 @@ func New( // Create static IBC router, add transfer route, then set and seal it ibcRouter := ibcporttypes.NewRouter() - ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferIBCModule) + ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferStack) app.IBCKeeper.SetRouter(ibcRouter) /**** Module Options ****/ diff --git a/x/tokenfilter/ibc_middleware.go b/x/tokenfilter/ibc_middleware.go new file mode 100644 index 0000000000..3186d4b5b6 --- /dev/null +++ b/x/tokenfilter/ibc_middleware.go @@ -0,0 +1,77 @@ +package tokenfilter + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + transfertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/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" + "github.com/cosmos/ibc-go/v6/modules/core/exported" +) + +const ModuleName = "tokenfilter" + +// tokenFilterMiddleware directly inherits the IBCModule and ICS4Wrapper interfaces. +// Only with OnRecvPacket, does it wrap the underlying implementation with additional +// stateless logic for rejecting the inbound transfer of non-native tokens. This +// middleware is unilateral and no handshake is required. If using this middleware +// on an existing chain, tokens that have been routed through this chain will still +// be allowed to unwrap. +type tokenFilterMiddleware struct { + porttypes.IBCModule +} + +// NewIBCMiddleware creates a new instance of the token filter middleware for +// the transfer module. +func NewIBCMiddleware(ibcModule porttypes.IBCModule) porttypes.IBCModule { + return &tokenFilterMiddleware{ + IBCModule: ibcModule, + } +} + +// OnRecvPacket implements the IBCModule interface. It is called whenever a new packet +// from another chain is received on this chain. Here, the token filter middleware +// unmarshals the FungibleTokenPacketData and checks to see if the denomination being +// transferred to this chain originally came from this chain i.e. is a native token. +// If not, it returns an ErrorAcknowledgement. +func (m *tokenFilterMiddleware) OnRecvPacket( + ctx sdk.Context, + packet channeltypes.Packet, + relayer sdk.AccAddress, +) exported.Acknowledgement { + var data transfertypes.FungibleTokenPacketData + if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil { + // If this happens either a) a user has crafted an invalid packet, b) a + // software developer has connected the middleware to a stack that does + // not have a transfer module, or c) the transfer module has been modified + // to accept other Packets. The best thing we can do here is pass the packet + // on down the stack. + return m.IBCModule.OnRecvPacket(ctx, packet, relayer) + } + + // This checks the first channel and port in the denomination path. If it matches + // our channel and port it means that the token was originally sent from this + // chain. Note that this firewall prevents routing of other transactions through + // the chain so from this logic, the denom has to be a native denom. + if transfertypes.ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data.Denom) { + return m.IBCModule.OnRecvPacket(ctx, packet, relayer) + } + + ackErr := sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "only native denom transfers accepted, got %s", data.Denom) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + transfertypes.EventTypePacket, + sdk.NewAttribute(sdk.AttributeKeyModule, ModuleName), + sdk.NewAttribute(sdk.AttributeKeySender, data.Sender), + sdk.NewAttribute(transfertypes.AttributeKeyReceiver, data.Receiver), + sdk.NewAttribute(transfertypes.AttributeKeyDenom, data.Denom), + sdk.NewAttribute(transfertypes.AttributeKeyAmount, data.Amount), + sdk.NewAttribute(transfertypes.AttributeKeyMemo, data.Memo), + sdk.NewAttribute(transfertypes.AttributeKeyAckSuccess, "false"), + sdk.NewAttribute(transfertypes.AttributeKeyAckError, ackErr.Error()), + ), + ) + + return channeltypes.NewErrorAcknowledgement(ackErr) +} diff --git a/x/tokenfilter/ibc_middleware_test.go b/x/tokenfilter/ibc_middleware_test.go new file mode 100644 index 0000000000..0843774301 --- /dev/null +++ b/x/tokenfilter/ibc_middleware_test.go @@ -0,0 +1,170 @@ +package tokenfilter_test + +import ( + "testing" + + 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" + "github.com/cosmos/ibc-go/v6/modules/core/exported" + + "github.com/celestiaorg/celestia-app/x/tokenfilter" +) + +func TestOnRecvPacket(t *testing.T) { + data := transfertypes.NewFungibleTokenPacketData("portid/channelid/utia", sdk.NewInt(100).String(), "alice", "bob", "gm") + packet := channeltypes.NewPacket(data.GetBytes(), 1, "portid", "channelid", "counterpartyportid", "counterpartychannelid", clienttypes.Height{}, 0) + packetFromOtherChain := channeltypes.NewPacket(data.GetBytes(), 1, "counterpartyportid", "counterpartychannelid", "portid", "channelid", clienttypes.Height{}, 0) + randomPacket := channeltypes.NewPacket([]byte{1, 2, 3, 4}, 1, "portid", "channelid", "counterpartyportid", "counterpartychannelid", clienttypes.Height{}, 0) + + testCases := []struct { + name string + packet channeltypes.Packet + err bool + }{ + { + name: "packet with native token", + packet: packet, + err: false, + }, + { + name: "packet with non-native token", + packet: packetFromOtherChain, + err: true, + }, + { + name: "random packet from a different module", + packet: randomPacket, + err: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + module := &MockIBCModule{t: t, called: false} + middleware := tokenfilter.NewIBCMiddleware(module) + + ctx := sdk.Context{} + ctx = ctx.WithEventManager(sdk.NewEventManager()) + ack := middleware.OnRecvPacket( + ctx, + tc.packet, + []byte{}, + ) + if tc.err { + if module.MethodCalled() { + t.Fatal("expected error but `OnRecvPacket` was called") + } + if ack.Success() { + t.Fatal("expected error acknowledgement but got success") + } + } + }) + } +} + +type MockIBCModule struct { + t *testing.T + called bool +} + +func (m *MockIBCModule) MethodCalled() bool { + return m.called +} + +func (m *MockIBCModule) OnChanOpenInit( + ctx sdk.Context, + order channeltypes.Order, + connectionHops []string, + portID string, + channelID string, + channelCap *capabilitytypes.Capability, + counterparty channeltypes.Counterparty, + version string, +) (string, error) { + m.t.Fatalf("unexpected call to OnChanOpenInit") + return "", nil +} + +func (m *MockIBCModule) OnChanOpenTry( + ctx sdk.Context, + order channeltypes.Order, + connectionHops []string, + portID, + channelID string, + channelCap *capabilitytypes.Capability, + counterparty channeltypes.Counterparty, + counterpartyVersion string, +) (version string, err error) { + m.t.Fatalf("unexpected call to OnChanOpenTry") + return "", nil +} + +func (m *MockIBCModule) OnChanOpenAck( + ctx sdk.Context, + portID, + channelID string, + counterpartyChannelID string, + counterpartyVersion string, +) error { + m.t.Fatalf("unexpected call to OnChanOpenAck") + return nil +} + +func (m *MockIBCModule) OnChanOpenConfirm( + ctx sdk.Context, + portID, + channelID string, +) error { + m.t.Fatalf("unexpected call to OnChanOpenConfirm") + return nil +} + +func (m *MockIBCModule) OnChanCloseInit( + ctx sdk.Context, + portID, + channelID string, +) error { + m.t.Fatalf("unexpected call to OnChanCloseInit") + return nil +} + +func (m *MockIBCModule) OnChanCloseConfirm( + ctx sdk.Context, + portID, + channelID string, +) error { + m.t.Fatalf("unexpected call to OnChanCloseConfirm") + return nil +} + +func (m *MockIBCModule) OnRecvPacket( + ctx sdk.Context, + packet channeltypes.Packet, + relayer sdk.AccAddress, +) exported.Acknowledgement { + m.called = true + return channeltypes.NewResultAcknowledgement([]byte{byte(1)}) +} + +func (m *MockIBCModule) OnAcknowledgementPacket( + ctx sdk.Context, + packet channeltypes.Packet, + acknowledgement []byte, + relayer sdk.AccAddress, +) error { + m.t.Fatalf("unexpected call to OnAcknowledgementPacket") + return nil +} + +func (m *MockIBCModule) OnTimeoutPacket( + ctx sdk.Context, + packet channeltypes.Packet, + relayer sdk.AccAddress, +) error { + m.t.Fatalf("unexpected call to OnTimeoutPacket") + return nil +} diff --git a/x/tokenfilter/keeper.go b/x/tokenfilter/keeper.go new file mode 100644 index 0000000000..c587cc4a95 --- /dev/null +++ b/x/tokenfilter/keeper.go @@ -0,0 +1,18 @@ +package tokenfilter + +import ( + porttypes "github.com/cosmos/ibc-go/v6/modules/core/05-port/types" +) + +// Keeper is so far a noop as the tokenfilter doesn't have any need to +// act as middleware for outgoing messages (only inbound ones). +type Keeper struct { + porttypes.ICS4Wrapper +} + +// NewKeeper creates a new tokenfilter Keeper instance. +func NewKeeper(wrapper porttypes.ICS4Wrapper) Keeper { + return Keeper{ + ICS4Wrapper: wrapper, + } +}