diff --git a/go.mod b/go.mod index 2a2e1e3f..a7c0c42d 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( cosmossdk.io/x/evidence v0.1.1 cosmossdk.io/x/feegrant v0.1.1 cosmossdk.io/x/upgrade v0.1.4 - dollar.noble.xyz v1.0.0-alpha.3 + dollar.noble.xyz v1.0.0-alpha.4 github.com/circlefin/noble-cctp v0.0.0-20241031192117-4285c94ec194 github.com/circlefin/noble-fiattokenfactory v0.0.0-20250123235012-5f9bd9dd2c5b github.com/cometbft/cometbft v0.38.17 @@ -35,6 +35,7 @@ require ( github.com/spf13/cast v1.7.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.10.0 github.com/wormhole-foundation/wormhole/sdk v0.0.0-20241218143724-3797ed082150 jester.noble.xyz/api v0.1.0 mvdan.cc/gofumpt v0.7.0 @@ -297,7 +298,6 @@ require ( github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tdakkota/asciicheck v0.2.0 // indirect diff --git a/go.sum b/go.sum index 342dc5b6..28eac258 100644 --- a/go.sum +++ b/go.sum @@ -227,8 +227,8 @@ cosmossdk.io/x/tx v0.13.8/go.mod h1:V6DImnwJMTq5qFjeGWpXNiT/fjgE4HtmclRmTqRVM3w= cosmossdk.io/x/upgrade v0.1.4 h1:/BWJim24QHoXde8Bc64/2BSEB6W4eTydq0X/2f8+g38= cosmossdk.io/x/upgrade v0.1.4/go.mod h1:9v0Aj+fs97O+Ztw+tG3/tp5JSlrmT7IcFhAebQHmOPo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -dollar.noble.xyz v1.0.0-alpha.3 h1:lDCuwPKsMrdf6IXB6mBLzdnJekN3q1JmbSUg3jI2GwE= -dollar.noble.xyz v1.0.0-alpha.3/go.mod h1:xUIvQy23cN7ci4G/WkQ0Yu+tR6NxF/Khl0xuiyYcG4M= +dollar.noble.xyz v1.0.0-alpha.4 h1:DV5mRLjsQBikWKjXr027wHPNJM5r9TQ9TASgMhbVzbU= +dollar.noble.xyz v1.0.0-alpha.4/go.mod h1:75aH3kpPLDoQosHSvZYgm2DULvmDH5DAgcnt1FPkyk8= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/4meepo/tagalign v1.3.4 h1:P51VcvBnf04YkHzjfclN6BbsopfJR5rxs1n+5zHt+w8= diff --git a/ics4_wrapper.go b/ics4_wrapper.go new file mode 100644 index 00000000..8963acb5 --- /dev/null +++ b/ics4_wrapper.go @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2025 NASD Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package noble + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + porttypes "github.com/cosmos/ibc-go/v8/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported" +) + +var _ porttypes.ICS4Wrapper = &NobleICS4Wrapper{} + +// NobleICS4Wrapper implements the ICS4Wrapper interface. It implements custom logic in SendPacket in order +// to check all outgoing IBC transfers so that $USDN cannot be sent to another chain. +type NobleICS4Wrapper struct { + ics4Wrapper porttypes.ICS4Wrapper + dollarKeeper ExpectedDollarKeeper +} + +// ExpectedDollarKeeper defines the interface expected by NobleICS4Wrapper for the Noble Dollar module. +type ExpectedDollarKeeper interface { + GetDenom() string +} + +// NewNobleICS4Wrapper returns a new instance of NobleICS4Wrapper. +func NewNobleICS4Wrapper(app porttypes.ICS4Wrapper, dollarKeeper ExpectedDollarKeeper) porttypes.ICS4Wrapper { + return NobleICS4Wrapper{ + ics4Wrapper: app, + dollarKeeper: dollarKeeper, + } +} + +// SendPacket attempts to unmarshal the provided packet data as the ICS-20 +// FungibleTokenPacketData type. If the packet is a valid ICS-20 transfer, then +// a check is performed on the denom to ensure that $USDN cannot be transferred +// out of Noble via IBC. +func (w NobleICS4Wrapper) SendPacket(ctx sdk.Context, chanCap *capabilitytypes.Capability, sourcePort string, sourceChannel string, timeoutHeight clienttypes.Height, timeoutTimestamp uint64, data []byte) (sequence uint64, err error) { + var packetData transfertypes.FungibleTokenPacketData + if err := transfertypes.ModuleCdc.UnmarshalJSON(data, &packetData); err != nil { + return w.ics4Wrapper.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) + } + + denom := w.dollarKeeper.GetDenom() + if packetData.Denom == denom { + return 0, fmt.Errorf("ibc transfers of %s are currently disabled", denom) + } + + return w.ics4Wrapper.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) +} + +// WriteAcknowledgement implements the ICS4Wrapper interface. +func (w NobleICS4Wrapper) WriteAcknowledgement(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) error { + return w.ics4Wrapper.WriteAcknowledgement(ctx, chanCap, packet, ack) +} + +// GetAppVersion implements the ICS4Wrapper interface. +func (w NobleICS4Wrapper) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { + return w.ics4Wrapper.GetAppVersion(ctx, portID, channelID) +} diff --git a/ics4_wrapper_test.go b/ics4_wrapper_test.go new file mode 100644 index 00000000..ab8a1480 --- /dev/null +++ b/ics4_wrapper_test.go @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2025 NASD Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package noble + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + porttypes "github.com/cosmos/ibc-go/v8/modules/core/05-port/types" + "github.com/cosmos/ibc-go/v8/modules/core/exported" + "github.com/stretchr/testify/require" +) + +var _ porttypes.ICS4Wrapper = (*MockICS4Wrapper)(nil) + +type MockICS4Wrapper struct { + t *testing.T +} + +func (m MockICS4Wrapper) SendPacket( + ctx sdk.Context, + chanCap *capabilitytypes.Capability, + sourcePort string, + sourceChannel string, + timeoutHeight clienttypes.Height, + timeoutTimestamp uint64, + data []byte, +) (sequence uint64, err error) { + return 0, nil +} + +func (m MockICS4Wrapper) WriteAcknowledgement( + ctx sdk.Context, + chanCap *capabilitytypes.Capability, + packet exported.PacketI, + ack exported.Acknowledgement, +) error { + m.t.Fatal("WriteAcknowledgement should not have been called") + return nil +} + +func (m MockICS4Wrapper) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { + m.t.Fatal("GetAppVersion should not have been called") + return "", false +} + +type MockDollarKeeper struct { + denom string +} + +func (m MockDollarKeeper) GetDenom() string { + return m.denom +} + +// TestSendPacket asserts that outgoing IBC transfers work as expected in cases +// where the denom is $USDN, as well as cases where the denom is not. +func TestSendPacket(t *testing.T) { + denom := "uusdn" + + tc := []struct { + name string + data transfertypes.FungibleTokenPacketData + fail bool + }{ + { + "Outgoing IBC transfer of USDN - should be blocked", + transfertypes.NewFungibleTokenPacketData(denom, "1000000", "test", "test", "test"), + true, + }, + { + "Outgoing IBC transfer of USDC - should not be blocked", + transfertypes.NewFungibleTokenPacketData("uusdc", "1000000", "test", "test", "test"), + false, + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + wrapper := MockICS4Wrapper{t} + keeper := MockDollarKeeper{denom: denom} + nobleWrapper := NewNobleICS4Wrapper(wrapper, keeper) + + data, err := transfertypes.ModuleCdc.MarshalJSON(&tt.data) + require.NoError(t, err) + + ctx := sdk.Context{} + timeout := uint64(0) + + _, err = nobleWrapper.SendPacket(ctx, nil, "transfer", "channel-0", clienttypes.Height{}, timeout, data) + + if tt.fail { + require.Error(t, err) + require.ErrorContains(t, err, fmt.Sprintf("ibc transfers of %s are currently disabled", denom)) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/legacy.go b/legacy.go index 76f7eeb5..04968107 100644 --- a/legacy.go +++ b/legacy.go @@ -18,7 +18,6 @@ package noble import ( storetypes "cosmossdk.io/store/types" - dollar "dollar.noble.xyz" "github.com/circlefin/noble-fiattokenfactory/x/blockibc" pfm "github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v8/packetforward" pfmkeeper "github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v8/packetforward/keeper" @@ -96,12 +95,15 @@ func (app *App) RegisterLegacyModules() error { ) app.ICAHostKeeper.WithQueryRouter(app.GRPCQueryRouter()) + // Create custom ICS4Wrapper so that we can block outgoing $USDN IBC transfers. + ics4Wrapper := NewNobleICS4Wrapper(app.IBCKeeper.ChannelKeeper, app.DollarKeeper) + scopedTransferKeeper := app.CapabilityKeeper.ScopeToModule(transfertypes.ModuleName) app.TransferKeeper = transferkeeper.NewKeeper( app.appCodec, app.GetKey(transfertypes.StoreKey), app.GetSubspace(transfertypes.ModuleName), - app.IBCKeeper.ChannelKeeper, + ics4Wrapper, app.IBCKeeper.ChannelKeeper, app.IBCKeeper.PortKeeper, app.AccountKeeper, @@ -122,7 +124,6 @@ func (app *App) RegisterLegacyModules() error { var transferStack porttypes.IBCModule transferStack = transfer.NewIBCModule(app.TransferKeeper) - transferStack = dollar.NewIBCMiddleware(transferStack, app.IBCKeeper.ChannelKeeper, app.DollarKeeper) transferStack = forwarding.NewMiddleware(transferStack, app.AccountKeeper, app.ForwardingKeeper) transferStack = pfm.NewIBCMiddleware( transferStack,