Skip to content

Commit

Permalink
feat! implement token filter IBC middleware (#1219)
Browse files Browse the repository at this point in the history
Closes: #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.
  • Loading branch information
cmwaters committed Jan 17, 2023
1 parent 222bb1e commit 6a57d4d
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 3 deletions.
14 changes: 11 additions & 3 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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(
Expand All @@ -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 ****/
Expand Down
77 changes: 77 additions & 0 deletions x/tokenfilter/ibc_middleware.go
Original file line number Diff line number Diff line change
@@ -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)
}
170 changes: 170 additions & 0 deletions x/tokenfilter/ibc_middleware_test.go
Original file line number Diff line number Diff line change
@@ -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
}
18 changes: 18 additions & 0 deletions x/tokenfilter/keeper.go
Original file line number Diff line number Diff line change
@@ -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,
}
}

0 comments on commit 6a57d4d

Please sign in to comment.