diff --git a/.gitignore b/.gitignore index 77339a3b..25638121 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,10 @@ # Dependency directories (remove the comment below to include it) # vendor/ +<<<<<<< HEAD +======= + +target/ + +go.work.sum +>>>>>>> d72e83c ([pfm][fix] patch arbitrary account passthrough (#71)) diff --git a/middleware/packet-forward-middleware/README.md b/middleware/packet-forward-middleware/README.md index 8da77207..ba202b01 100644 --- a/middleware/packet-forward-middleware/README.md +++ b/middleware/packet-forward-middleware/README.md @@ -61,8 +61,10 @@ Utilizing the packet `memo` field, instructions can be encoded as JSON for multi ### Minimal Example - Chain forward A->B->C - The packet-forward-middleware integrated on Chain B. +- The packet data `receiver` for the `MsgTransfer` on Chain A is set to `"pfm"` or some other invalid bech32 string.* - The packet `memo` is included in `MsgTransfer` by user on Chain A. +memo: ``` { "forward": { @@ -76,6 +78,8 @@ Utilizing the packet `memo` field, instructions can be encoded as JSON for multi ### Full Example - Chain forward A->B->C->D with retry on timeout - The packet-forward-middleware integrated on Chain B and Chain C. +- The packet data `receiver` for the `MsgTransfer` on Chain A is set to `"pfm"` or some other invalid bech32 string.* +- The forward metadata `receiver` for the hop from Chain B to Chain C is set to `"pfm"` or some other invalid bech32 string.* - The packet `memo` is included in `MsgTransfer` by user on Chain A. - A packet timeout of 10 minutes and 2 retries is set for both forwards. @@ -87,7 +91,7 @@ In the case of a timeout after 10 minutes for either forward, the packet would b ``` { "forward": { - "receiver": "chain-c-bech32-address", + "receiver": "pfm", // purposely using invalid bech32 here* "port": "transfer", "channel": "channel-123", "timeout": "10m", @@ -109,7 +113,7 @@ In the case of a timeout after 10 minutes for either forward, the packet would b ``` { "forward": { - "receiver": "chain-c-bech32-address", + "receiver": "pfm", // purposely using invalid bech32 here* "port": "transfer", "channel": "channel-123", "timeout": "10m", @@ -119,6 +123,14 @@ In the case of a timeout after 10 minutes for either forward, the packet would b } ``` +## Intermediate Receivers* + +PFM does not need the packet data `receiver` address to be valid, as it will create a hash of the sender and channel to derive a receiver address on the intermediate chains. This is done for security purposes to ensure that users cannot move funds through arbitrary accounts on intermediate chains. + +To prevent accidentally sending funds to a chain which does not have PFM, it is recommended to use an invalid bech32 string (such as `"pfm"`) for the `receiver` on intermediate chains. By using an invalid bech32 string, a transfer that is accidentally sent to a chain that does not have PFM would fail to be received, and properly refunded to the user on the source chain, rather than having funds get stuck on the intermediate chain. + +The examples above show the intended usage of the `receiver` field for one or multiple intermediate PFM chains. + ## References - https://www.mintscan.io/cosmos/proposals/56 diff --git a/middleware/packet-forward-middleware/router/ibc_middleware.go b/middleware/packet-forward-middleware/router/ibc_middleware.go index 9b92a3df..b4eca0d7 100644 --- a/middleware/packet-forward-middleware/router/ibc_middleware.go +++ b/middleware/packet-forward-middleware/router/ibc_middleware.go @@ -13,6 +13,7 @@ import ( errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" @@ -134,6 +135,28 @@ func getBoolFromAny(value any) bool { return boolVal } +// getReceiver returns the receiver address for a given channel and original sender. +// it overrides the receiver address to be a hash of the channel/origSender so that +// the receiver address is deterministic and can be used to identify the sender on the +// initial chain. +func getReceiver(channel string, originalSender string) (string, error) { + senderStr := fmt.Sprintf("%s/%s", channel, originalSender) + senderHash32 := address.Hash(types.ModuleName, []byte(senderStr)) + sender := sdk.AccAddress(senderHash32[:20]) + bech32Prefix := sdk.GetConfig().GetBech32AccountAddrPrefix() + return sdk.Bech32ifyAddressBytes(bech32Prefix, sender) +} + +// newErrorAcknowledgement returns an error that identifies PFM and provides the error. +// It's okay if these errors are non-deterministic, because they will not be committed to state, only emitted as events. +func newErrorAcknowledgement(err error) channeltypes.Acknowledgement { + return channeltypes.Acknowledgement{ + Response: &channeltypes.Acknowledgement_Error{ + Error: fmt.Sprintf("packet-forward-middleware error: %s", err.Error()), + }, + } +} + // OnRecvPacket checks the memo field on this packet and if the metadata inside's root key indicates this packet // should be handled by the swap middleware it attempts to perform a swap. If the swap is successful // the underlying application's OnRecvPacket callback is invoked, an ack error is returned otherwise. @@ -142,12 +165,15 @@ func (im IBCMiddleware) OnRecvPacket( packet channeltypes.Packet, relayer sdk.AccAddress, ) ibcexported.Acknowledgement { + logger := im.keeper.Logger(ctx) + var data transfertypes.FungibleTokenPacketData if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil { - return channeltypes.NewErrorAcknowledgement(err) + logger.Error("packetForwardMiddleware OnRecvPacketfailed to unmarshal packet data as FungibleTokenPacketData", "error", err) + return newErrorAcknowledgement(fmt.Errorf("failed to unmarshal packet data as FungibleTokenPacketData: %w", err)) } - im.keeper.Logger(ctx).Debug("packetForwardMiddleware OnRecvPacket", + logger.Debug("packetForwardMiddleware OnRecvPacket", "sequence", packet.Sequence, "src-channel", packet.SourceChannel, "src-port", packet.SourcePort, "dst-channel", packet.DestinationChannel, "dst-port", packet.DestinationPort, @@ -158,13 +184,14 @@ func (im IBCMiddleware) OnRecvPacket( err := json.Unmarshal([]byte(data.Memo), &d) if err != nil || d["forward"] == nil { // not a packet that should be forwarded - im.keeper.Logger(ctx).Debug("packetForwardMiddleware OnRecvPacket forward metadata does not exist") + logger.Debug("packetForwardMiddleware OnRecvPacket forward metadata does not exist") return im.app.OnRecvPacket(ctx, packet, relayer) } m := &types.PacketMetadata{} err = json.Unmarshal([]byte(data.Memo), m) if err != nil { - return channeltypes.NewErrorAcknowledgement(fmt.Errorf("packetForwardMiddleware error parsing forward metadata, %s", err)) + logger.Error("packetForwardMiddleware OnRecvPacket error parsing forward metadata", "error", err) + return newErrorAcknowledgement(fmt.Errorf("error parsing forward metadata: %w", err)) } metadata := m.Forward @@ -175,16 +202,24 @@ func (im IBCMiddleware) OnRecvPacket( disableDenomComposition := getBoolFromAny(goCtx.Value(types.DisableDenomCompositionKey{})) if err := metadata.Validate(); err != nil { - return channeltypes.NewErrorAcknowledgement(err) + logger.Error("packetForwardMiddleware OnRecvPacket forward metadata is invalid", "error", err) + return newErrorAcknowledgement(err) + } + + // override the receiver so that senders cannot move funds through arbitrary addresses. + overrideReceiver, err := getReceiver(packet.DestinationChannel, data.Sender) + if err != nil { + logger.Error("packetForwardMiddleware OnRecvPacket failed to construct override receiver", "error", err) + return newErrorAcknowledgement(fmt.Errorf("failed to construct override receiver: %w", err)) } // if this packet has been handled by another middleware in the stack there may be no need to call into the // underlying app, otherwise the transfer module's OnRecvPacket callback could be invoked more than once // which would mint/burn vouchers more than once if !processed { - ack := im.app.OnRecvPacket(ctx, packet, relayer) - if ack == nil || !ack.Success() { - return ack + if err := im.receiveFunds(ctx, packet, data, overrideReceiver, relayer); err != nil { + logger.Error("packetForwardMiddleware OnRecvPacket error receiving packet", "error", err) + return newErrorAcknowledgement(fmt.Errorf("error receiving packet: %w", err)) } } @@ -201,7 +236,8 @@ func (im IBCMiddleware) OnRecvPacket( amountInt, ok := sdk.NewIntFromString(data.Amount) if !ok { - return channeltypes.NewErrorAcknowledgement(fmt.Errorf("error parsing amount for forward: %s", data.Amount)) + logger.Error("packetForwardMiddleware OnRecvPacket error parsing amount for forward", "amount", data.Amount) + return newErrorAcknowledgement(fmt.Errorf("error parsing amount for forward: %s", data.Amount)) } token := sdk.NewCoin(denomOnThisChain, amountInt) @@ -219,9 +255,10 @@ func (im IBCMiddleware) OnRecvPacket( retries = im.retriesOnTimeout } - err = im.keeper.ForwardTransferPacket(ctx, nil, packet, data.Sender, data.Receiver, metadata, token, retries, timeout, []metrics.Label{}, nonrefundable) + err = im.keeper.ForwardTransferPacket(ctx, nil, packet, data.Sender, overrideReceiver, metadata, token, retries, timeout, []metrics.Label{}, nonrefundable) if err != nil { - return channeltypes.NewErrorAcknowledgement(err) + logger.Error("packetForwardMiddleware OnRecvPacket error forwarding packet", "error", err) + return newErrorAcknowledgement(err) } // returning nil ack will prevent WriteAcknowledgement from occurring for forwarded packet. @@ -229,6 +266,47 @@ func (im IBCMiddleware) OnRecvPacket( return nil } +// receiveFunds receives funds from the packet into the override receiver +// address and returns an error if the funds cannot be received. +func (im IBCMiddleware) receiveFunds( + ctx sdk.Context, + packet channeltypes.Packet, + data transfertypes.FungibleTokenPacketData, + overrideReceiver string, + relayer sdk.AccAddress, +) error { + overrideData := transfertypes.FungibleTokenPacketData{ + Denom: data.Denom, + Amount: data.Amount, + Sender: data.Sender, + Receiver: overrideReceiver, // override receiver + // Memo explicitly zeroed + } + overrideDataBz := transfertypes.ModuleCdc.MustMarshalJSON(&overrideData) + overridePacket := channeltypes.Packet{ + Sequence: packet.Sequence, + SourcePort: packet.SourcePort, + SourceChannel: packet.SourceChannel, + DestinationPort: packet.DestinationPort, + DestinationChannel: packet.DestinationChannel, + Data: overrideDataBz, // override data + TimeoutHeight: packet.TimeoutHeight, + TimeoutTimestamp: packet.TimeoutTimestamp, + } + + ack := im.app.OnRecvPacket(ctx, overridePacket, relayer) + + if ack == nil { + return fmt.Errorf("ack is nil") + } + + if !ack.Success() { + return fmt.Errorf("ack error: %s", string(ack.Acknowledgement())) + } + + return nil +} + // OnAcknowledgementPacket implements the IBCModule interface. func (im IBCMiddleware) OnAcknowledgementPacket( ctx sdk.Context, @@ -294,7 +372,7 @@ func (im IBCMiddleware) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Pac im.keeper.RemoveInFlightPacket(ctx, packet) // this is a forwarded packet, so override handling to avoid refund from being processed on this chain. // WriteAcknowledgement with proxied ack to return success/fail to previous chain. - return im.keeper.WriteAcknowledgementForForwardedPacket(ctx, packet, data, inFlightPacket, channeltypes.NewErrorAcknowledgement(err)) + return im.keeper.WriteAcknowledgementForForwardedPacket(ctx, packet, data, inFlightPacket, newErrorAcknowledgement(err)) } // timeout should be retried. In order to do that, we need to handle this timeout to refund on this chain first. if err := im.app.OnTimeoutPacket(ctx, packet, relayer); err != nil { diff --git a/middleware/packet-forward-middleware/router/module_test.go b/middleware/packet-forward-middleware/router/module_test.go index 68085818..f449b113 100644 --- a/middleware/packet-forward-middleware/router/module_test.go +++ b/middleware/packet-forward-middleware/router/module_test.go @@ -27,6 +27,16 @@ var ( testSourceChannel = "channel-10" testDestinationPort = "transfer" testDestinationChannel = "channel-11" + + senderAddr = "cosmos1wnlew8ss0sqclfalvj6jkcyvnwq79fd74qxxue" + hostAddr = "cosmos1vzxkv3lxccnttr9rs0002s93sgw72h7ghukuhs" + intermediateAddr = "cosmos1v954djef63x2lqj8yy7r3r487heg0exdmkj0sr" + hostAddr2 = "cosmos1q4p4gx889lfek5augdurrjclwtqvjhuntm6j4m" + intermediateAddr2 = "cosmos1eadmq78mkhg6lrk87lxgateketvz44crq45jpe" + destAddr = "cosmos16plylpsgxechajltx9yeseqexzdzut9g8vla4k" + port = "transfer" + channel = "channel-0" + channel2 = "channel-1" ) func makeIBCDenom(port, channel, denom string) string { @@ -38,12 +48,13 @@ func emptyPacket() channeltypes.Packet { return channeltypes.Packet{} } -func transferPacket(t *testing.T, receiver string, metadata any) channeltypes.Packet { +func transferPacket(t *testing.T, sender string, receiver string, metadata any) channeltypes.Packet { t.Helper() transferPacket := transfertypes.FungibleTokenPacketData{ Denom: testDenom, Amount: testAmount, + Sender: sender, Receiver: receiver, } @@ -69,11 +80,12 @@ func transferPacket(t *testing.T, receiver string, metadata any) channeltypes.Pa } } -func transferPacket256(t *testing.T, receiver string, metadata any) channeltypes.Packet { +func transferPacket256(t *testing.T, sender string, receiver string, metadata any) channeltypes.Packet { t.Helper() transferPacket := transfertypes.FungibleTokenPacketData{ Denom: testDenom, Amount: testAmount256, + Sender: sender, Receiver: receiver, } @@ -117,7 +129,7 @@ func TestOnRecvPacket_EmptyPacket(t *testing.T) { expectedAck := &channeltypes.Acknowledgement{} err := cdc.UnmarshalJSON(ack.Acknowledgement(), expectedAck) require.NoError(t, err) - require.Equal(t, "ABCI code: 1: error handling packet: see events for details", expectedAck.GetError()) + require.Equal(t, "packet-forward-middleware error: failed to unmarshal packet data as FungibleTokenPacketData: EOF", expectedAck.GetError()) } func TestOnRecvPacket_InvalidReceiver(t *testing.T) { @@ -129,8 +141,13 @@ func TestOnRecvPacket_InvalidReceiver(t *testing.T) { forwardMiddleware := setup.ForwardMiddleware // Test data +<<<<<<< HEAD senderAccAddr := test.AccAddress(t) packet := transferPacket(t, "", nil) +======= + senderAccAddr := test.AccAddress() + packet := transferPacket(t, test.AccAddress().String(), "", nil) +>>>>>>> d72e83c ([pfm][fix] patch arbitrary account passthrough (#71)) // Expected mocks gomock.InOrder( @@ -155,8 +172,13 @@ func TestOnRecvPacket_NoForward(t *testing.T) { forwardMiddleware := setup.ForwardMiddleware // Test data +<<<<<<< HEAD senderAccAddr := test.AccAddress(t) packet := transferPacket(t, "cosmos16plylpsgxechajltx9yeseqexzdzut9g8vla4k", nil) +======= + senderAccAddr := test.AccAddress() + packet := transferPacket(t, test.AccAddress().String(), "cosmos16plylpsgxechajltx9yeseqexzdzut9g8vla4k", nil) +>>>>>>> d72e83c ([pfm][fix] patch arbitrary account passthrough (#71)) // Expected mocks gomock.InOrder( @@ -182,8 +204,13 @@ func TestOnRecvPacket_NoMemo(t *testing.T) { forwardMiddleware := setup.ForwardMiddleware // Test data +<<<<<<< HEAD senderAccAddr := test.AccAddress(t) packet := transferPacket(t, "cosmos16plylpsgxechajltx9yeseqexzdzut9g8vla4k", "{}") +======= + senderAccAddr := test.AccAddress() + packet := transferPacket(t, test.AccAddress().String(), "cosmos16plylpsgxechajltx9yeseqexzdzut9g8vla4k", "{}") +>>>>>>> d72e83c ([pfm][fix] patch arbitrary account passthrough (#71)) // Expected mocks gomock.InOrder( @@ -208,8 +235,13 @@ func TestOnRecvPacket_RecvPacketFailed(t *testing.T) { cdc := setup.Initializer.Marshaler forwardMiddleware := setup.ForwardMiddleware +<<<<<<< HEAD senderAccAddr := test.AccAddress(t) packet := transferPacket(t, "cosmos16plylpsgxechajltx9yeseqexzdzut9g8vla4k", nil) +======= + senderAccAddr := test.AccAddress() + packet := transferPacket(t, test.AccAddress().String(), "cosmos16plylpsgxechajltx9yeseqexzdzut9g8vla4k", nil) +>>>>>>> d72e83c ([pfm][fix] patch arbitrary account passthrough (#71)) // Expected mocks gomock.InOrder( @@ -236,31 +268,24 @@ func TestOnRecvPacket_ForwardNoFee(t *testing.T) { cdc := setup.Initializer.Marshaler forwardMiddleware := setup.ForwardMiddleware - // Test data - const ( - hostAddr = "cosmos1vzxkv3lxccnttr9rs0002s93sgw72h7ghukuhs" - destAddr = "cosmos16plylpsgxechajltx9yeseqexzdzut9g8vla4k" - port = "transfer" - channel = "channel-0" - ) denom := makeIBCDenom(testDestinationPort, testDestinationChannel, testDenom) senderAccAddr := test.AccAddress(t) testCoin := sdk.NewCoin(denom, sdk.NewInt(100)) - packetOrig := transferPacket(t, hostAddr, &types.PacketMetadata{ - Forward: &types.ForwardMetadata{ - Receiver: destAddr, - Port: port, - Channel: channel, - }, - }) - packetFwd := transferPacket(t, destAddr, nil) + metadata := &types.PacketMetadata{Forward: &types.ForwardMetadata{ + Receiver: destAddr, + Port: port, + Channel: channel, + }} + packetOrig := transferPacket(t, senderAddr, hostAddr, metadata) + packetModifiedSender := transferPacket(t, senderAddr, intermediateAddr, metadata) + packetFwd := transferPacket(t, intermediateAddr, destAddr, nil) acknowledgement := channeltypes.NewResultAcknowledgement([]byte("test")) successAck := cdc.MustMarshalJSON(&acknowledgement) // Expected mocks gomock.InOrder( - setup.Mocks.IBCModuleMock.EXPECT().OnRecvPacket(ctx, packetOrig, senderAccAddr). + setup.Mocks.IBCModuleMock.EXPECT().OnRecvPacket(ctx, packetModifiedSender, senderAccAddr). Return(acknowledgement), setup.Mocks.TransferKeeperMock.EXPECT().Transfer( @@ -269,7 +294,7 @@ func TestOnRecvPacket_ForwardNoFee(t *testing.T) { port, channel, testCoin, - hostAddr, + intermediateAddr, destAddr, keeper.DefaultTransferPacketTimeoutHeight, uint64(ctx.BlockTime().UnixNano())+uint64(keeper.DefaultForwardTransferPacketTimeoutTimestamp.Nanoseconds()), @@ -298,13 +323,6 @@ func TestOnRecvPacket_ForwardAmountInt256(t *testing.T) { cdc := setup.Initializer.Marshaler forwardMiddleware := setup.ForwardMiddleware - // Test data - const ( - hostAddr = "cosmos1vzxkv3lxccnttr9rs0002s93sgw72h7ghukuhs" - destAddr = "cosmos16plylpsgxechajltx9yeseqexzdzut9g8vla4k" - port = "transfer" - channel = "channel-0" - ) denom := makeIBCDenom(testDestinationPort, testDestinationChannel, testDenom) senderAccAddr := test.AccAddress(t) @@ -312,21 +330,22 @@ func TestOnRecvPacket_ForwardAmountInt256(t *testing.T) { require.True(t, ok) testCoin := sdk.NewCoin(denom, amount256) - packetOrig := transferPacket256(t, hostAddr, &types.PacketMetadata{ - Forward: &types.ForwardMetadata{ - Receiver: destAddr, - Port: port, - Channel: channel, - }, - }) - packetFwd := transferPacket256(t, destAddr, nil) + metadata := &types.PacketMetadata{Forward: &types.ForwardMetadata{ + Receiver: destAddr, + Port: port, + Channel: channel, + }} + + packetOrig := transferPacket256(t, senderAddr, hostAddr, metadata) + packetModifiedSender := transferPacket256(t, senderAddr, intermediateAddr, metadata) + packetFwd := transferPacket256(t, intermediateAddr, destAddr, nil) acknowledgement := channeltypes.NewResultAcknowledgement([]byte("test")) successAck := cdc.MustMarshalJSON(&acknowledgement) // Expected mocks gomock.InOrder( - setup.Mocks.IBCModuleMock.EXPECT().OnRecvPacket(ctx, packetOrig, senderAccAddr). + setup.Mocks.IBCModuleMock.EXPECT().OnRecvPacket(ctx, packetModifiedSender, senderAccAddr). Return(acknowledgement), setup.Mocks.TransferKeeperMock.EXPECT().Transfer( @@ -335,7 +354,7 @@ func TestOnRecvPacket_ForwardAmountInt256(t *testing.T) { port, channel, testCoin, - hostAddr, + intermediateAddr, destAddr, keeper.DefaultTransferPacketTimeoutHeight, uint64(ctx.BlockTime().UnixNano())+uint64(keeper.DefaultForwardTransferPacketTimeoutTimestamp.Nanoseconds()), @@ -367,38 +386,36 @@ func TestOnRecvPacket_ForwardWithFee(t *testing.T) { // Set fee param to 10% setup.Keepers.RouterKeeper.SetParams(ctx, types.NewParams(sdk.NewDecWithPrec(10, 2))) - // Test data - const ( - hostAddr = "cosmos1vzxkv3lxccnttr9rs0002s93sgw72h7ghukuhs" - destAddr = "cosmos16plylpsgxechajltx9yeseqexzdzut9g8vla4k" - port = "transfer" - channel = "channel-0" - ) denom := makeIBCDenom(testDestinationPort, testDestinationChannel, testDenom) +<<<<<<< HEAD senderAccAddr := test.AccAddress(t) hostAccAddr := test.AccAddressFromBech32(t, hostAddr) +======= + senderAccAddr := test.AccAddress() + intermediateAccAddr := test.AccAddressFromBech32(t, intermediateAddr) +>>>>>>> d72e83c ([pfm][fix] patch arbitrary account passthrough (#71)) testCoin := sdk.NewCoin(denom, sdk.NewInt(90)) feeCoins := sdk.Coins{sdk.NewCoin(denom, sdk.NewInt(10))} - packetOrig := transferPacket(t, hostAddr, &types.PacketMetadata{ - Forward: &types.ForwardMetadata{ - Receiver: destAddr, - Port: port, - Channel: channel, - }, - }) - packetFwd := transferPacket(t, destAddr, nil) + metadata := &types.PacketMetadata{Forward: &types.ForwardMetadata{ + Receiver: destAddr, + Port: port, + Channel: channel, + }} + packetOrig := transferPacket(t, senderAddr, hostAddr, metadata) + packetModifiedSender := transferPacket(t, senderAddr, intermediateAddr, metadata) + packetFwd := transferPacket(t, intermediateAddr, destAddr, nil) acknowledgement := channeltypes.NewResultAcknowledgement([]byte("test")) successAck := cdc.MustMarshalJSON(&acknowledgement) // Expected mocks gomock.InOrder( - setup.Mocks.IBCModuleMock.EXPECT().OnRecvPacket(ctx, packetOrig, senderAccAddr). + setup.Mocks.IBCModuleMock.EXPECT().OnRecvPacket(ctx, packetModifiedSender, senderAccAddr). Return(acknowledgement), setup.Mocks.DistributionKeeperMock.EXPECT().FundCommunityPool( ctx, feeCoins, - hostAccAddr, + intermediateAccAddr, ).Return(nil), setup.Mocks.TransferKeeperMock.EXPECT().Transfer( @@ -407,7 +424,7 @@ func TestOnRecvPacket_ForwardWithFee(t *testing.T) { port, channel, testCoin, - hostAddr, + intermediateAddr, destAddr, keeper.DefaultTransferPacketTimeoutHeight, uint64(ctx.BlockTime().UnixNano())+uint64(keeper.DefaultForwardTransferPacketTimeoutTimestamp.Nanoseconds()), @@ -436,16 +453,6 @@ func TestOnRecvPacket_ForwardMultihopStringNext(t *testing.T) { cdc := setup.Initializer.Marshaler forwardMiddleware := setup.ForwardMiddleware - // Test data - const ( - hostAddr = "cosmos1vzxkv3lxccnttr9rs0002s93sgw72h7ghukuhs" - hostAddr2 = "cosmos1q4p4gx889lfek5augdurrjclwtqvjhuntm6j4m" - destAddr = "cosmos16plylpsgxechajltx9yeseqexzdzut9g8vla4k" - port = "transfer" - channel = "channel-0" - channel2 = "channel-1" - ) - denom := makeIBCDenom(testDestinationPort, testDestinationChannel, testDenom) senderAccAddr := test.AccAddress(t) senderAccAddr2 := test.AccAddress(t) @@ -460,16 +467,20 @@ func TestOnRecvPacket_ForwardMultihopStringNext(t *testing.T) { nextBz, err := json.Marshal(nextMetadata) require.NoError(t, err) - packetOrig := transferPacket(t, hostAddr, &types.PacketMetadata{ + metadata := &types.PacketMetadata{ Forward: &types.ForwardMetadata{ Receiver: hostAddr2, Port: port, Channel: channel, Next: types.NewJSONObject(false, nextBz, orderedmap.OrderedMap{}), }, - }) - packet2 := transferPacket(t, hostAddr2, nextMetadata) - packetFwd := transferPacket(t, destAddr, nil) + } + + packetOrig := transferPacket(t, senderAddr, hostAddr, metadata) + packetModifiedSender := transferPacket(t, senderAddr, intermediateAddr, metadata) + packet2 := transferPacket(t, intermediateAddr, hostAddr2, nextMetadata) + packet2ModifiedSender := transferPacket(t, intermediateAddr, intermediateAddr2, nextMetadata) + packetFwd := transferPacket(t, intermediateAddr2, destAddr, nil) memo1, err := json.Marshal(nextMetadata) require.NoError(t, err) @@ -478,7 +489,7 @@ func TestOnRecvPacket_ForwardMultihopStringNext(t *testing.T) { port, channel, testCoin, - hostAddr, + intermediateAddr, hostAddr2, keeper.DefaultTransferPacketTimeoutHeight, uint64(ctx.BlockTime().UnixNano())+uint64(keeper.DefaultForwardTransferPacketTimeoutTimestamp.Nanoseconds()), @@ -490,7 +501,7 @@ func TestOnRecvPacket_ForwardMultihopStringNext(t *testing.T) { port, channel2, testCoin, - hostAddr2, + intermediateAddr2, destAddr, keeper.DefaultTransferPacketTimeoutHeight, uint64(ctx.BlockTime().UnixNano())+uint64(keeper.DefaultForwardTransferPacketTimeoutTimestamp.Nanoseconds()), @@ -501,7 +512,7 @@ func TestOnRecvPacket_ForwardMultihopStringNext(t *testing.T) { // Expected mocks gomock.InOrder( - setup.Mocks.IBCModuleMock.EXPECT().OnRecvPacket(ctx, packetOrig, senderAccAddr). + setup.Mocks.IBCModuleMock.EXPECT().OnRecvPacket(ctx, packetModifiedSender, senderAccAddr). Return(acknowledgement), setup.Mocks.TransferKeeperMock.EXPECT().Transfer( @@ -509,7 +520,7 @@ func TestOnRecvPacket_ForwardMultihopStringNext(t *testing.T) { msgTransfer1, ).Return(&transfertypes.MsgTransferResponse{Sequence: 0}, nil), - setup.Mocks.IBCModuleMock.EXPECT().OnRecvPacket(ctx, packet2, senderAccAddr2). + setup.Mocks.IBCModuleMock.EXPECT().OnRecvPacket(ctx, packet2ModifiedSender, senderAccAddr2). Return(acknowledgement), setup.Mocks.TransferKeeperMock.EXPECT().Transfer( @@ -550,16 +561,6 @@ func TestOnRecvPacket_ForwardMultihopJSONNext(t *testing.T) { cdc := setup.Initializer.Marshaler forwardMiddleware := setup.ForwardMiddleware - // Test data - const ( - hostAddr = "cosmos1vzxkv3lxccnttr9rs0002s93sgw72h7ghukuhs" - hostAddr2 = "cosmos1q4p4gx889lfek5augdurrjclwtqvjhuntm6j4m" - destAddr = "cosmos16plylpsgxechajltx9yeseqexzdzut9g8vla4k" - port = "transfer" - channel = "channel-0" - channel2 = "channel-1" - ) - denom := makeIBCDenom(testDestinationPort, testDestinationChannel, testDenom) senderAccAddr := test.AccAddress(t) senderAccAddr2 := test.AccAddress(t) @@ -578,22 +579,25 @@ func TestOnRecvPacket_ForwardMultihopJSONNext(t *testing.T) { err = json.Unmarshal(nextBz, nextJSONObject) require.NoError(t, err) - packetOrig := transferPacket(t, hostAddr, &types.PacketMetadata{ + metadata := &types.PacketMetadata{ Forward: &types.ForwardMetadata{ Receiver: hostAddr2, Port: port, Channel: channel, Next: nextJSONObject, }, - }) - packet2 := transferPacket(t, hostAddr2, nextMetadata) - packetFwd := transferPacket(t, destAddr, nil) + } + packetOrig := transferPacket(t, senderAddr, hostAddr, metadata) + packetModifiedSender := transferPacket(t, senderAddr, intermediateAddr, metadata) + packet2 := transferPacket(t, intermediateAddr, hostAddr2, nextMetadata) + packet2ModifiedSender := transferPacket(t, intermediateAddr, intermediateAddr2, nextMetadata) + packetFwd := transferPacket(t, intermediateAddr2, destAddr, nil) msgTransfer1 := transfertypes.NewMsgTransfer( port, channel, testCoin, - hostAddr, + intermediateAddr, hostAddr2, keeper.DefaultTransferPacketTimeoutHeight, uint64(ctx.BlockTime().UnixNano())+uint64(keeper.DefaultForwardTransferPacketTimeoutTimestamp.Nanoseconds()), @@ -606,7 +610,7 @@ func TestOnRecvPacket_ForwardMultihopJSONNext(t *testing.T) { port, channel2, testCoin, - hostAddr2, + intermediateAddr2, destAddr, keeper.DefaultTransferPacketTimeoutHeight, uint64(ctx.BlockTime().UnixNano())+uint64(keeper.DefaultForwardTransferPacketTimeoutTimestamp.Nanoseconds()), @@ -618,7 +622,7 @@ func TestOnRecvPacket_ForwardMultihopJSONNext(t *testing.T) { // Expected mocks gomock.InOrder( - setup.Mocks.IBCModuleMock.EXPECT().OnRecvPacket(ctx, packetOrig, senderAccAddr). + setup.Mocks.IBCModuleMock.EXPECT().OnRecvPacket(ctx, packetModifiedSender, senderAccAddr). Return(acknowledgement), setup.Mocks.TransferKeeperMock.EXPECT().Transfer( @@ -626,7 +630,7 @@ func TestOnRecvPacket_ForwardMultihopJSONNext(t *testing.T) { msgTransfer1, ).Return(&transfertypes.MsgTransferResponse{Sequence: 0}, nil), - setup.Mocks.IBCModuleMock.EXPECT().OnRecvPacket(ctx, packet2, senderAccAddr2). + setup.Mocks.IBCModuleMock.EXPECT().OnRecvPacket(ctx, packet2ModifiedSender, senderAccAddr2). Return(acknowledgement), setup.Mocks.TransferKeeperMock.EXPECT().Transfer( diff --git a/middleware/packet-forward-middleware/router/types/forward.go b/middleware/packet-forward-middleware/router/types/forward.go index ce16f098..aad670a3 100644 --- a/middleware/packet-forward-middleware/router/types/forward.go +++ b/middleware/packet-forward-middleware/router/types/forward.go @@ -31,13 +31,13 @@ type Duration time.Duration func (m *ForwardMetadata) Validate() error { if m.Receiver == "" { - return fmt.Errorf("failed to validate forward metadata. receiver cannot be empty") + return fmt.Errorf("failed to validate metadata. receiver cannot be empty") } if err := host.PortIdentifierValidator(m.Port); err != nil { - return fmt.Errorf("failed to validate forward metadata: %w", err) + return fmt.Errorf("failed to validate metadata: %w", err) } if err := host.ChannelIdentifierValidator(m.Channel); err != nil { - return fmt.Errorf("failed to validate forward metadata: %w", err) + return fmt.Errorf("failed to validate metadata: %w", err) } return nil diff --git a/modules/ibc-hooks/client/cli/query.go b/modules/ibc-hooks/client/cli/query.go new file mode 100644 index 00000000..339e5517 --- /dev/null +++ b/modules/ibc-hooks/client/cli/query.go @@ -0,0 +1,77 @@ +package cli + +import ( + "fmt" + "strings" + + "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7/keeper" + "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7/types" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/flags" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/version" +) + +func indexRunCmd(cmd *cobra.Command, args []string) error { + usageTemplate := `Usage:{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}} + +{{if .HasAvailableSubCommands}}Available Commands:{{range .Commands}}{{if .IsAvailableCommand}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +` + cmd.SetUsageTemplate(usageTemplate) + return cmd.Help() +} + +// GetQueryCmd returns the cli query commands for this module. +func GetQueryCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: types.ModuleName, + Short: fmt.Sprintf("Querying commands for the %s module", types.ModuleName), + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: indexRunCmd, + } + + cmd.Short = fmt.Sprintf("Querying commands for the %s module", types.ModuleName) + cmd.AddCommand( + GetCmdWasmSender(), + ) + return cmd +} + +// GetCmdPoolParams return pool params. +func GetCmdWasmSender() *cobra.Command { + cmd := &cobra.Command{ + Use: "wasm-sender ", + Short: "Generate the local address for a wasm hooks sender", + Long: strings.TrimSpace( + fmt.Sprintf(`Generate the local address for a wasm hooks sender. +Example: +$ %s query ibc-hooks wasm-hooks-sender channel-42 juno12smx2wdlyttvyzvzg54y2vnqwq2qjatezqwqxu +`, + version.AppName, + ), + ), + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + channelID := args[0] + originalSender := args[1] + // ToDo: Make this flexible as an arg + prefix := sdk.GetConfig().GetBech32AccountAddrPrefix() + senderBech32, err := keeper.DeriveIntermediateSender(channelID, originalSender, prefix) + if err != nil { + return err + } + fmt.Println(senderBech32) + return nil + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} diff --git a/modules/ibc-hooks/hooks.go b/modules/ibc-hooks/hooks.go new file mode 100644 index 00000000..b1394594 --- /dev/null +++ b/modules/ibc-hooks/hooks.go @@ -0,0 +1,145 @@ +package ibc_hooks + +import ( + // external libraries + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + // ibc-go + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" +) + +type Hooks interface{} + +type OnChanOpenInitOverrideHooks interface { + OnChanOpenInitOverride(im IBCMiddleware, ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID string, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, version string) (string, error) +} +type OnChanOpenInitBeforeHooks interface { + OnChanOpenInitBeforeHook(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID string, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, version string) +} +type OnChanOpenInitAfterHooks interface { + OnChanOpenInitAfterHook(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID string, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, version string, finalVersion string, err error) +} + +// OnChanOpenTry Hooks +type OnChanOpenTryOverrideHooks interface { + OnChanOpenTryOverride(im IBCMiddleware, ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, counterpartyVersion string) (string, error) +} +type OnChanOpenTryBeforeHooks interface { + OnChanOpenTryBeforeHook(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, counterpartyVersion string) +} +type OnChanOpenTryAfterHooks interface { + OnChanOpenTryAfterHook(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, channelCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, counterpartyVersion string, version string, err error) +} + +// OnChanOpenAck Hooks +type OnChanOpenAckOverrideHooks interface { + OnChanOpenAckOverride(im IBCMiddleware, ctx sdk.Context, portID, channelID string, counterpartyChannelID string, counterpartyVersion string) error +} +type OnChanOpenAckBeforeHooks interface { + OnChanOpenAckBeforeHook(ctx sdk.Context, portID, channelID string, counterpartyChannelID string, counterpartyVersion string) +} +type OnChanOpenAckAfterHooks interface { + OnChanOpenAckAfterHook(ctx sdk.Context, portID, channelID string, counterpartyChannelID string, counterpartyVersion string, err error) +} + +// OnChanOpenConfirm Hooks +type OnChanOpenConfirmOverrideHooks interface { + OnChanOpenConfirmOverride(im IBCMiddleware, ctx sdk.Context, portID, channelID string) error +} +type OnChanOpenConfirmBeforeHooks interface { + OnChanOpenConfirmBeforeHook(ctx sdk.Context, portID, channelID string) +} +type OnChanOpenConfirmAfterHooks interface { + OnChanOpenConfirmAfterHook(ctx sdk.Context, portID, channelID string, err error) +} + +// OnChanCloseInit Hooks +type OnChanCloseInitOverrideHooks interface { + OnChanCloseInitOverride(im IBCMiddleware, ctx sdk.Context, portID, channelID string) error +} +type OnChanCloseInitBeforeHooks interface { + OnChanCloseInitBeforeHook(ctx sdk.Context, portID, channelID string) +} +type OnChanCloseInitAfterHooks interface { + OnChanCloseInitAfterHook(ctx sdk.Context, portID, channelID string, err error) +} + +// OnChanCloseConfirm Hooks +type OnChanCloseConfirmOverrideHooks interface { + OnChanCloseConfirmOverride(im IBCMiddleware, ctx sdk.Context, portID, channelID string) error +} +type OnChanCloseConfirmBeforeHooks interface { + OnChanCloseConfirmBeforeHook(ctx sdk.Context, portID, channelID string) +} +type OnChanCloseConfirmAfterHooks interface { + OnChanCloseConfirmAfterHook(ctx sdk.Context, portID, channelID string, err error) +} + +// OnRecvPacket Hooks +type OnRecvPacketOverrideHooks interface { + OnRecvPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement +} +type OnRecvPacketBeforeHooks interface { + OnRecvPacketBeforeHook(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) +} +type OnRecvPacketAfterHooks interface { + OnRecvPacketAfterHook(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress, ack ibcexported.Acknowledgement) +} + +// OnAcknowledgementPacket Hooks +type OnAcknowledgementPacketOverrideHooks interface { + OnAcknowledgementPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress) error +} +type OnAcknowledgementPacketBeforeHooks interface { + OnAcknowledgementPacketBeforeHook(ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress) +} +type OnAcknowledgementPacketAfterHooks interface { + OnAcknowledgementPacketAfterHook(ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress, err error) +} + +// OnTimeoutPacket Hooks +type OnTimeoutPacketOverrideHooks interface { + OnTimeoutPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error +} +type OnTimeoutPacketBeforeHooks interface { + OnTimeoutPacketBeforeHook(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) +} +type OnTimeoutPacketAfterHooks interface { + OnTimeoutPacketAfterHook(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress, err error) +} + +// SendPacket Hooks +type SendPacketOverrideHooks interface { + SendPacketOverride(i ICS4Middleware, ctx sdk.Context, channelCap *capabilitytypes.Capability, sourcePort string, sourceChannel string, timeoutHeight ibcclienttypes.Height, timeoutTimestamp uint64, data []byte) (sequence uint64, err error) +} +type SendPacketBeforeHooks interface { + SendPacketBeforeHook(ctx sdk.Context, chanCap *capabilitytypes.Capability, sourcePort string, sourceChannel string, timeoutHeight ibcclienttypes.Height, timeoutTimestamp uint64, data []byte) +} +type SendPacketAfterHooks interface { + SendPacketAfterHook(ctx sdk.Context, chanCap *capabilitytypes.Capability, sourcePort string, sourceChannel string, timeoutHeight ibcclienttypes.Height, timeoutTimestamp uint64, data []byte, err error) +} + +// WriteAcknowledgement Hooks +type WriteAcknowledgementOverrideHooks interface { + WriteAcknowledgementOverride(i ICS4Middleware, ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) error +} +type WriteAcknowledgementBeforeHooks interface { + WriteAcknowledgementBeforeHook(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) +} +type WriteAcknowledgementAfterHooks interface { + WriteAcknowledgementAfterHook(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, ack ibcexported.Acknowledgement, err error) +} + +// GetAppVersion Hooks +type GetAppVersionOverrideHooks interface { + GetAppVersionOverride(i ICS4Middleware, ctx sdk.Context, portID, channelID string) (string, bool) +} +type GetAppVersionBeforeHooks interface { + GetAppVersionBeforeHook(ctx sdk.Context, portID, channelID string) +} +type GetAppVersionAfterHooks interface { + GetAppVersionAfterHook(ctx sdk.Context, portID, channelID string, result string, success bool) +} diff --git a/modules/ibc-hooks/ibc_module.go b/modules/ibc-hooks/ibc_module.go new file mode 100644 index 00000000..49e5b88c --- /dev/null +++ b/modules/ibc-hooks/ibc_module.go @@ -0,0 +1,262 @@ +package ibc_hooks + +import ( + // external libraries + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + // ibc-go + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" +) + +var _ porttypes.Middleware = &IBCMiddleware{} + +type IBCMiddleware struct { + App porttypes.IBCModule + ICS4Middleware *ICS4Middleware +} + +func NewIBCMiddleware(app porttypes.IBCModule, ics4 *ICS4Middleware) IBCMiddleware { + return IBCMiddleware{ + App: app, + ICS4Middleware: ics4, + } +} + +// OnChanOpenInit implements the IBCMiddleware interface +func (im IBCMiddleware) OnChanOpenInit( + ctx sdk.Context, + order channeltypes.Order, + connectionHops []string, + portID string, + channelID string, + channelCap *capabilitytypes.Capability, + counterparty channeltypes.Counterparty, + version string, +) (string, error) { + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenInitOverrideHooks); ok { + return hook.OnChanOpenInitOverride(im, ctx, order, connectionHops, portID, channelID, channelCap, counterparty, version) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenInitBeforeHooks); ok { + hook.OnChanOpenInitBeforeHook(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, version) + } + + finalVersion, err := im.App.OnChanOpenInit(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, version) + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenInitAfterHooks); ok { + hook.OnChanOpenInitAfterHook(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, version, finalVersion, err) + } + return version, err +} + +// OnChanOpenTry implements the IBCMiddleware interface +func (im IBCMiddleware) OnChanOpenTry( + ctx sdk.Context, + order channeltypes.Order, + connectionHops []string, + portID, + channelID string, + channelCap *capabilitytypes.Capability, + counterparty channeltypes.Counterparty, + counterpartyVersion string, +) (string, error) { + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenTryOverrideHooks); ok { + return hook.OnChanOpenTryOverride(im, ctx, order, connectionHops, portID, channelID, channelCap, counterparty, counterpartyVersion) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenTryBeforeHooks); ok { + hook.OnChanOpenTryBeforeHook(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, counterpartyVersion) + } + + version, err := im.App.OnChanOpenTry(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, counterpartyVersion) + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenTryAfterHooks); ok { + hook.OnChanOpenTryAfterHook(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, counterpartyVersion, version, err) + } + return version, err +} + +// OnChanOpenAck implements the IBCMiddleware interface +func (im IBCMiddleware) OnChanOpenAck( + ctx sdk.Context, + portID, + channelID string, + counterpartyChannelID string, + counterpartyVersion string, +) error { + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenAckOverrideHooks); ok { + return hook.OnChanOpenAckOverride(im, ctx, portID, channelID, counterpartyChannelID, counterpartyVersion) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenAckBeforeHooks); ok { + hook.OnChanOpenAckBeforeHook(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion) + } + err := im.App.OnChanOpenAck(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion) + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenAckAfterHooks); ok { + hook.OnChanOpenAckAfterHook(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion, err) + } + + return err +} + +// OnChanOpenConfirm implements the IBCMiddleware interface +func (im IBCMiddleware) OnChanOpenConfirm( + ctx sdk.Context, + portID, + channelID string, +) error { + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenConfirmOverrideHooks); ok { + return hook.OnChanOpenConfirmOverride(im, ctx, portID, channelID) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenConfirmBeforeHooks); ok { + hook.OnChanOpenConfirmBeforeHook(ctx, portID, channelID) + } + err := im.App.OnChanOpenConfirm(ctx, portID, channelID) + if hook, ok := im.ICS4Middleware.Hooks.(OnChanOpenConfirmAfterHooks); ok { + hook.OnChanOpenConfirmAfterHook(ctx, portID, channelID, err) + } + return err +} + +// OnChanCloseInit implements the IBCMiddleware interface +func (im IBCMiddleware) OnChanCloseInit( + ctx sdk.Context, + portID, + channelID string, +) error { + // Here we can remove the limits when a new channel is closed. For now, they can remove them manually on the contract + if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseInitOverrideHooks); ok { + return hook.OnChanCloseInitOverride(im, ctx, portID, channelID) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseInitBeforeHooks); ok { + hook.OnChanCloseInitBeforeHook(ctx, portID, channelID) + } + err := im.App.OnChanCloseInit(ctx, portID, channelID) + if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseInitAfterHooks); ok { + hook.OnChanCloseInitAfterHook(ctx, portID, channelID, err) + } + + return err +} + +// OnChanCloseConfirm implements the IBCMiddleware interface +func (im IBCMiddleware) OnChanCloseConfirm( + ctx sdk.Context, + portID, + channelID string, +) error { + // Here we can remove the limits when a new channel is closed. For now, they can remove them manually on the contract + if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseConfirmOverrideHooks); ok { + return hook.OnChanCloseConfirmOverride(im, ctx, portID, channelID) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseConfirmBeforeHooks); ok { + hook.OnChanCloseConfirmBeforeHook(ctx, portID, channelID) + } + err := im.App.OnChanCloseConfirm(ctx, portID, channelID) + if hook, ok := im.ICS4Middleware.Hooks.(OnChanCloseConfirmAfterHooks); ok { + hook.OnChanCloseConfirmAfterHook(ctx, portID, channelID, err) + } + + return err +} + +// OnRecvPacket implements the IBCMiddleware interface +func (im IBCMiddleware) OnRecvPacket( + ctx sdk.Context, + packet channeltypes.Packet, + relayer sdk.AccAddress, +) ibcexported.Acknowledgement { + if hook, ok := im.ICS4Middleware.Hooks.(OnRecvPacketOverrideHooks); ok { + return hook.OnRecvPacketOverride(im, ctx, packet, relayer) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnRecvPacketBeforeHooks); ok { + hook.OnRecvPacketBeforeHook(ctx, packet, relayer) + } + + ack := im.App.OnRecvPacket(ctx, packet, relayer) + + if hook, ok := im.ICS4Middleware.Hooks.(OnRecvPacketAfterHooks); ok { + hook.OnRecvPacketAfterHook(ctx, packet, relayer, ack) + } + + return ack +} + +// OnAcknowledgementPacket implements the IBCMiddleware interface +func (im IBCMiddleware) OnAcknowledgementPacket( + ctx sdk.Context, + packet channeltypes.Packet, + acknowledgement []byte, + relayer sdk.AccAddress, +) error { + if hook, ok := im.ICS4Middleware.Hooks.(OnAcknowledgementPacketOverrideHooks); ok { + return hook.OnAcknowledgementPacketOverride(im, ctx, packet, acknowledgement, relayer) + } + if hook, ok := im.ICS4Middleware.Hooks.(OnAcknowledgementPacketBeforeHooks); ok { + hook.OnAcknowledgementPacketBeforeHook(ctx, packet, acknowledgement, relayer) + } + + err := im.App.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer) + + if hook, ok := im.ICS4Middleware.Hooks.(OnAcknowledgementPacketAfterHooks); ok { + hook.OnAcknowledgementPacketAfterHook(ctx, packet, acknowledgement, relayer, err) + } + + return err +} + +// OnTimeoutPacket implements the IBCMiddleware interface +func (im IBCMiddleware) OnTimeoutPacket( + ctx sdk.Context, + packet channeltypes.Packet, + relayer sdk.AccAddress, +) error { + if hook, ok := im.ICS4Middleware.Hooks.(OnTimeoutPacketOverrideHooks); ok { + return hook.OnTimeoutPacketOverride(im, ctx, packet, relayer) + } + + if hook, ok := im.ICS4Middleware.Hooks.(OnTimeoutPacketBeforeHooks); ok { + hook.OnTimeoutPacketBeforeHook(ctx, packet, relayer) + } + err := im.App.OnTimeoutPacket(ctx, packet, relayer) + if hook, ok := im.ICS4Middleware.Hooks.(OnTimeoutPacketAfterHooks); ok { + hook.OnTimeoutPacketAfterHook(ctx, packet, relayer, err) + } + + return err +} + +// SendPacket implements the ICS4 Wrapper interface +func (im IBCMiddleware) SendPacket( + ctx sdk.Context, + chanCap *capabilitytypes.Capability, + sourcePort string, + sourceChannel string, + timeoutHeight ibcclienttypes.Height, + timeoutTimestamp uint64, + data []byte, +) (sequence uint64, err error) { + return im.ICS4Middleware.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) +} + +// WriteAcknowledgement implements the ICS4 Wrapper interface +func (im IBCMiddleware) WriteAcknowledgement( + ctx sdk.Context, + chanCap *capabilitytypes.Capability, + packet ibcexported.PacketI, + ack ibcexported.Acknowledgement, +) error { + return im.ICS4Middleware.WriteAcknowledgement(ctx, chanCap, packet, ack) +} + +func (im IBCMiddleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { + return im.ICS4Middleware.GetAppVersion(ctx, portID, channelID) +} diff --git a/modules/ibc-hooks/ics4_middleware.go b/modules/ibc-hooks/ics4_middleware.go new file mode 100644 index 00000000..25a2ed98 --- /dev/null +++ b/modules/ibc-hooks/ics4_middleware.go @@ -0,0 +1,78 @@ +package ibc_hooks + +import ( + // external libraries + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + // ibc-go + porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" +) + +var _ porttypes.ICS4Wrapper = &ICS4Middleware{} + +type ICS4Middleware struct { + channel porttypes.ICS4Wrapper + + // Hooks + Hooks Hooks +} + +func NewICS4Middleware(channel porttypes.ICS4Wrapper, hooks Hooks) ICS4Middleware { + return ICS4Middleware{ + channel: channel, + Hooks: hooks, + } +} + +func (i ICS4Middleware) SendPacket(ctx sdk.Context, channelCap *capabilitytypes.Capability, sourcePort string, sourceChannel string, timeoutHeight ibcclienttypes.Height, timeoutTimestamp uint64, data []byte) (sequence uint64, err error) { + if hook, ok := i.Hooks.(SendPacketOverrideHooks); ok { + return hook.SendPacketOverride(i, ctx, channelCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) + } + + if hook, ok := i.Hooks.(SendPacketBeforeHooks); ok { + hook.SendPacketBeforeHook(ctx, channelCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) + } + + seq, err := i.channel.SendPacket(ctx, channelCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) + + if hook, ok := i.Hooks.(SendPacketAfterHooks); ok { + hook.SendPacketAfterHook(ctx, channelCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data, err) + } + + return seq, err +} + +func (i ICS4Middleware) WriteAcknowledgement(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) error { + if hook, ok := i.Hooks.(WriteAcknowledgementOverrideHooks); ok { + return hook.WriteAcknowledgementOverride(i, ctx, chanCap, packet, ack) + } + + if hook, ok := i.Hooks.(WriteAcknowledgementBeforeHooks); ok { + hook.WriteAcknowledgementBeforeHook(ctx, chanCap, packet, ack) + } + err := i.channel.WriteAcknowledgement(ctx, chanCap, packet, ack) + if hook, ok := i.Hooks.(WriteAcknowledgementAfterHooks); ok { + hook.WriteAcknowledgementAfterHook(ctx, chanCap, packet, ack, err) + } + + return err +} + +func (i ICS4Middleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { + if hook, ok := i.Hooks.(GetAppVersionOverrideHooks); ok { + return hook.GetAppVersionOverride(i, ctx, portID, channelID) + } + + if hook, ok := i.Hooks.(GetAppVersionBeforeHooks); ok { + hook.GetAppVersionBeforeHook(ctx, portID, channelID) + } + version, err := i.channel.GetAppVersion(ctx, portID, channelID) + if hook, ok := i.Hooks.(GetAppVersionAfterHooks); ok { + hook.GetAppVersionAfterHook(ctx, portID, channelID, version, err) + } + + return version, err +} diff --git a/modules/ibc-hooks/keeper/keeper.go b/modules/ibc-hooks/keeper/keeper.go new file mode 100644 index 00000000..0e65e593 --- /dev/null +++ b/modules/ibc-hooks/keeper/keeper.go @@ -0,0 +1,62 @@ +package keeper + +import ( + "fmt" + + "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7/types" + + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + + "github.com/cometbft/cometbft/libs/log" +) + +type ( + Keeper struct { + storeKey storetypes.StoreKey + } +) + +// NewKeeper returns a new instance of the x/ibchooks keeper +func NewKeeper( + storeKey storetypes.StoreKey, +) Keeper { + return Keeper{ + storeKey: storeKey, + } +} + +// Logger returns a logger for the x/tokenfactory module +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) +} + +func GetPacketKey(channel string, packetSequence uint64) []byte { + return []byte(fmt.Sprintf("%s::%d", channel, packetSequence)) +} + +// StorePacketCallback stores which contract will be listening for the ack or timeout of a packet +func (k Keeper) StorePacketCallback(ctx sdk.Context, channel string, packetSequence uint64, contract string) { + store := ctx.KVStore(k.storeKey) + store.Set(GetPacketKey(channel, packetSequence), []byte(contract)) +} + +// GetPacketCallback returns the bech32 addr of the contract that is expecting a callback from a packet +func (k Keeper) GetPacketCallback(ctx sdk.Context, channel string, packetSequence uint64) string { + store := ctx.KVStore(k.storeKey) + return string(store.Get(GetPacketKey(channel, packetSequence))) +} + +// DeletePacketCallback deletes the callback from storage once it has been processed +func (k Keeper) DeletePacketCallback(ctx sdk.Context, channel string, packetSequence uint64) { + store := ctx.KVStore(k.storeKey) + store.Delete(GetPacketKey(channel, packetSequence)) +} + +func DeriveIntermediateSender(channel, originalSender, bech32Prefix string) (string, error) { + senderStr := fmt.Sprintf("%s/%s", channel, originalSender) + senderHash32 := address.Hash(types.SenderPrefix, []byte(senderStr)) + sender := sdk.AccAddress(senderHash32) + return sdk.Bech32ifyAddressBytes(bech32Prefix, sender) +} diff --git a/modules/ibc-hooks/sdkmodule.go b/modules/ibc-hooks/sdkmodule.go new file mode 100644 index 00000000..bd8d43ba --- /dev/null +++ b/modules/ibc-hooks/sdkmodule.go @@ -0,0 +1,120 @@ +package ibc_hooks + +import ( + "encoding/json" + + "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7/client/cli" + "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7/types" + "github.com/gorilla/mux" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + + abci "github.com/cometbft/cometbft/abci/types" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// AppModuleBasic defines the basic application module used by the ibc-hooks module. +type AppModuleBasic struct{} + +var _ module.AppModuleBasic = AppModuleBasic{} + +// Name returns the ibc-hooks module's name. +func (AppModuleBasic) Name() string { + return types.ModuleName +} + +// RegisterLegacyAminoCodec registers the ibc-hooks module's types on the given LegacyAmino codec. +func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {} + +// RegisterInterfaces registers the module's interface types. +func (b AppModuleBasic) RegisterInterfaces(_ cdctypes.InterfaceRegistry) {} + +// DefaultGenesis returns default genesis state as raw bytes for the +// module. +func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + emptyString := "{}" + return []byte(emptyString) +} + +// ValidateGenesis performs genesis state validation for the ibc-hooks module. +func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncodingConfig, bz json.RawMessage) error { + return nil +} + +// RegisterRESTRoutes registers the REST routes for the ibc-hooks module. +func (AppModuleBasic) RegisterRESTRoutes(clientCtx client.Context, rtr *mux.Router) {} + +// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the ibc-hooks module. +func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) {} + +// GetTxCmd returns no root tx command for the ibc-hooks module. +func (AppModuleBasic) GetTxCmd() *cobra.Command { return nil } + +// GetQueryCmd returns the root query command for the ibc-hooks module. +func (AppModuleBasic) GetQueryCmd() *cobra.Command { + return cli.GetQueryCmd() +} + +// ___________________________________________________________________________ + +// AppModule implements an application module for the ibc-hooks module. +type AppModule struct { + AppModuleBasic + + authKeeper authkeeper.AccountKeeper +} + +// NewAppModule creates a new AppModule object. +func NewAppModule(ak authkeeper.AccountKeeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + authKeeper: ak, + } +} + +// Name returns the ibc-hooks module's name. +func (AppModule) Name() string { + return types.ModuleName +} + +// RegisterInvariants registers the ibc-hooks module invariants. +func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// RegisterServices registers a gRPC query service to respond to the +// module-specific gRPC queries. +func (am AppModule) RegisterServices(cfg module.Configurator) { +} + +// InitGenesis performs genesis initialization for the ibc-hooks module. It returns +// no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} + +func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { + return json.RawMessage([]byte("{}")) +} + +// BeginBlock returns the begin blocker for the ibc-hooks module. +func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { +} + +// EndBlock returns the end blocker for the ibc-hooks module. It returns no validator +// updates. +func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} + +// ConsensusVersion implements AppModule/ConsensusVersion. +func (AppModule) ConsensusVersion() uint64 { return 1 } diff --git a/modules/ibc-hooks/simapp/ante.go b/modules/ibc-hooks/simapp/ante.go new file mode 100644 index 00000000..acb2fb99 --- /dev/null +++ b/modules/ibc-hooks/simapp/ante.go @@ -0,0 +1,65 @@ +package simapp + +import ( + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmTypes "github.com/CosmWasm/wasmd/x/wasm/types" + + errors "cosmossdk.io/errors" + + "github.com/cosmos/cosmos-sdk/codec" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth/ante" + + ibcante "github.com/cosmos/ibc-go/v7/modules/core/ante" + ibckeeper "github.com/cosmos/ibc-go/v7/modules/core/keeper" +) + +// HandlerOptions extends the SDK's AnteHandler options by requiring the IBC +// channel keeper. +type HandlerOptions struct { + ante.HandlerOptions + + IBCKeeper *ibckeeper.Keeper + TxCounterStoreKey storetypes.StoreKey + WasmConfig wasmTypes.WasmConfig + Cdc codec.BinaryCodec +} + +// NewAnteHandler returns an AnteHandler that checks and increments sequence +// numbers, checks signatures & account numbers, and deducts fees from the first +// signer. +func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { + if options.AccountKeeper == nil { + return nil, errors.Wrap(sdkerrors.ErrLogic, "account keeper is required for ante builder") + } + + if options.BankKeeper == nil { + return nil, errors.Wrap(sdkerrors.ErrLogic, "bank keeper is required for ante builder") + } + + if options.SignModeHandler == nil { + return nil, errors.Wrap(sdkerrors.ErrLogic, "sign mode handler is required for ante builder") + } + + anteDecorators := []sdk.AnteDecorator{ + ante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first + wasmkeeper.NewLimitSimulationGasDecorator(options.WasmConfig.SimulationGasLimit), + wasmkeeper.NewCountTXDecorator(options.TxCounterStoreKey), + ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker), + ante.NewValidateBasicDecorator(), + ante.NewTxTimeoutHeightDecorator(), + ante.NewValidateMemoDecorator(options.AccountKeeper), + ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper), + ante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker), + ante.NewSetPubKeyDecorator(options.AccountKeeper), // SetPubKeyDecorator must be called before all signature verification decorators + ante.NewValidateSigCountDecorator(options.AccountKeeper), + ante.NewSigGasConsumeDecorator(options.AccountKeeper, options.SigGasConsumer), + ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler), + ante.NewIncrementSequenceDecorator(options.AccountKeeper), + ibcante.NewRedundantRelayDecorator(options.IBCKeeper), + } + + return sdk.ChainAnteDecorators(anteDecorators...), nil +} diff --git a/modules/ibc-hooks/simapp/app.go b/modules/ibc-hooks/simapp/app.go new file mode 100644 index 00000000..2ce12d6e --- /dev/null +++ b/modules/ibc-hooks/simapp/app.go @@ -0,0 +1,991 @@ +package simapp + +import ( + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/CosmWasm/wasmd/x/wasm" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + ibchooks "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7" + ibchookskeeper "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7/keeper" + ibchookstypes "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7/types" + "github.com/prometheus/client_golang/prometheus" + "github.com/spf13/cast" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/grpc/node" + "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/runtime" + "github.com/cosmos/cosmos-sdk/server/api" + "github.com/cosmos/cosmos-sdk/server/config" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdktypes "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/version" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/ante" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authsim "github.com/cosmos/cosmos-sdk/x/auth/simulation" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + "github.com/cosmos/cosmos-sdk/x/authz" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + authzmodule "github.com/cosmos/cosmos-sdk/x/authz/module" + "github.com/cosmos/cosmos-sdk/x/bank" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/capability" + capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + "github.com/cosmos/cosmos-sdk/x/consensus" + consensuskeeper "github.com/cosmos/cosmos-sdk/x/consensus/keeper" + consensustypes "github.com/cosmos/cosmos-sdk/x/consensus/types" + "github.com/cosmos/cosmos-sdk/x/crisis" + crisiskeeper "github.com/cosmos/cosmos-sdk/x/crisis/keeper" + crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" + distr "github.com/cosmos/cosmos-sdk/x/distribution" + distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/cosmos/cosmos-sdk/x/evidence" + evidencekeeper "github.com/cosmos/cosmos-sdk/x/evidence/keeper" + evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" + "github.com/cosmos/cosmos-sdk/x/feegrant" + feegrantkeeper "github.com/cosmos/cosmos-sdk/x/feegrant/keeper" + feegrantmodule "github.com/cosmos/cosmos-sdk/x/feegrant/module" + "github.com/cosmos/cosmos-sdk/x/genutil" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + "github.com/cosmos/cosmos-sdk/x/gov" + govclient "github.com/cosmos/cosmos-sdk/x/gov/client" + govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + "github.com/cosmos/cosmos-sdk/x/group" + groupkeeper "github.com/cosmos/cosmos-sdk/x/group/keeper" + groupmodule "github.com/cosmos/cosmos-sdk/x/group/module" + "github.com/cosmos/cosmos-sdk/x/mint" + mintkeeper "github.com/cosmos/cosmos-sdk/x/mint/keeper" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + "github.com/cosmos/cosmos-sdk/x/nft" + nftkeeper "github.com/cosmos/cosmos-sdk/x/nft/keeper" + nftmodule "github.com/cosmos/cosmos-sdk/x/nft/module" + "github.com/cosmos/cosmos-sdk/x/params" + paramsclient "github.com/cosmos/cosmos-sdk/x/params/client" + paramskeeper "github.com/cosmos/cosmos-sdk/x/params/keeper" + paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" + paramsproposaltypes "github.com/cosmos/cosmos-sdk/x/params/types/proposal" + "github.com/cosmos/cosmos-sdk/x/slashing" + slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/cosmos/cosmos-sdk/x/upgrade" + upgradeclient "github.com/cosmos/cosmos-sdk/x/upgrade/client" + upgradekeeper "github.com/cosmos/cosmos-sdk/x/upgrade/keeper" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + + tmdb "github.com/cometbft/cometbft-db" + abcitypes "github.com/cometbft/cometbft/abci/types" + tmjson "github.com/cometbft/cometbft/libs/json" + "github.com/cometbft/cometbft/libs/log" + + ica "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts" + icacontrollerkeeper "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/controller/keeper" + icacontrollertypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/controller/types" + icahost "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/host" + icahostkeeper "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/host/keeper" + icahosttypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/host/types" + icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" + ibcfee "github.com/cosmos/ibc-go/v7/modules/apps/29-fee" + ibcfeekeeper "github.com/cosmos/ibc-go/v7/modules/apps/29-fee/keeper" + ibcfeetypes "github.com/cosmos/ibc-go/v7/modules/apps/29-fee/types" + ibctransfer "github.com/cosmos/ibc-go/v7/modules/apps/transfer" + ibctransferkeeper "github.com/cosmos/ibc-go/v7/modules/apps/transfer/keeper" + ibctransfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + ibc "github.com/cosmos/ibc-go/v7/modules/core" + ibcclient "github.com/cosmos/ibc-go/v7/modules/core/02-client" + ibcclientclient "github.com/cosmos/ibc-go/v7/modules/core/02-client/client" + ibcporttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" + ibckeeper "github.com/cosmos/ibc-go/v7/modules/core/keeper" + ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" +) + +// DO NOT change the names of these variables! +// TODO: to prevent other users from changing these variables, we could probably just publish our own package like https://pkg.go.dev/github.com/cosmos/cosmos-sdk/version +var ( + AccountAddressPrefix = "feath" + AccountPubKeyPrefix = "feathpub" + ValidatorAddressPrefix = "feathvaloper" + ValidatorPubKeyPrefix = "feathvaloperpub" + ConsensusNodeAddressPrefix = "feathvalcons" + ConsensusNodePubKeyPrefix = "feathvalconspub" + BondDenom = "featherstake" + AppName = "feather-core" +) + +// TODO: What is this? +func getGovProposalHandlers() []govclient.ProposalHandler { + var govProposalHandlers []govclient.ProposalHandler + // this line is used by starport scaffolding # stargate/app/govProposalHandlers + + govProposalHandlers = append(govProposalHandlers, + paramsclient.ProposalHandler, + upgradeclient.LegacyProposalHandler, + upgradeclient.LegacyCancelProposalHandler, + ibcclientclient.UpdateClientProposalHandler, + ibcclientclient.UpgradeProposalHandler, + // this line is used by starport scaffolding # stargate/app/govProposalHandler + ) + + return govProposalHandlers +} + +var ( + // DefaultNodeHome default home directories for the application daemon + DefaultNodeHome string + + // ModuleBasics defines the module BasicManager is in charge of setting up basic, + // non-dependant module elements, such as codec registration + // and genesis verification. + ModuleBasics = module.NewBasicManager( + auth.AppModuleBasic{}, + bank.AppModuleBasic{}, + authzmodule.AppModuleBasic{}, + capability.AppModuleBasic{}, + consensus.AppModuleBasic{}, + crisis.AppModuleBasic{}, + feegrantmodule.AppModuleBasic{}, + groupmodule.AppModuleBasic{}, + staking.AppModuleBasic{}, + mint.AppModuleBasic{}, + nftmodule.AppModuleBasic{}, + genutil.NewAppModuleBasic(genutiltypes.DefaultMessageValidator), + vesting.AppModuleBasic{}, + slashing.AppModuleBasic{}, + gov.NewAppModuleBasic(getGovProposalHandlers()), // TODO: Do we need the legacy proposal handlers? + distr.AppModuleBasic{}, + params.AppModuleBasic{}, + evidence.AppModuleBasic{}, + upgrade.AppModuleBasic{}, + ibc.AppModuleBasic{}, + ibctransfer.AppModuleBasic{}, + ica.AppModuleBasic{}, + ibcfee.AppModuleBasic{}, + ibchooks.AppModuleBasic{}, + wasm.AppModuleBasic{}, + ibctm.AppModuleBasic{}, + ) +) + +var ( + _ servertypes.Application = (*App)(nil) + _ runtime.AppI = (*App)(nil) +) + +func init() { + userHomeDir, err := os.UserHomeDir() + if err != nil { + panic(err) + } + + DefaultNodeHome = filepath.Join(userHomeDir, "."+AppName) +} + +// App extends an ABCI application, but with most of its parameters exported. +// They are exported for convenience in creating helper functions, as object +// capabilities aren't needed for testing. +type App struct { + *baseapp.BaseApp + + cdc codec.Codec + legacyAmino *codec.LegacyAmino + txConfig client.TxConfig + interfaceRegistry types.InterfaceRegistry + + keys map[string]*storetypes.KVStoreKey + + // keepers + AuthKeeper authkeeper.AccountKeeper // TODO: Do we even need to store this state? + AuthzKeeper authzkeeper.Keeper + BankKeeper bankkeeper.Keeper + CapabilityKeeper *capabilitykeeper.Keeper + StakingKeeper *stakingkeeper.Keeper + SlashingKeeper slashingkeeper.Keeper + MintKeeper mintkeeper.Keeper + DistrKeeper distrkeeper.Keeper + GovKeeper govkeeper.Keeper + CrisisKeeper *crisiskeeper.Keeper + UpgradeKeeper *upgradekeeper.Keeper + NftKeeper nftkeeper.Keeper + ParamsKeeper paramskeeper.Keeper + IBCKeeper *ibckeeper.Keeper // IBC Keeper must be a pointer in the app, so we can SetRouter on it correctly + EvidenceKeeper evidencekeeper.Keeper + TransferKeeper ibctransferkeeper.Keeper + ICAHostKeeper icahostkeeper.Keeper + FeeGrantKeeper feegrantkeeper.Keeper + GroupKeeper groupkeeper.Keeper + WasmKeeper wasm.Keeper + ConsensusParamsKeeper consensuskeeper.Keeper + IBCFeeKeeper ibcfeekeeper.Keeper + ContractKeeper wasmtypes.ContractOpsKeeper + + // IBC hooks + IBCHooksKeeper ibchookskeeper.Keeper + + // ModuleManager is the module manager + ModuleManager *module.Manager + + // sm is the simulation manager + sm *module.SimulationManager + configurator module.Configurator +} + +// NewSimApp returns a reference to an initialized blockchain app +func NewSimApp( + logger log.Logger, + db tmdb.DB, + traceStore io.Writer, + loadLatest bool, + skipUpgradeHeights map[int64]bool, + homePath string, + invCheckPeriod uint, + encodingConfig EncodingConfig, + appOpts servertypes.AppOptions, + baseAppOptions ...func(*baseapp.BaseApp), +) *App { + cdc := encodingConfig.Marshaler + legacyAmino := encodingConfig.Amino + interfaceRegistry := encodingConfig.InterfaceRegistry + txConfig := encodingConfig.TxConfig + tkeys := make(map[string]*storetypes.TransientStoreKey) + memkeys := make(map[string]*storetypes.MemoryStoreKey) + + // Init App + app := &App{ + BaseApp: baseapp.NewBaseApp( + AppName, + logger, + db, + encodingConfig.TxConfig.TxDecoder(), + baseAppOptions..., + ), + cdc: cdc, + legacyAmino: legacyAmino, + interfaceRegistry: interfaceRegistry, + txConfig: txConfig, + keys: make(map[string]*storetypes.KVStoreKey), + } + defer app.Seal() + app.SetCommitMultiStoreTracer(traceStore) + app.SetVersion(version.Version) + app.SetInterfaceRegistry(interfaceRegistry) + + modules := make([]module.AppModule, 0) + simModules := make([]module.AppModuleSimulation, 0) + + // 'auth' module + app.keys[authtypes.StoreKey] = storetypes.NewKVStoreKey(authtypes.StoreKey) + app.AuthKeeper = authkeeper.NewAccountKeeper( + cdc, + app.keys[authtypes.StoreKey], + authtypes.ProtoBaseAccount, + make(map[string][]string), // This will be populated by each module later + AccountAddressPrefix, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) + defer func() { // TODO: Does deferring this even work? + app.AuthKeeper.GetModulePermissions()[authtypes.FeeCollectorName] = authtypes.NewPermissionsForAddress(authtypes.FeeCollectorName, nil) // This implicitly creates a module account + app.BankKeeper.GetBlockedAddresses()[authtypes.NewModuleAddress(authtypes.FeeCollectorName).String()] = true + }() + modules = append(modules, auth.NewAppModule(cdc, app.AuthKeeper, nil, nil)) + simModules = append(simModules, auth.NewAppModule(cdc, app.AuthKeeper, authsim.RandomGenesisAccounts, nil)) + + // 'bank' module - depends on + // 1. 'auth' + app.keys[banktypes.StoreKey] = storetypes.NewKVStoreKey(banktypes.StoreKey) + app.BankKeeper = bankkeeper.NewBaseKeeper( + cdc, + app.keys[banktypes.StoreKey], + app.AuthKeeper, + make(map[string]bool), + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) + modules = append(modules, bank.NewAppModule(cdc, app.BankKeeper, app.AuthKeeper, nil)) + simModules = append(simModules, bank.NewAppModule(cdc, app.BankKeeper, app.AuthKeeper, nil)) + + // 'authz' module - depends on + // 1. 'auth' + // 2. 'bank' + app.keys[authzkeeper.StoreKey] = storetypes.NewKVStoreKey(authzkeeper.StoreKey) + app.AuthzKeeper = authzkeeper.NewKeeper( + app.keys[authzkeeper.StoreKey], + cdc, + app.MsgServiceRouter(), + app.AuthKeeper, + ) + modules = append(modules, authzmodule.NewAppModule(cdc, app.AuthzKeeper, app.AuthKeeper, app.BankKeeper, interfaceRegistry)) + simModules = append(simModules, authzmodule.NewAppModule(cdc, app.AuthzKeeper, app.AuthKeeper, app.BankKeeper, interfaceRegistry)) + + // 'capability' module + app.keys[capabilitytypes.StoreKey] = storetypes.NewKVStoreKey(capabilitytypes.StoreKey) + memkeys[capabilitytypes.MemStoreKey] = storetypes.NewMemoryStoreKey(capabilitytypes.MemStoreKey) + app.CapabilityKeeper = capabilitykeeper.NewKeeper( + cdc, + app.keys[capabilitytypes.StoreKey], + memkeys[capabilitytypes.MemStoreKey], + ) + defer app.CapabilityKeeper.Seal() + modules = append(modules, capability.NewAppModule(cdc, *app.CapabilityKeeper, false)) // TODO: Find out what is sealkeeper + simModules = append(simModules, capability.NewAppModule(cdc, *app.CapabilityKeeper, false)) + + // 'consensus' module + app.keys[consensustypes.StoreKey] = storetypes.NewKVStoreKey(consensustypes.StoreKey) + app.ConsensusParamsKeeper = consensuskeeper.NewKeeper( + cdc, + app.keys[consensustypes.StoreKey], + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) + app.SetParamStore(&app.ConsensusParamsKeeper) + modules = append(modules, consensus.NewAppModule(cdc, app.ConsensusParamsKeeper)) + + // 'crisis' module - depends on + // 1. 'bank' + app.keys[crisistypes.StoreKey] = storetypes.NewKVStoreKey(crisistypes.StoreKey) + app.CrisisKeeper = crisiskeeper.NewKeeper( + cdc, + app.keys[crisistypes.StoreKey], + invCheckPeriod, + app.BankKeeper, + authtypes.FeeCollectorName, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) + modules = append(modules, crisis.NewAppModule(app.CrisisKeeper, false, nil)) // Never skip invariant checks on genesis + defer func() { app.ModuleManager.RegisterInvariants(app.CrisisKeeper) }() + + // 'feegrant' module - depends on + // 1. 'auth' + // 2. 'bank' + app.keys[feegrant.StoreKey] = storetypes.NewKVStoreKey(feegrant.StoreKey) + app.FeeGrantKeeper = feegrantkeeper.NewKeeper( + cdc, + app.keys[feegrant.StoreKey], + app.AuthKeeper, + ) + modules = append(modules, feegrantmodule.NewAppModule(cdc, app.AuthKeeper, app.BankKeeper, app.FeeGrantKeeper, interfaceRegistry)) + simModules = append(simModules, feegrantmodule.NewAppModule(cdc, app.AuthKeeper, app.BankKeeper, app.FeeGrantKeeper, interfaceRegistry)) + + // 'group' module - depends on + // 1. 'auth' + // 2. 'bank' + app.keys[group.StoreKey] = storetypes.NewKVStoreKey(group.StoreKey) + app.GroupKeeper = groupkeeper.NewKeeper( + app.keys[group.StoreKey], + cdc, + app.MsgServiceRouter(), + app.AuthKeeper, + group.DefaultConfig(), + ) + modules = append(modules, groupmodule.NewAppModule(cdc, app.GroupKeeper, app.AuthKeeper, app.BankKeeper, interfaceRegistry)) + simModules = append(simModules, groupmodule.NewAppModule(cdc, app.GroupKeeper, app.AuthKeeper, app.BankKeeper, interfaceRegistry)) + + // 'staking' module - depends on + // 1. 'auth' + // 2. 'bank' + app.AuthKeeper.GetModulePermissions()[stakingtypes.BondedPoolName] = authtypes.NewPermissionsForAddress(stakingtypes.BondedPoolName, []string{authtypes.Burner, authtypes.Staking}) + app.BankKeeper.GetBlockedAddresses()[authtypes.NewModuleAddress(stakingtypes.BondedPoolName).String()] = true + app.AuthKeeper.GetModulePermissions()[stakingtypes.NotBondedPoolName] = authtypes.NewPermissionsForAddress(stakingtypes.NotBondedPoolName, []string{authtypes.Burner, authtypes.Staking}) + app.BankKeeper.GetBlockedAddresses()[authtypes.NewModuleAddress(stakingtypes.NotBondedPoolName).String()] = true + app.keys[stakingtypes.StoreKey] = storetypes.NewKVStoreKey(stakingtypes.StoreKey) + app.StakingKeeper = stakingkeeper.NewKeeper( + cdc, + app.keys[stakingtypes.StoreKey], + app.AuthKeeper, + app.BankKeeper, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) + stakingHooks := make([]stakingtypes.StakingHooks, 0) + defer func() { app.StakingKeeper.SetHooks(stakingtypes.NewMultiStakingHooks(stakingHooks...)) }() + modules = append(modules, staking.NewAppModule(cdc, app.StakingKeeper, app.AuthKeeper, app.BankKeeper, nil)) + simModules = append(simModules, staking.NewAppModule(cdc, app.StakingKeeper, app.AuthKeeper, app.BankKeeper, nil)) + + // 'mint' module - depends on + // 1. 'staking' + // 2. 'auth' + // 3. 'bank' + app.AuthKeeper.GetModulePermissions()[minttypes.ModuleName] = authtypes.NewPermissionsForAddress(minttypes.ModuleName, []string{authtypes.Minter}) + app.BankKeeper.GetBlockedAddresses()[authtypes.NewModuleAddress(minttypes.ModuleName).String()] = true + app.keys[minttypes.StoreKey] = storetypes.NewKVStoreKey(minttypes.StoreKey) + app.MintKeeper = mintkeeper.NewKeeper( + cdc, + app.keys[minttypes.StoreKey], + app.StakingKeeper, + app.AuthKeeper, + app.BankKeeper, + authtypes.FeeCollectorName, // TODO: Find out what this is + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) + modules = append(modules, mint.NewAppModule(cdc, app.MintKeeper, app.AuthKeeper, nil, nil)) + simModules = append(simModules, mint.NewAppModule(cdc, app.MintKeeper, app.AuthKeeper, nil, nil)) + + // 'nft' module - depends on + // 1. 'auth' + // 2. 'bank' + app.AuthKeeper.GetModulePermissions()[nft.ModuleName] = authtypes.NewPermissionsForAddress(nft.ModuleName, nil) + app.BankKeeper.GetBlockedAddresses()[authtypes.NewModuleAddress(nft.ModuleName).String()] = true + app.keys[nftkeeper.StoreKey] = storetypes.NewKVStoreKey(nftkeeper.StoreKey) + app.NftKeeper = nftkeeper.NewKeeper( + app.keys[nftkeeper.StoreKey], + cdc, + app.AuthKeeper, + app.BankKeeper, + ) + modules = append(modules, nftmodule.NewAppModule(cdc, app.NftKeeper, app.AuthKeeper, app.BankKeeper, interfaceRegistry)) + simModules = append(simModules, nftmodule.NewAppModule(cdc, app.NftKeeper, app.AuthKeeper, app.BankKeeper, interfaceRegistry)) + + // 'genutil' module - depends on + // 1. 'auth' + // 2. 'staking' + modules = append(modules, genutil.NewAppModule(app.AuthKeeper, app.StakingKeeper, app.BaseApp.DeliverTx, encodingConfig.TxConfig)) + + // 'vesting' module - depends on + // 1. 'auth' + // 2. 'bank' + modules = append(modules, vesting.NewAppModule(app.AuthKeeper, app.BankKeeper)) + + // 'slashing' module - depends on + // 1. 'staking' + // 2. 'auth' + // 3. 'bank' + app.keys[slashingtypes.StoreKey] = storetypes.NewKVStoreKey(slashingtypes.StoreKey) + app.SlashingKeeper = slashingkeeper.NewKeeper( + cdc, + encodingConfig.Amino, + app.keys[slashingtypes.StoreKey], + app.StakingKeeper, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) + stakingHooks = append(stakingHooks, app.SlashingKeeper.Hooks()) + modules = append(modules, slashing.NewAppModule(cdc, app.SlashingKeeper, app.AuthKeeper, app.BankKeeper, app.StakingKeeper, nil)) + simModules = append(simModules, slashing.NewAppModule(cdc, app.SlashingKeeper, app.AuthKeeper, app.BankKeeper, app.StakingKeeper, nil)) + + // 'gov' module - depends on + // 1. 'auth' + // 2. 'bank' + // 3. 'staking' + app.AuthKeeper.GetModulePermissions()[govtypes.ModuleName] = authtypes.NewPermissionsForAddress(govtypes.ModuleName, []string{authtypes.Burner}) + app.keys[govtypes.StoreKey] = storetypes.NewKVStoreKey(govtypes.StoreKey) + app.GovKeeper = *govkeeper.NewKeeper( + cdc, + app.keys[govtypes.StoreKey], + app.AuthKeeper, + app.BankKeeper, + app.StakingKeeper, + app.MsgServiceRouter(), + govtypes.DefaultConfig(), + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) + // Set legacy router for backwards compatibility with gov v1beta1 + govLegacyRouter := govv1beta1.NewRouter() + defer app.GovKeeper.SetLegacyRouter(govLegacyRouter) + govLegacyRouter.AddRoute(govtypes.RouterKey, govv1beta1.ProposalHandler) + modules = append(modules, gov.NewAppModule(cdc, &app.GovKeeper, app.AuthKeeper, app.BankKeeper, nil)) + simModules = append(simModules, gov.NewAppModule(cdc, &app.GovKeeper, app.AuthKeeper, app.BankKeeper, nil)) + + // 'distribution' module - depends on + // 1. 'auth' + // 2. 'bank' + // 3. 'staking' + // 4. 'gov' + app.AuthKeeper.GetModulePermissions()[distrtypes.ModuleName] = authtypes.NewPermissionsForAddress(distrtypes.ModuleName, nil) + app.BankKeeper.GetBlockedAddresses()[authtypes.NewModuleAddress(distrtypes.ModuleName).String()] = true + app.keys[distrtypes.StoreKey] = storetypes.NewKVStoreKey(distrtypes.StoreKey) + app.DistrKeeper = distrkeeper.NewKeeper( + cdc, + app.keys[distrtypes.StoreKey], + app.AuthKeeper, + app.BankKeeper, + app.StakingKeeper, + authtypes.FeeCollectorName, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) + stakingHooks = append(stakingHooks, app.DistrKeeper.Hooks()) + modules = append(modules, distr.NewAppModule(cdc, app.DistrKeeper, app.AuthKeeper, app.BankKeeper, app.StakingKeeper, nil)) + simModules = append(simModules, distr.NewAppModule(cdc, app.DistrKeeper, app.AuthKeeper, app.BankKeeper, app.StakingKeeper, nil)) + + // 'params' module - depends on + // 1. 'gov' + app.keys[paramstypes.StoreKey] = storetypes.NewKVStoreKey(paramstypes.StoreKey) + tkeys[paramstypes.TStoreKey] = storetypes.NewTransientStoreKey(paramstypes.TStoreKey) + app.ParamsKeeper = paramskeeper.NewKeeper( + cdc, + legacyAmino, + app.keys[paramstypes.StoreKey], + tkeys[paramstypes.TStoreKey], + ) + govLegacyRouter.AddRoute(paramsproposaltypes.RouterKey, params.NewParamChangeProposalHandler(app.ParamsKeeper)) + modules = append(modules, params.NewAppModule(app.ParamsKeeper)) + simModules = append(simModules, params.NewAppModule(app.ParamsKeeper)) + + // 'evidence' module - depends on + // 1. 'staking' + // 2. 'slashing' + app.keys[evidencetypes.StoreKey] = storetypes.NewKVStoreKey(evidencetypes.StoreKey) + app.EvidenceKeeper = *evidencekeeper.NewKeeper( + cdc, + app.keys[evidencetypes.StoreKey], + app.StakingKeeper, + app.SlashingKeeper, + ) + modules = append(modules, evidence.NewAppModule(app.EvidenceKeeper)) + simModules = append(simModules, evidence.NewAppModule(app.EvidenceKeeper)) + + // 'upgrade' module - depends on + // 1. 'gov' + app.keys[upgradetypes.StoreKey] = storetypes.NewKVStoreKey(upgradetypes.StoreKey) + app.UpgradeKeeper = upgradekeeper.NewKeeper( + skipUpgradeHeights, // TODO: What is this? + app.keys[upgradetypes.StoreKey], + cdc, + homePath, + app, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) + govLegacyRouter.AddRoute(upgradetypes.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(app.UpgradeKeeper)) + modules = append(modules, upgrade.NewAppModule(app.UpgradeKeeper)) + + // 'ibc' module - depends on + // 1. 'staking' + // 2. 'upgrade' + // 3. 'capability' + // 4. 'gov' + // 5. 'params' + app.keys[ibcexported.StoreKey] = storetypes.NewKVStoreKey(ibcexported.StoreKey) + app.IBCKeeper = ibckeeper.NewKeeper( + cdc, + app.keys[ibcexported.StoreKey], + app.ParamsKeeper.Subspace(ibcexported.ModuleName), + app.StakingKeeper, + app.UpgradeKeeper, + app.CapabilityKeeper.ScopeToModule(ibcexported.ModuleName), + ) + govLegacyRouter.AddRoute(ibcexported.RouterKey, ibcclient.NewClientProposalHandler(app.IBCKeeper.ClientKeeper)) + modules = append(modules, ibc.NewAppModule(app.IBCKeeper)) + simModules = append(simModules, ibc.NewAppModule(app.IBCKeeper)) + + // 'ibcfeekeeper' module - depends on + // 1. 'bank' + // 2. 'auth' + // 3. 'ibc channel' + // 4. 'ibc port' + app.IBCFeeKeeper = ibcfeekeeper.NewKeeper( + app.cdc, + app.keys[ibcfeetypes.StoreKey], + app.IBCKeeper.ChannelKeeper, + app.IBCKeeper.ChannelKeeper, + &app.IBCKeeper.PortKeeper, + app.AuthKeeper, + app.BankKeeper, + ) + app.keys[ibcporttypes.StoreKey] = storetypes.NewKVStoreKey(ibcporttypes.StoreKey) + app.AuthKeeper.GetModulePermissions()[ibctransfertypes.ModuleName] = authtypes.NewPermissionsForAddress(ibcfeetypes.ModuleName, nil) + icaHostIBCModule := icahost.NewIBCModule(app.ICAHostKeeper) + icaHostStack := ibcfee.NewIBCMiddleware(icaHostIBCModule, app.IBCFeeKeeper) + + modules = append(modules, ibcfee.NewAppModule(app.IBCFeeKeeper)) + simModules = append(simModules, ibcfee.NewAppModule(app.IBCFeeKeeper)) + + // Create fee enabled wasm ibc Stack + var wasmStack ibcporttypes.IBCModule + wasmStack = wasm.NewIBCHandler(app.WasmKeeper, app.IBCKeeper.ChannelKeeper, app.IBCFeeKeeper) + wasmStack = ibcfee.NewIBCMiddleware(wasmStack, app.IBCFeeKeeper) + + app.IBCKeeper.SetRouter(ibcporttypes.NewRouter(). + AddRoute(icahosttypes.SubModuleName, icaHostStack). + AddRoute(wasm.ModuleName, wasmStack)) + + // 'ibc-hooks' module - depends on + // 1. 'auth' + // 2. 'bank' + // 3. 'distr' + app.keys[ibchookstypes.StoreKey] = storetypes.NewKVStoreKey(ibchookstypes.StoreKey) + app.IBCHooksKeeper = ibchookskeeper.NewKeeper( + app.keys[ibchookstypes.StoreKey], + ) + ics20WasmHooks := ibchooks.NewWasmHooks(&app.IBCHooksKeeper, nil, AccountAddressPrefix) // The contract keeper needs to be set later + hooksICS4Wrapper := ibchooks.NewICS4Middleware( + app.IBCKeeper.ChannelKeeper, + ics20WasmHooks, + ) + + // 'ibctransfer' module - depends on + // 1. 'ibc' + // 2. 'auth' + // 3. 'bank' + // 4. 'capability' + app.AuthKeeper.GetModulePermissions()[ibctransfertypes.ModuleName] = authtypes.NewPermissionsForAddress(ibctransfertypes.ModuleName, []string{authtypes.Minter, authtypes.Burner}) + app.BankKeeper.GetBlockedAddresses()[authtypes.NewModuleAddress(ibctransfertypes.ModuleName).String()] = true + app.keys[ibctransfertypes.StoreKey] = storetypes.NewKVStoreKey(ibctransfertypes.StoreKey) + app.TransferKeeper = ibctransferkeeper.NewKeeper( + cdc, + app.keys[ibctransfertypes.StoreKey], + app.ParamsKeeper.Subspace(ibctransfertypes.ModuleName), + hooksICS4Wrapper, + app.IBCKeeper.ChannelKeeper, + &app.IBCKeeper.PortKeeper, + app.AuthKeeper, + app.BankKeeper, + app.CapabilityKeeper.ScopeToModule(ibctransfertypes.ModuleName), + ) + modules = append(modules, ibctransfer.NewAppModule(app.TransferKeeper)) + simModules = append(simModules, ibctransfer.NewAppModule(app.TransferKeeper)) + + // 'ica' + app.AuthKeeper.GetModulePermissions()[icatypes.ModuleName] = authtypes.NewPermissionsForAddress(icatypes.ModuleName, nil) + app.BankKeeper.GetBlockedAddresses()[authtypes.NewModuleAddress(icatypes.ModuleName).String()] = true + + // 'icacontroller' module - depends on + // 1. 'ibc' + // 2. 'capability' + app.keys[icacontrollertypes.StoreKey] = storetypes.NewKVStoreKey(icacontrollertypes.StoreKey) + icaControllerKeeper := icacontrollerkeeper.NewKeeper( + cdc, + app.keys[icacontrollertypes.StoreKey], + app.ParamsKeeper.Subspace(icacontrollertypes.SubModuleName), + app.IBCKeeper.ChannelKeeper, // may be replaced with middleware such as ics29 fee + app.IBCKeeper.ChannelKeeper, + &app.IBCKeeper.PortKeeper, + app.CapabilityKeeper.ScopeToModule(icacontrollertypes.SubModuleName), + app.MsgServiceRouter(), + ) + + // 'icahost' module - depends on + // 1. 'ibc' + // 2. 'auth' + // 3. 'capability' + // 4. 'icacontroller' + app.keys[icahosttypes.StoreKey] = storetypes.NewKVStoreKey(icahosttypes.StoreKey) + app.ICAHostKeeper = icahostkeeper.NewKeeper( + cdc, + app.keys[icahosttypes.StoreKey], + app.ParamsKeeper.Subspace(icahosttypes.SubModuleName), + app.IBCKeeper.ChannelKeeper, + app.IBCKeeper.ChannelKeeper, + &app.IBCKeeper.PortKeeper, + app.AuthKeeper, + app.CapabilityKeeper.ScopeToModule(icahosttypes.SubModuleName), + app.MsgServiceRouter(), + ) + // app.IBCKeeper.Router.AddRoute(icahosttypes.SubModuleName, icahost.NewIBCModule(app.ICAHostKeeper)) + modules = append(modules, ica.NewAppModule(&icaControllerKeeper, &app.ICAHostKeeper)) + simModules = append(simModules, ica.NewAppModule(&icaControllerKeeper, &app.ICAHostKeeper)) + + // 'wasm' module - depends on + // 1. 'gov' + // 2. 'auth' + // 3. 'bank' + // 4. 'staking' + // 5. 'distribution' + // 6. 'capability' + // 7. 'ibc' + // 8. 'ibctransfer' + app.AuthKeeper.GetModulePermissions()[wasmtypes.ModuleName] = authtypes.NewPermissionsForAddress(wasmtypes.ModuleName, []string{authtypes.Burner}) + app.BankKeeper.GetBlockedAddresses()[authtypes.NewModuleAddress(wasmtypes.ModuleName).String()] = true + app.keys[wasmtypes.StoreKey] = storetypes.NewKVStoreKey(wasmtypes.StoreKey) + wasmConfig, err := wasm.ReadWasmConfig(appOpts) + if err != nil { + panic(fmt.Sprintf("error while reading wasm config: %s", err)) + } + app.WasmKeeper = wasm.NewKeeper( + cdc, + app.keys[wasmtypes.StoreKey], + app.AuthKeeper, + app.BankKeeper, + app.StakingKeeper, + distrkeeper.NewQuerier(app.DistrKeeper), + app.IBCKeeper.ChannelKeeper, + &app.IBCKeeper.PortKeeper, + app.CapabilityKeeper.ScopeToModule(wasm.ModuleName), + app.TransferKeeper, + app.MsgServiceRouter(), + app.GRPCQueryRouter(), + filepath.Join(homePath, "wasm"), + wasmConfig, + "iterator,staking,stargate,token_factory,cosmwasm_1_1", + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + GetWasmOpts(app, appOpts)..., + ) + govLegacyRouter.AddRoute(wasm.RouterKey, wasm.NewWasmProposalHandler(app.WasmKeeper, wasm.EnableAllProposals)) + modules = append(modules, wasm.NewAppModule(cdc, &app.WasmKeeper, app.StakingKeeper, app.AuthKeeper, app.BankKeeper, app.MsgServiceRouter(), nil)) + simModules = append(simModules, wasm.NewAppModule(cdc, &app.WasmKeeper, app.StakingKeeper, app.AuthKeeper, app.BankKeeper, app.MsgServiceRouter(), nil)) + app.ContractKeeper = wasmkeeper.NewDefaultPermissionKeeper(&app.WasmKeeper) + + /**** Module Options ****/ + + // NOTE: Any module instantiated in the module manager that is later modified + // must be passed by reference here. + + app.ModuleManager = module.NewManager(modules...) + + // During begin block slashing happens after distr.BeginBlocker so that + // there is nothing left over in the validator fee pool, so as to keep the + // CanWithdrawInvariant invariant. + // NOTE: staking module is required if HistoricalEntries param > 0 + app.ModuleManager.SetOrderBeginBlockers( + // upgrades should be run first + upgradetypes.ModuleName, + capabilitytypes.ModuleName, + minttypes.ModuleName, + consensustypes.ModuleName, + distrtypes.ModuleName, + slashingtypes.ModuleName, + evidencetypes.ModuleName, + stakingtypes.ModuleName, + authtypes.ModuleName, + banktypes.ModuleName, + govtypes.ModuleName, + crisistypes.ModuleName, + ibctransfertypes.ModuleName, + ibcexported.ModuleName, + icatypes.ModuleName, + ibcfeetypes.ModuleName, + genutiltypes.ModuleName, + authz.ModuleName, + feegrant.ModuleName, + group.ModuleName, + paramstypes.ModuleName, + vestingtypes.ModuleName, + nft.ModuleName, + ibchookstypes.ModuleName, + wasm.ModuleName, + // this line is used by starport scaffolding # stargate/app/beginBlockers + ) + + app.ModuleManager.SetOrderEndBlockers( + crisistypes.ModuleName, + govtypes.ModuleName, + stakingtypes.ModuleName, + consensustypes.ModuleName, + ibctransfertypes.ModuleName, + ibcexported.ModuleName, + icatypes.ModuleName, + capabilitytypes.ModuleName, + authtypes.ModuleName, + banktypes.ModuleName, + distrtypes.ModuleName, + slashingtypes.ModuleName, + minttypes.ModuleName, + genutiltypes.ModuleName, + evidencetypes.ModuleName, + authz.ModuleName, + feegrant.ModuleName, + group.ModuleName, + paramstypes.ModuleName, + upgradetypes.ModuleName, + vestingtypes.ModuleName, + nft.ModuleName, + ibcfeetypes.ModuleName, + ibchookstypes.ModuleName, + wasm.ModuleName, + // this line is used by starport scaffolding # stargate/app/endBlockers + ) + + // NOTE: The genutils module must occur after staking so that pools are + // properly initialized with tokens from genesis accounts. + // NOTE: Capability module must occur first so that it can initialize any capabilities + // so that other modules that want to create or claim capabilities afterwards in InitChain + // can do so safely. + app.ModuleManager.SetOrderInitGenesis( + capabilitytypes.ModuleName, + authtypes.ModuleName, + banktypes.ModuleName, + distrtypes.ModuleName, + stakingtypes.ModuleName, + slashingtypes.ModuleName, + consensustypes.ModuleName, + govtypes.ModuleName, + minttypes.ModuleName, + crisistypes.ModuleName, + genutiltypes.ModuleName, + ibctransfertypes.ModuleName, + ibcexported.ModuleName, + icatypes.ModuleName, + evidencetypes.ModuleName, + authz.ModuleName, + feegrant.ModuleName, + group.ModuleName, + paramstypes.ModuleName, + upgradetypes.ModuleName, + vestingtypes.ModuleName, + nft.ModuleName, + ibcfeetypes.ModuleName, + ibchookstypes.ModuleName, + wasm.ModuleName, + // this line is used by starport scaffolding # stargate/app/initGenesis + ) + + // Uncomment if you want to set a custom migration order here. + // app.mm.SetOrderMigrations(custom order) + + app.configurator = module.NewConfigurator(cdc, app.MsgServiceRouter(), app.GRPCQueryRouter()) + app.ModuleManager.RegisterServices(app.configurator) + + app.MountKVStores(app.keys) + app.MountTransientStores(tkeys) + app.MountMemoryStores(memkeys) + + // create the simulation manager and define the order of the modules for deterministic simulations + app.sm = module.NewSimulationManager(simModules...) + app.sm.RegisterStoreDecoders() + + // initialize BaseApp + app.SetInitChainer(app.InitChainer) + app.SetBeginBlocker(app.BeginBlocker) + + anteHandler, err := NewAnteHandler( + HandlerOptions{ + HandlerOptions: ante.HandlerOptions{ + AccountKeeper: app.AuthKeeper, + BankKeeper: app.BankKeeper, + SignModeHandler: encodingConfig.TxConfig.SignModeHandler(), + FeegrantKeeper: app.FeeGrantKeeper, + SigGasConsumer: ante.DefaultSigVerificationGasConsumer, + }, + IBCKeeper: app.IBCKeeper, + TxCounterStoreKey: app.keys[wasmtypes.StoreKey], + WasmConfig: wasmConfig, + Cdc: cdc, + }, + ) + if err != nil { + panic(fmt.Errorf("failed to create AnteHandler: %s", err)) + } + + app.SetAnteHandler(anteHandler) + app.SetEndBlocker(app.EndBlocker) + + if loadLatest { + if err := app.LoadLatestVersion(); err != nil { + logger.Error("error on loading last version", "err", err) + os.Exit(1) // nolint + } + } + + return app +} + +// GetWasmOpts build wasm options +func GetWasmOpts(app *App, appOpts servertypes.AppOptions) []wasm.Option { + var wasmOpts []wasm.Option + if cast.ToBool(appOpts.Get("telemetry.enabled")) { + wasmOpts = append(wasmOpts, wasmkeeper.WithVMCacheMetrics(prometheus.DefaultRegisterer)) + } + + return wasmOpts +} + +// Name returns the name of the App +func (app *App) Name() string { return app.BaseApp.Name() } + +// BeginBlocker application updates every begin block +func (app *App) BeginBlocker(ctx sdktypes.Context, req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { + return app.ModuleManager.BeginBlock(ctx, req) +} + +// EndBlocker application updates every end block +func (app *App) EndBlocker(ctx sdktypes.Context, req abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock { + return app.ModuleManager.EndBlock(ctx, req) +} + +// InitChainer application update at chain initialization +func (app *App) InitChainer(ctx sdktypes.Context, req abcitypes.RequestInitChain) abcitypes.ResponseInitChain { + var genesisState GenesisState + if err := tmjson.Unmarshal(req.AppStateBytes, &genesisState); err != nil { + panic(err) + } + app.UpgradeKeeper.SetModuleVersionMap(ctx, app.ModuleManager.GetVersionMap()) + return app.ModuleManager.InitGenesis(ctx, app.cdc, genesisState) +} + +// LoadHeight loads a particular height +func (app *App) LoadHeight(height int64) error { + return app.LoadVersion(height) +} + +// LegacyAmino returns SimApp's amino codec. +// +// NOTE: This is solely to be used for testing purposes as it may be desirable +// for modules to register their own custom testing types. +func (app *App) LegacyAmino() *codec.LegacyAmino { + return app.legacyAmino +} + +// AppCodec returns an app codec. +// +// NOTE: This is solely to be used for testing purposes as it may be desirable +// for modules to register their own custom testing types. +func (app *App) AppCodec() codec.Codec { + return app.cdc +} + +// InterfaceRegistry returns an InterfaceRegistry +func (app *App) InterfaceRegistry() types.InterfaceRegistry { + return app.interfaceRegistry +} + +// TxConfig returns a TxConfig +func (app *App) TxConfig() client.TxConfig { + return app.txConfig +} + +// RegisterAPIRoutes registers all application module routes with the provided +// API server. +func (app *App) RegisterAPIRoutes(apiSvr *api.Server, apiConfig config.APIConfig) { + clientCtx := apiSvr.ClientCtx + // Register new tx routes from grpc-gateway. + authtx.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) + // Register new tendermint queries routes from grpc-gateway. + tmservice.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) + + // Register grpc-gateway routes for all modules. + ModuleBasics.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) +} + +// RegisterTxService implements the Application.RegisterTxService method. +func (app *App) RegisterTxService(clientCtx client.Context) { + authtx.RegisterTxService(app.BaseApp.GRPCQueryRouter(), clientCtx, app.BaseApp.Simulate, app.interfaceRegistry) +} + +// RegisterTendermintService implements the Application.RegisterTendermintService method. +func (app *App) RegisterTendermintService(clientCtx client.Context) { + tmservice.RegisterTendermintService( + clientCtx, + app.BaseApp.GRPCQueryRouter(), + app.interfaceRegistry, + app.Query, + ) +} + +func (app *App) RegisterNodeService(clientCtx client.Context) { + node.RegisterNodeService(clientCtx, app.GRPCQueryRouter()) +} + +// SimulationManager implements the SimulationApp interface +func (app *App) SimulationManager() *module.SimulationManager { + return app.sm +} + +// DefaultGenesis returns a default genesis from the registered AppModuleBasic's. +func (app *App) DefaultGenesis() map[string]json.RawMessage { + return ModuleBasics.DefaultGenesis(app.cdc) +} diff --git a/modules/ibc-hooks/simapp/export.go b/modules/ibc-hooks/simapp/export.go new file mode 100644 index 00000000..8b1ecea1 --- /dev/null +++ b/modules/ibc-hooks/simapp/export.go @@ -0,0 +1,195 @@ +package simapp + +import ( + "encoding/json" + "log" + + servertypes "github.com/cosmos/cosmos-sdk/server/types" + sdk "github.com/cosmos/cosmos-sdk/types" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" +) + +// ExportAppStateAndValidators exports the state of the application for a genesis +// file. +func (app *App) ExportAppStateAndValidators( + forZeroHeight bool, jailAllowedAddrs []string, modulesToExport []string, +) (servertypes.ExportedApp, error) { + // as if they could withdraw from the start of the next block + ctx := app.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}) + + // We export at last height + 1, because that's the height at which + // Tendermint will start InitChain. + height := app.LastBlockHeight() + 1 + if forZeroHeight { + height = 0 + app.prepForZeroHeightGenesis(ctx, jailAllowedAddrs) + } + + genState := app.ModuleManager.ExportGenesisForModules(ctx, app.cdc, modulesToExport) + appState, err := json.MarshalIndent(genState, "", " ") + if err != nil { + return servertypes.ExportedApp{}, err + } + + validators, err := staking.WriteValidators(ctx, app.StakingKeeper) + if err != nil { + return servertypes.ExportedApp{}, err + } + return servertypes.ExportedApp{ + AppState: appState, + Validators: validators, + Height: height, + ConsensusParams: app.BaseApp.GetConsensusParams(ctx), + }, nil +} + +// prepare for fresh start at zero height +// NOTE zero height genesis is a temporary feature which will be deprecated +// in favour of export at a block height +func (app *App) prepForZeroHeightGenesis(ctx sdk.Context, jailAllowedAddrs []string) { + applyAllowedAddrs := false + + // check if there is a allowed address list + if len(jailAllowedAddrs) > 0 { + applyAllowedAddrs = true + } + + allowedAddrsMap := make(map[string]bool) + + for _, addr := range jailAllowedAddrs { + _, err := sdk.ValAddressFromBech32(addr) + if err != nil { + log.Fatal(err) + } + allowedAddrsMap[addr] = true + } + + /* Just to be safe, assert the invariants on current state. */ + app.CrisisKeeper.AssertInvariants(ctx) + + /* Handle fee distribution state. */ + + // withdraw all validator commission + app.StakingKeeper.IterateValidators(ctx, func(_ int64, val stakingtypes.ValidatorI) (stop bool) { + _, err := app.DistrKeeper.WithdrawValidatorCommission(ctx, val.GetOperator()) + if err != nil { + panic(err) + } + return false + }) + + // withdraw all delegator rewards + dels := app.StakingKeeper.GetAllDelegations(ctx) + for _, delegation := range dels { + _, err := app.DistrKeeper.WithdrawDelegationRewards(ctx, delegation.GetDelegatorAddr(), delegation.GetValidatorAddr()) + if err != nil { + panic(err) + } + } + + // clear validator slash events + app.DistrKeeper.DeleteAllValidatorSlashEvents(ctx) + + // clear validator historical rewards + app.DistrKeeper.DeleteAllValidatorHistoricalRewards(ctx) + + // set context height to zero + height := ctx.BlockHeight() + ctx = ctx.WithBlockHeight(0) + + // reinitialize all validators + app.StakingKeeper.IterateValidators(ctx, func(_ int64, val stakingtypes.ValidatorI) (stop bool) { + // donate any unwithdrawn outstanding reward fraction tokens to the community pool + scraps := app.DistrKeeper.GetValidatorOutstandingRewardsCoins(ctx, val.GetOperator()) + feePool := app.DistrKeeper.GetFeePool(ctx) + feePool.CommunityPool = feePool.CommunityPool.Add(scraps...) + app.DistrKeeper.SetFeePool(ctx, feePool) + + err := app.DistrKeeper.Hooks().AfterValidatorCreated(ctx, val.GetOperator()) + if err != nil { + panic(err) + } + return false + }) + + // reinitialize all delegations + for _, del := range dels { + err := app.DistrKeeper.Hooks().BeforeDelegationCreated(ctx, del.GetDelegatorAddr(), del.GetValidatorAddr()) + if err != nil { + panic(err) + } + err = app.DistrKeeper.Hooks().AfterDelegationModified(ctx, del.GetDelegatorAddr(), del.GetValidatorAddr()) + if err != nil { + panic(err) + } + } + + // reset context height + ctx = ctx.WithBlockHeight(height) + + /* Handle staking state. */ + + // iterate through redelegations, reset creation height + app.StakingKeeper.IterateRedelegations(ctx, func(_ int64, red stakingtypes.Redelegation) (stop bool) { + for i := range red.Entries { + red.Entries[i].CreationHeight = 0 + } + app.StakingKeeper.SetRedelegation(ctx, red) + return false + }) + + // iterate through unbonding delegations, reset creation height + app.StakingKeeper.IterateUnbondingDelegations(ctx, func(_ int64, ubd stakingtypes.UnbondingDelegation) (stop bool) { + for i := range ubd.Entries { + ubd.Entries[i].CreationHeight = 0 + } + app.StakingKeeper.SetUnbondingDelegation(ctx, ubd) + return false + }) + + // Iterate through validators by power descending, reset bond heights, and + // update bond intra-tx counters. + store := ctx.KVStore(app.keys[stakingtypes.StoreKey]) + iter := sdk.KVStoreReversePrefixIterator(store, stakingtypes.ValidatorsKey) + counter := int16(0) + + for ; iter.Valid(); iter.Next() { + addr := sdk.ValAddress(iter.Key()[1:]) + validator, found := app.StakingKeeper.GetValidator(ctx, addr) + if !found { + panic("expected validator, not found") + } + + validator.UnbondingHeight = 0 + if applyAllowedAddrs && !allowedAddrsMap[addr.String()] { + validator.Jailed = true + } + + app.StakingKeeper.SetValidator(ctx, validator) + counter++ + } + + if err := iter.Close(); err != nil { + panic(err) + } + + if _, err := app.StakingKeeper.ApplyAndReturnValidatorSetUpdates(ctx); err != nil { + panic(err) + } + + /* Handle slashing state. */ + + // reset start height on signing infos + app.SlashingKeeper.IterateValidatorSigningInfos( + ctx, + func(addr sdk.ConsAddress, info slashingtypes.ValidatorSigningInfo) (stop bool) { + info.StartHeight = 0 + app.SlashingKeeper.SetValidatorSigningInfo(ctx, addr, info) + return false + }, + ) +} diff --git a/modules/ibc-hooks/simapp/simulation_test.go b/modules/ibc-hooks/simapp/simulation_test.go new file mode 100644 index 00000000..690f230f --- /dev/null +++ b/modules/ibc-hooks/simapp/simulation_test.go @@ -0,0 +1,86 @@ +package simapp + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/baseapp" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + simcli "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" +) + +// Hardcoded chainID for simulation. +const ( + simulationAppChainID = "simulation-app" + simulationDirPrefix = "leveldb-app-sim" + simulationDbName = "Simulation" +) + +func init() { + simcli.GetSimulatorFlags() +} + +// Running as a go test: +// +// go test -v -run=TestFullAppSimulation ./app -NumBlocks 200 -BlockSize 50 -Commit -Enabled -Period 1 -Seed 40 +func TestFullAppSimulation(t *testing.T) { + config := simcli.NewConfigFromFlags() + config.ChainID = simulationAppChainID + + if !simcli.FlagEnabledValue { + t.Skip("skipping application simulation") + } + + db, dir, logger, _, err := simtestutil.SetupSimulation( + config, + simulationDirPrefix, + simulationDbName, + simcli.FlagVerboseValue, + true, // Don't use this as it is confusing + ) + require.NoError(t, err, "simulation setup failed") + + defer func() { + require.NoError(t, db.Close()) + require.NoError(t, os.RemoveAll(dir)) + }() + + app := NewSimApp(logger, + db, + nil, + true, + map[int64]bool{}, + DefaultNodeHome, + simcli.FlagPeriodValue, + MakeEncodingConfig(), + simtestutil.EmptyAppOptions{}, + baseapp.SetChainID(simulationAppChainID), + ) + require.Equal(t, AppName, app.Name()) + + // run randomized simulation + _, simParams, simErr := simulation.SimulateFromSeed( + t, + os.Stdout, + app.BaseApp, + simtestutil.AppStateFn(app.AppCodec(), app.SimulationManager(), app.DefaultGenesis()), + simtypes.RandomAccounts, + simtestutil.SimulationOperations(app, app.AppCodec(), config), + app.BankKeeper.GetBlockedAddresses(), + config, + app.AppCodec(), + ) + + // export state and simParams before the simulatino error is checked + err = simtestutil.CheckExportSimulation(app, config, simParams) + require.NoError(t, err) + require.NoError(t, simErr) + + if config.Commit { + simtestutil.PrintStats(db) + } +} diff --git a/modules/ibc-hooks/simapp/test_helpers.go b/modules/ibc-hooks/simapp/test_helpers.go new file mode 100644 index 00000000..91872911 --- /dev/null +++ b/modules/ibc-hooks/simapp/test_helpers.go @@ -0,0 +1,465 @@ +package simapp + +import ( + "encoding/json" + "fmt" + "math/rand" + "os" + "path/filepath" + "testing" + "time" + + "github.com/CosmWasm/wasmd/x/wasm" + "github.com/stretchr/testify/require" + + "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/server" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + pruningtypes "github.com/cosmos/cosmos-sdk/store/pruning/types" + "github.com/cosmos/cosmos-sdk/testutil/mock" + "github.com/cosmos/cosmos-sdk/testutil/network" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module/testutil" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + simcli "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + dbm "github.com/cometbft/cometbft-db" + abci "github.com/cometbft/cometbft/abci/types" + tmjson "github.com/cometbft/cometbft/libs/json" + "github.com/cometbft/cometbft/libs/log" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + tmtypes "github.com/cometbft/cometbft/types" +) + +// SetupOptions defines arguments that are passed into `WasmApp` constructor. +type SetupOptions struct { + Logger log.Logger + DB *dbm.MemDB + AppOpts servertypes.AppOptions + WasmOpts []wasm.Option +} + +// NewTestNetworkFixture returns a new simapp AppConstructor for network simulation tests +func NewTestNetworkFixture() network.TestFixture { + dir, err := os.MkdirTemp("", "simapp") + if err != nil { + panic(fmt.Sprintf("failed creating temporary directory: %v", err)) + } + defer os.RemoveAll(dir) + + app := NewSimApp(log.NewNopLogger(), + dbm.NewMemDB(), + nil, + true, + map[int64]bool{}, + "", + 0, + MakeEncodingConfig(), + simtestutil.NewAppOptionsWithFlagHome(dir)) + + appCtr := func(val network.ValidatorI) servertypes.Application { + return NewSimApp( + val.GetCtx().Logger, + dbm.NewMemDB(), + nil, + true, + map[int64]bool{}, + "", + 0, + MakeEncodingConfig(), + simtestutil.NewAppOptionsWithFlagHome(val.GetCtx().Config.RootDir), + baseapp.SetPruning(pruningtypes.NewPruningOptionsFromString(val.GetAppConfig().Pruning)), + baseapp.SetMinGasPrices(val.GetAppConfig().MinGasPrices), + baseapp.SetChainID(val.GetCtx().Viper.GetString(flags.FlagChainID)), + ) + } + + return network.TestFixture{ + AppConstructor: appCtr, + GenesisState: app.DefaultGenesis(), + EncodingConfig: testutil.TestEncodingConfig{ + InterfaceRegistry: app.InterfaceRegistry(), + Codec: app.AppCodec(), + TxConfig: app.TxConfig(), + Amino: app.LegacyAmino(), + }, + } +} + +func setup(tb testing.TB, chainID string, withGenesis bool, invCheckPeriod uint, opts ...wasm.Option) (*App, GenesisState) { + tb.Helper() + db := dbm.NewMemDB() + nodeHome := tb.TempDir() + snapshotDir := filepath.Join(nodeHome, "data", "snapshots") + + snapshotDB, err := dbm.NewDB("metadata", dbm.GoLevelDBBackend, snapshotDir) + require.NoError(tb, err) + tb.Cleanup(func() { + err := snapshotDB.Close() + require.NoError(tb, err) + }) + + appOptions := make(simtestutil.AppOptionsMap, 0) + appOptions[flags.FlagHome] = nodeHome // ensure unique folder + appOptions[server.FlagInvCheckPeriod] = invCheckPeriod + app := NewSimApp(log.NewNopLogger(), + db, + nil, + true, + map[int64]bool{}, + DefaultNodeHome, + simcli.FlagPeriodValue, + MakeEncodingConfig(), + appOptions, + baseapp.SetChainID("testing"), + ) + + if withGenesis { + return app, NewDefaultGenesisState(app.AppCodec()) + } + return app, GenesisState{} +} + +// NewAppWithCustomOptions initializes a new WasmApp with custom options. +func NewAppWithCustomOptions(t *testing.T, isCheckTx bool, options SetupOptions) *App { + t.Helper() + db := dbm.NewMemDB() + + privVal := mock.NewPV() + pubKey, err := privVal.GetPubKey() + require.NoError(t, err) + // create validator set with single validator + validator := tmtypes.NewValidator(pubKey, 1) + valSet := tmtypes.NewValidatorSet([]*tmtypes.Validator{validator}) + + // generate genesis account + senderPrivKey := secp256k1.GenPrivKey() + acc := authtypes.NewBaseAccount(senderPrivKey.PubKey().Address().Bytes(), senderPrivKey.PubKey(), 0, 0) + balance := banktypes.Balance{ + Address: acc.GetAddress().String(), + Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100000000000000))), + } + + app := NewSimApp(log.NewNopLogger(), + db, + nil, + true, + map[int64]bool{}, + DefaultNodeHome, + simcli.FlagPeriodValue, + MakeEncodingConfig(), + options.AppOpts, + baseapp.SetChainID("testing"), + ) + genesisState := NewDefaultGenesisState(app.AppCodec()) + genesisState, err = GenesisStateWithValSet(app.AppCodec(), genesisState, valSet, []authtypes.GenesisAccount{acc}, balance) + require.NoError(t, err) + + if !isCheckTx { + // init chain must be called to stop deliverState from being nil + stateBytes, err := tmjson.MarshalIndent(genesisState, "", " ") + require.NoError(t, err) + + // Initialize the chain + app.InitChain( + abci.RequestInitChain{ + Validators: []abci.ValidatorUpdate{}, + ConsensusParams: simtestutil.DefaultConsensusParams, + AppStateBytes: stateBytes, + }, + ) + } + + return app +} + +// Setup initializes a new WasmApp. A Nop logger is set in WasmApp. +func Setup(t *testing.T, opts ...wasm.Option) (*App, sdk.Context, *authtypes.BaseAccount) { + t.Helper() + + privVal := mock.NewPV() + pubKey, err := privVal.GetPubKey() + require.NoError(t, err) + + // create validator set with single validator + validator := tmtypes.NewValidator(pubKey, 1) + valSet := tmtypes.NewValidatorSet([]*tmtypes.Validator{validator}) + + // generate genesis account + accPrivKey := secp256k1.GenPrivKey() + acc := authtypes.NewBaseAccount(accPrivKey.PubKey().Address().Bytes(), accPrivKey.PubKey(), 1, 1) + balance := banktypes.Balance{ + Address: acc.GetAddress().String(), + Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100000000000000))), + } + chainID := "testing" + app := SetupWithGenesisValSet(t, valSet, []authtypes.GenesisAccount{acc}, chainID, opts, balance) + + ctx := app.BaseApp.NewContext(false, tmproto.Header{ + Height: app.LastBlockHeight(), + ChainID: chainID, + Time: time.Now().UTC(), + }) + + // Mint coins and send to the acc + err = app.BankKeeper.MintCoins(ctx, + minttypes.ModuleName, + sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, + sdk.NewInt(100000000000000))), + ) + require.NoError(t, err) + err = app.BankKeeper.SendCoinsFromModuleToAccount(ctx, + minttypes.ModuleName, + acc.GetAddress(), + sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100000000000000))), + ) + require.NoError(t, err) + + return app, ctx, acc +} + +// SetupWithGenesisValSet initializes a new WasmApp with a validator set and genesis accounts +// that also act as delegators. For simplicity, each validator is bonded with a delegation +// of one consensus engine unit in the default token of the WasmApp from first genesis +// account. A Nop logger is set in WasmApp. +func SetupWithGenesisValSet(t *testing.T, valSet *tmtypes.ValidatorSet, genAccs []authtypes.GenesisAccount, chainID string, opts []wasm.Option, balances ...banktypes.Balance) *App { + t.Helper() + + app, genesisState := setup(t, chainID, true, 5, opts...) + genesisState, err := GenesisStateWithValSet(app.AppCodec(), genesisState, valSet, genAccs, balances...) + require.NoError(t, err) + + stateBytes, err := json.MarshalIndent(genesisState, "", " ") + require.NoError(t, err) + + // init chain will set the validator set and initialize the genesis accounts + consensusParams := simtestutil.DefaultConsensusParams + consensusParams.Block.MaxGas = 100 * simtestutil.DefaultGenTxGas + app.InitChain( + abci.RequestInitChain{ + ChainId: chainID, + Validators: []abci.ValidatorUpdate{}, + ConsensusParams: consensusParams, + AppStateBytes: stateBytes, + }, + ) + // commit genesis changes + app.Commit() + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{ + ChainID: chainID, + Height: app.LastBlockHeight() + 1, + AppHash: app.LastCommitID().Hash, + Time: time.Now().UTC(), + ValidatorsHash: valSet.Hash(), + NextValidatorsHash: valSet.Hash(), + }}) + + return app +} + +// SetupWithEmptyStore set up a wasmd app instance with empty DB +func SetupWithEmptyStore(tb testing.TB) *App { + tb.Helper() + app, _ := setup(tb, "testing", false, 0) + return app +} + +// GenesisStateWithSingleValidator initializes GenesisState with a single validator and genesis accounts +// that also act as delegators. +func GenesisStateWithSingleValidator(t *testing.T, app *App) GenesisState { + t.Helper() + + privVal := mock.NewPV() + pubKey, err := privVal.GetPubKey() + require.NoError(t, err) + + // create validator set with single validator + validator := tmtypes.NewValidator(pubKey, 1) + valSet := tmtypes.NewValidatorSet([]*tmtypes.Validator{validator}) + + // generate genesis account + senderPrivKey := secp256k1.GenPrivKey() + acc := authtypes.NewBaseAccount(senderPrivKey.PubKey().Address().Bytes(), senderPrivKey.PubKey(), 0, 0) + balances := []banktypes.Balance{ + { + Address: acc.GetAddress().String(), + Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100000000000000))), + }, + } + + genesisState := NewDefaultGenesisState(app.AppCodec()) + genesisState, err = GenesisStateWithValSet(app.AppCodec(), genesisState, valSet, []authtypes.GenesisAccount{acc}, balances...) + require.NoError(t, err) + + return genesisState +} + +// AddTestAddrsIncremental constructs and returns accNum amount of accounts with an +// initial balance of accAmt in random order +func AddTestAddrsIncremental(app *App, ctx sdk.Context, accNum int, accAmt math.Int) []sdk.AccAddress { + return addTestAddrs(app, ctx, accNum, accAmt, simtestutil.CreateIncrementalAccounts) +} + +func addTestAddrs(app *App, ctx sdk.Context, accNum int, accAmt math.Int, strategy simtestutil.GenerateAccountStrategy) []sdk.AccAddress { + testAddrs := strategy(accNum) + + initCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), accAmt)) + + for _, addr := range testAddrs { + initAccountWithCoins(app, ctx, addr, initCoins) + } + + return testAddrs +} + +func initAccountWithCoins(app *App, ctx sdk.Context, addr sdk.AccAddress, coins sdk.Coins) { + err := app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, coins) + if err != nil { + panic(err) + } + + err = app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, addr, coins) + if err != nil { + panic(err) + } +} + +// SignAndDeliverWithoutCommit signs and delivers a transaction. No commit +func SignAndDeliverWithoutCommit( + t *testing.T, txCfg client.TxConfig, app *baseapp.BaseApp, header tmproto.Header, msgs []sdk.Msg, + chainID string, accNums, accSeqs []uint64, priv ...cryptotypes.PrivKey, +) (sdk.GasInfo, *sdk.Result, error) { + t.Helper() + tx, err := simtestutil.GenSignedMockTx( + /* #nosec */ + rand.New(rand.NewSource(time.Now().UnixNano())), + txCfg, + msgs, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, + simtestutil.DefaultGenTxGas, + chainID, + accNums, + accSeqs, + priv..., + ) + require.NoError(t, err) + + // Simulate a sending a transaction and committing a block + // app.BeginBlock(abci.RequestBeginBlock{Header: header}) + gInfo, res, err := app.SimDeliver(txCfg.TxEncoder(), tx) + // app.EndBlock(abci.RequestEndBlock{}) + // app.Commit() + + return gInfo, res, err +} + +// GenesisStateWithValSet returns a new genesis state with the validator set +// copied from simtestutil with delegation not added to supply +func GenesisStateWithValSet( + codec codec.Codec, + genesisState map[string]json.RawMessage, + valSet *tmtypes.ValidatorSet, + genAccs []authtypes.GenesisAccount, + balances ...banktypes.Balance, +) (map[string]json.RawMessage, error) { + // set genesis accounts + authGenesis := authtypes.NewGenesisState(authtypes.DefaultParams(), genAccs) + genesisState[authtypes.ModuleName] = codec.MustMarshalJSON(authGenesis) + + validators := make([]stakingtypes.Validator, 0, len(valSet.Validators)) + delegations := make([]stakingtypes.Delegation, 0, len(valSet.Validators)) + + bondAmt := sdk.DefaultPowerReduction + + for _, val := range valSet.Validators { + pk, err := cryptocodec.FromTmPubKeyInterface(val.PubKey) + if err != nil { + return nil, fmt.Errorf("failed to convert pubkey: %w", err) + } + + pkAny, err := codectypes.NewAnyWithValue(pk) + if err != nil { + return nil, fmt.Errorf("failed to create new any: %w", err) + } + + validator := stakingtypes.Validator{ + OperatorAddress: sdk.ValAddress(val.Address).String(), + ConsensusPubkey: pkAny, + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: bondAmt, + DelegatorShares: math.LegacyOneDec(), + Description: stakingtypes.Description{}, + UnbondingHeight: int64(0), + UnbondingTime: time.Unix(0, 0).UTC(), + Commission: stakingtypes.NewCommission(math.LegacyZeroDec(), math.LegacyZeroDec(), math.LegacyZeroDec()), + MinSelfDelegation: math.ZeroInt(), + } + validators = append(validators, validator) + delegations = append(delegations, stakingtypes.NewDelegation(genAccs[0].GetAddress(), val.Address.Bytes(), math.LegacyOneDec())) + } + + // set validators and delegations + stakingGenesis := stakingtypes.NewGenesisState(stakingtypes.DefaultParams(), validators, delegations) + genesisState[stakingtypes.ModuleName] = codec.MustMarshalJSON(stakingGenesis) + + // add bonded amount to bonded pool module account + balances = append(balances, banktypes.Balance{ + Address: authtypes.NewModuleAddress(stakingtypes.BondedPoolName).String(), + Coins: sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, bondAmt.MulRaw(int64(len(valSet.Validators))))}, + }) + + totalSupply := sdk.NewCoins() + for _, b := range balances { + // add genesis acc tokens to total supply + totalSupply = totalSupply.Add(b.Coins...) + } + + // update total supply + bankGenesis := banktypes.NewGenesisState(banktypes.DefaultGenesisState().Params, balances, totalSupply, []banktypes.Metadata{}, []banktypes.SendEnabled{}) + genesisState[banktypes.ModuleName] = codec.MustMarshalJSON(bankGenesis) + println(string(genesisState[banktypes.ModuleName])) + return genesisState, nil +} + +// FundAccount is a utility function that funds an account by minting and +// sending the coins to the address. This should be used for testing purposes +// only! +// +// Instead of using the mint module account, which has the +// permission of minting, create a "faucet" account. (@fdymylja) +func FundAccount(bankKeeper bankkeeper.Keeper, ctx sdk.Context, addr sdk.AccAddress, amounts sdk.Coins) error { + if err := bankKeeper.MintCoins(ctx, minttypes.ModuleName, amounts); err != nil { + return err + } + + return bankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, addr, amounts) +} + +// FundModuleAccount is a utility function that funds a module account by +// minting and sending the coins to the address. This should be used for testing +// purposes only! +// +// Instead of using the mint module account, which has the +// permission of minting, create a "faucet" account. (@fdymylja) +func FundModuleAccount(bankKeeper bankkeeper.Keeper, ctx sdk.Context, recipientMod string, amounts sdk.Coins) error { + if err := bankKeeper.MintCoins(ctx, minttypes.ModuleName, amounts); err != nil { + return err + } + + return bankKeeper.SendCoinsFromModuleToModule(ctx, minttypes.ModuleName, recipientMod, amounts) +} diff --git a/modules/ibc-hooks/tests/unit/mocks/isc4_middleware_mock.go b/modules/ibc-hooks/tests/unit/mocks/isc4_middleware_mock.go new file mode 100644 index 00000000..c357554a --- /dev/null +++ b/modules/ibc-hooks/tests/unit/mocks/isc4_middleware_mock.go @@ -0,0 +1,43 @@ +package mocks + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + + ibcclienttypes "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/cosmos/ibc-go/v7/modules/core/exported" +) + +var _ porttypes.ICS4Wrapper = &ICS4WrapperMock{} + +type ICS4WrapperMock struct{} + +func (m *ICS4WrapperMock) SendPacket( + ctx sdk.Context, + chanCap *capabilitytypes.Capability, + sourcePort string, + sourceChannel string, + timeoutHeight ibcclienttypes.Height, + timeoutTimestamp uint64, + data []byte, +) (sequence uint64, err error) { + return 1, nil +} + +func (m *ICS4WrapperMock) WriteAcknowledgement( + ctx sdk.Context, + chanCap *capabilitytypes.Capability, + packet exported.PacketI, + ack exported.Acknowledgement, +) error { + return nil +} + +func (m *ICS4WrapperMock) GetAppVersion( + ctx sdk.Context, + portID, + channelID string, +) (string, bool) { + return "", false +} diff --git a/modules/ibc-hooks/tests/unit/module_test.go b/modules/ibc-hooks/tests/unit/module_test.go new file mode 100644 index 00000000..db72aab0 --- /dev/null +++ b/modules/ibc-hooks/tests/unit/module_test.go @@ -0,0 +1,401 @@ +package tests_unit + +import ( + "encoding/json" + "fmt" + "testing" + + ibc_hooks "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7" + ibchookskeeper "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7/keeper" + "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7/simapp" + "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7/tests/unit/mocks" + "github.com/stretchr/testify/suite" + + _ "embed" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + + ibctransfer "github.com/cosmos/ibc-go/v7/modules/apps/transfer" + transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + ibcmock "github.com/cosmos/ibc-go/v7/testing/mock" +) + +//go:embed testdata/counter/artifacts/counter.wasm +var counterWasm []byte + +//go:embed testdata/echo/artifacts/echo.wasm +var echoWasm []byte + +type HooksTestSuite struct { + suite.Suite + + App *simapp.App + Ctx sdk.Context + EchoContractAddr sdk.AccAddress + CounterContractAddr sdk.AccAddress + TestAddress *types.BaseAccount +} + +func TestIBCHooksTestSuite(t *testing.T) { + suite.Run(t, new(HooksTestSuite)) +} + +func (suite *HooksTestSuite) SetupEnv() { + // Setup the environment + app, ctx, acc := simapp.Setup(suite.T()) + + // create the echo contract + contractID, _, err := app.ContractKeeper.Create(ctx, acc.GetAddress(), counterWasm, nil) + suite.NoError(err) + counterContractAddr, _, err := app.ContractKeeper.Instantiate( + ctx, + contractID, + acc.GetAddress(), + nil, + []byte(`{"count": 0}`), + "counter contract", + nil, + ) + suite.NoError(err) + suite.Equal("cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmalr", counterContractAddr.String()) + + // create the counter contract + contractID, _, err = app.ContractKeeper.Create(ctx, acc.GetAddress(), echoWasm, nil) + suite.NoError(err) + echoContractAddr, _, err := app.ContractKeeper.Instantiate( + ctx, + contractID, + acc.GetAddress(), + nil, + []byte(`{}`), + "echo contract", + nil, + ) + suite.NoError(err) + suite.Equal("cosmos1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrqez7la9", echoContractAddr.String()) + + suite.App = app + suite.Ctx = ctx + suite.EchoContractAddr = echoContractAddr + suite.CounterContractAddr = counterContractAddr + suite.TestAddress = acc +} + +func (suite *HooksTestSuite) TestOnRecvPacketEcho() { + // create en env + suite.SetupEnv() + + // Create the packet + recvPacket := channeltypes.Packet{ + Data: transfertypes.FungibleTokenPacketData{ + Denom: "transfer/channel-0/stake", + Amount: "1", + Sender: suite.TestAddress.GetAddress().String(), + Receiver: suite.EchoContractAddr.String(), + Memo: fmt.Sprintf(`{"wasm":{"contract": "%s", "msg":{"echo":{"msg":"test"}}}}`, suite.EchoContractAddr.String()), + }.GetBytes(), + SourcePort: "transfer", + SourceChannel: "channel-0", + } + + // send funds to the escrow address to simulate a transfer from the ibc module + escrowAddress := transfertypes.GetEscrowAddress(recvPacket.GetDestPort(), recvPacket.GetDestChannel()) + err := suite.App.BankKeeper.SendCoins(suite.Ctx, suite.TestAddress.GetAddress(), escrowAddress, sdk.NewCoins(sdk.NewInt64Coin("stake", 2))) + suite.NoError(err) + + // create the wasm hooks + wasmHooks := ibc_hooks.NewWasmHooks( + &suite.App.IBCHooksKeeper, + &suite.App.WasmKeeper, + "cosmos", + ) + + // create the ics4 middleware + ics4Middleware := ibc_hooks.NewICS4Middleware( + suite.App.IBCKeeper.ChannelKeeper, + wasmHooks, + ) + + // create the ibc middleware + transferIBCModule := ibctransfer.NewIBCModule(suite.App.TransferKeeper) + ibcmiddleware := ibc_hooks.NewIBCMiddleware( + transferIBCModule, + &ics4Middleware, + ) + + // call the hook twice + res := ibcmiddleware.OnRecvPacket( + suite.Ctx, + recvPacket, + suite.TestAddress.GetAddress(), + ) + suite.True(res.Success()) + var ack map[string]string // This can't be unmarshalled to Acknowledgement because it's fetched from the events + err = json.Unmarshal(res.Acknowledgement(), &ack) + suite.Require().NoError(err) + suite.Require().NotContains(ack, "error") + suite.Require().Equal(ack["result"], "eyJjb250cmFjdF9yZXN1bHQiOiJkR2hwY3lCemFHOTFiR1FnWldOb2J3PT0iLCJpYmNfYWNrIjoiZXlKeVpYTjFiSFFpT2lKQlVUMDlJbjA9In0=") +} + +func (suite *HooksTestSuite) TestOnRecvPacketCounterContract() { + // create en env + suite.SetupEnv() + + // Create the packet + recvPacket := channeltypes.Packet{ + Data: transfertypes.FungibleTokenPacketData{ + Denom: "transfer/channel-0/stake", + Amount: "1", + Sender: suite.TestAddress.GetAddress().String(), + Receiver: suite.CounterContractAddr.String(), + Memo: fmt.Sprintf(`{"wasm":{"contract": "%s", "msg":{"increment":{}}}}`, suite.CounterContractAddr.String()), + }.GetBytes(), + SourcePort: "transfer", + SourceChannel: "channel-0", + } + + // send funds to the escrow address to simulate a transfer from the ibc module + escrowAddress := transfertypes.GetEscrowAddress(recvPacket.GetDestPort(), recvPacket.GetDestChannel()) + err := suite.App.BankKeeper.SendCoins(suite.Ctx, suite.TestAddress.GetAddress(), escrowAddress, sdk.NewCoins(sdk.NewInt64Coin("stake", 2))) + suite.NoError(err) + + // create the wasm hooks + wasmHooks := ibc_hooks.NewWasmHooks( + &suite.App.IBCHooksKeeper, + &suite.App.WasmKeeper, + "cosmos", + ) + + // create the ics4 middleware + ics4Middleware := ibc_hooks.NewICS4Middleware( + suite.App.IBCKeeper.ChannelKeeper, + wasmHooks, + ) + + // create the ibc middleware + transferIBCModule := ibctransfer.NewIBCModule(suite.App.TransferKeeper) + ibcmiddleware := ibc_hooks.NewIBCMiddleware( + transferIBCModule, + &ics4Middleware, + ) + + // call the hook twice + res := ibcmiddleware.OnRecvPacket( + suite.Ctx, + recvPacket, + suite.TestAddress.GetAddress(), + ) + suite.True(res.Success()) + res = ibcmiddleware.OnRecvPacket( + suite.Ctx, + recvPacket, + suite.TestAddress.GetAddress(), + ) + suite.True(res.Success()) + + // get the derived account to check the count + senderBech32, err := ibchookskeeper.DeriveIntermediateSender( + recvPacket.GetDestChannel(), + suite.TestAddress.GetAddress().String(), + "cosmos", + ) + suite.NoError(err) + // query the smart contract to assert the count + count, err := suite.App.WasmKeeper.QuerySmart( + suite.Ctx, + suite.CounterContractAddr, + []byte(fmt.Sprintf(`{"get_count":{"addr": "%s"}}`, senderBech32)), + ) + suite.NoError(err) + suite.Equal(`{"count":1}`, string(count)) +} + +func (suite *HooksTestSuite) TestOnAcknowledgementPacketCounterContract() { + suite.SetupEnv() + + callbackPacket := channeltypes.Packet{ + Data: transfertypes.FungibleTokenPacketData{ + Denom: "transfer/channel-0/stake", + Amount: "1", + Sender: suite.TestAddress.GetAddress().String(), + Receiver: suite.CounterContractAddr.String(), + Memo: fmt.Sprintf(`{"ibc_callback": "%s"}`, suite.CounterContractAddr), + }.GetBytes(), + Sequence: 1, + SourcePort: "transfer", + SourceChannel: "channel-0", + } + + // send funds to the escrow address to simulate a transfer from the ibc module + escrowAddress := transfertypes.GetEscrowAddress(callbackPacket.GetDestPort(), callbackPacket.GetDestChannel()) + err := suite.App.BankKeeper.SendCoins(suite.Ctx, suite.TestAddress.GetAddress(), escrowAddress, sdk.NewCoins(sdk.NewInt64Coin("stake", 2))) + suite.NoError(err) + + // create the wasm hooks + wasmHooks := ibc_hooks.NewWasmHooks( + &suite.App.IBCHooksKeeper, + &suite.App.WasmKeeper, + "cosmos", + ) + + // create the ics4 middleware + ics4Middleware := ibc_hooks.NewICS4Middleware( + &mocks.ICS4WrapperMock{}, + wasmHooks, + ) + + // create the ibc middleware + transferIBCModule := ibctransfer.NewIBCModule(suite.App.TransferKeeper) + ibcmiddleware := ibc_hooks.NewIBCMiddleware( + transferIBCModule, + &ics4Middleware, + ) + + // call the hook + seq, err := ibcmiddleware.SendPacket( + suite.Ctx, + &capabilitytypes.Capability{Index: 1}, + callbackPacket.SourcePort, + callbackPacket.SourceChannel, + ibcclienttypes.Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + 1, + callbackPacket.Data, + ) + + // require to be the first sequence + suite.Equal(uint64(1), seq) + // assert the request was successful + suite.NoError(err) + + // Create the packet + recvPacket := channeltypes.Packet{ + Data: transfertypes.FungibleTokenPacketData{ + Denom: "transfer/channel-0/stake", + Amount: "1", + Sender: suite.TestAddress.GetAddress().String(), + Receiver: suite.CounterContractAddr.String(), + Memo: fmt.Sprintf(`{"wasm":{"contract": "%s", "msg":{"increment":{}}}}`, suite.CounterContractAddr.String()), + }.GetBytes(), + Sequence: 1, + SourcePort: "transfer", + SourceChannel: "channel-0", + } + suite.NoError(err) + err = wasmHooks.OnAcknowledgementPacketOverride( + ibcmiddleware, + suite.Ctx, + recvPacket, + ibcmock.MockAcknowledgement.Acknowledgement(), + suite.TestAddress.GetAddress(), + ) + // assert the request was successful + suite.NoError(err) + + // query the smart contract to assert the count + count, err := suite.App.WasmKeeper.QuerySmart( + suite.Ctx, + suite.CounterContractAddr, + []byte(fmt.Sprintf(`{"get_count":{"addr": %q}}`, suite.CounterContractAddr.String())), + ) + suite.NoError(err) + suite.Equal(`{"count":1}`, string(count)) +} + +func (suite *HooksTestSuite) TestOnTimeoutPacketOverrideCounterContract() { + suite.SetupEnv() + callbackPacket := channeltypes.Packet{ + Data: transfertypes.FungibleTokenPacketData{ + Denom: "transfer/channel-0/stake", + Amount: "1", + Sender: suite.TestAddress.GetAddress().String(), + Receiver: suite.CounterContractAddr.String(), + Memo: fmt.Sprintf(`{"ibc_callback": "%s"}`, suite.CounterContractAddr), + }.GetBytes(), + Sequence: 1, + SourcePort: "transfer", + SourceChannel: "channel-0", + } + + // send funds to the escrow address to simulate a transfer from the ibc module + escrowAddress := transfertypes.GetEscrowAddress(callbackPacket.GetDestPort(), callbackPacket.GetDestChannel()) + err := suite.App.BankKeeper.SendCoins(suite.Ctx, suite.TestAddress.GetAddress(), escrowAddress, sdk.NewCoins(sdk.NewInt64Coin("stake", 2))) + suite.NoError(err) + + // create the wasm hooks + wasmHooks := ibc_hooks.NewWasmHooks( + &suite.App.IBCHooksKeeper, + &suite.App.WasmKeeper, + "cosmos", + ) + + // create the ics4 middleware + ics4Middleware := ibc_hooks.NewICS4Middleware( + &mocks.ICS4WrapperMock{}, + wasmHooks, + ) + + // create the ibc middleware + transferIBCModule := ibctransfer.NewIBCModule(suite.App.TransferKeeper) + ibcmiddleware := ibc_hooks.NewIBCMiddleware( + transferIBCModule, + &ics4Middleware, + ) + + // call the hook + seq, err := ibcmiddleware.SendPacket( + suite.Ctx, + &capabilitytypes.Capability{Index: 1}, + callbackPacket.SourcePort, + callbackPacket.SourceChannel, + ibcclienttypes.Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + 1, + callbackPacket.Data, + ) + + // require to be the first sequence + suite.Equal(uint64(1), seq) + // assert the request was successful + suite.NoError(err) + + // Create the packet + recvPacket := channeltypes.Packet{ + Data: transfertypes.FungibleTokenPacketData{ + Denom: "transfer/channel-0/stake", + Amount: "1", + Sender: suite.TestAddress.GetAddress().String(), + Receiver: suite.CounterContractAddr.String(), + Memo: fmt.Sprintf(`{"wasm":{"contract": "%s", "msg":{"increment":{}}}}`, suite.CounterContractAddr.String()), + }.GetBytes(), + Sequence: 1, + SourcePort: "transfer", + SourceChannel: "channel-0", + } + suite.NoError(err) + err = wasmHooks.OnTimeoutPacketOverride( + ibcmiddleware, + suite.Ctx, + recvPacket, + suite.TestAddress.GetAddress(), + ) + // assert the request was successful + suite.NoError(err) + + // query the smart contract to assert the count + count, err := suite.App.WasmKeeper.QuerySmart( + suite.Ctx, + suite.CounterContractAddr, + []byte(fmt.Sprintf(`{"get_count":{"addr": %q}}`, suite.CounterContractAddr.String())), + ) + suite.NoError(err) + suite.Equal(`{"count":10}`, string(count)) +} diff --git a/modules/ibc-hooks/wasm_hook.go b/modules/ibc-hooks/wasm_hook.go new file mode 100644 index 00000000..0aef149b --- /dev/null +++ b/modules/ibc-hooks/wasm_hook.go @@ -0,0 +1,433 @@ +package ibc_hooks + +import ( + "encoding/json" + "fmt" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7/keeper" + "github.com/cosmos/ibc-apps/modules/ibc-hooks/v7/types" + + errors "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + + transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" +) + +type ContractAck struct { + ContractResult []byte `json:"contract_result"` + IbcAck []byte `json:"ibc_ack"` +} + +type WasmHooks struct { + ContractKeeper *wasmkeeper.Keeper + ibcHooksKeeper *keeper.Keeper + bech32PrefixAccAddr string +} + +func NewWasmHooks(ibcHooksKeeper *keeper.Keeper, contractKeeper *wasmkeeper.Keeper, bech32PrefixAccAddr string) WasmHooks { + return WasmHooks{ + ContractKeeper: contractKeeper, + ibcHooksKeeper: ibcHooksKeeper, + bech32PrefixAccAddr: bech32PrefixAccAddr, + } +} + +func (h WasmHooks) ProperlyConfigured() bool { + return h.ContractKeeper != nil && h.ibcHooksKeeper != nil +} + +func (h WasmHooks) OnRecvPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement { + if !h.ProperlyConfigured() { + // Not configured + return im.App.OnRecvPacket(ctx, packet, relayer) + } + isIcs20, data := isIcs20Packet(packet.GetData()) + if !isIcs20 { + return im.App.OnRecvPacket(ctx, packet, relayer) + } + + // Validate the memo + isWasmRouted, contractAddr, msgBytes, err := ValidateAndParseMemo(data.GetMemo(), data.Receiver) + if !isWasmRouted { + return im.App.OnRecvPacket(ctx, packet, relayer) + } + if err != nil { + return NewEmitErrorAcknowledgement(ctx, types.ErrMsgValidation, err.Error()) + } + if msgBytes == nil || contractAddr == nil { // This should never happen + return NewEmitErrorAcknowledgement(ctx, types.ErrMsgValidation) + } + + // Calculate the receiver / contract caller based on the packet's channel and sender + channel := packet.GetDestChannel() + sender := data.GetSender() + senderBech32, err := keeper.DeriveIntermediateSender(channel, sender, h.bech32PrefixAccAddr) + if err != nil { + return NewEmitErrorAcknowledgement(ctx, types.ErrBadSender, fmt.Sprintf("cannot convert sender address %s/%s to bech32: %s", channel, sender, err.Error())) + } + + // The funds sent on this packet need to be transferred to the intermediary account for the sender. + // For this, we override the ICS20 packet's Receiver (essentially hijacking the funds to this new address) + // and execute the underlying OnRecvPacket() call (which should eventually land on the transfer app's + // relay.go and send the sunds to the intermediary account. + // + // If that succeeds, we make the contract call + data.Receiver = senderBech32 + bz, err := json.Marshal(data) + if err != nil { + return NewEmitErrorAcknowledgement(ctx, types.ErrMarshaling, err.Error()) + } + packet.Data = bz + + // Execute the receive + ack := im.App.OnRecvPacket(ctx, packet, relayer) + if !ack.Success() { + return ack + } + + amount, ok := sdk.NewIntFromString(data.GetAmount()) + if !ok { + // This should never happen, as it should've been caught in the underlaying call to OnRecvPacket, + // but returning here for completeness + return NewEmitErrorAcknowledgement(ctx, types.ErrInvalidPacket, "Amount is not an int") + } + + // The packet's denom is the denom in the sender chain. This needs to be converted to the local denom. + denom := MustExtractDenomFromPacketOnRecv(packet) + funds := sdk.NewCoins(sdk.NewCoin(denom, amount)) + + // Execute the contract + execMsg := wasmtypes.MsgExecuteContract{ + Sender: senderBech32, + Contract: contractAddr.String(), + Msg: msgBytes, + Funds: funds, + } + response, err := h.execWasmMsg(ctx, &execMsg) + if err != nil { + return NewEmitErrorAcknowledgement(ctx, types.ErrWasmError, err.Error()) + } + + fullAck := ContractAck{ContractResult: response.Data, IbcAck: ack.Acknowledgement()} + bz, err = json.Marshal(fullAck) + if err != nil { + return NewEmitErrorAcknowledgement(ctx, types.ErrBadResponse, err.Error()) + } + + return channeltypes.NewResultAcknowledgement(bz) +} + +func (h WasmHooks) execWasmMsg(ctx sdk.Context, execMsg *wasmtypes.MsgExecuteContract) (*wasmtypes.MsgExecuteContractResponse, error) { + if err := execMsg.ValidateBasic(); err != nil { + return nil, fmt.Errorf(types.ErrBadExecutionMsg, err.Error()) + } + wasmMsgServer := wasmkeeper.NewMsgServerImpl(h.ContractKeeper) + return wasmMsgServer.ExecuteContract(sdk.WrapSDKContext(ctx), execMsg) +} + +func isIcs20Packet(data []byte) (isIcs20 bool, ics20data transfertypes.FungibleTokenPacketData) { + var packetdata transfertypes.FungibleTokenPacketData + if err := json.Unmarshal(data, &packetdata); err != nil { + return false, packetdata + } + return true, packetdata +} + +// jsonStringHasKey parses the memo as a json object and checks if it contains the key. +func jsonStringHasKey(memo, key string) (found bool, jsonObject map[string]interface{}) { + jsonObject = make(map[string]interface{}) + + // If there is no memo, the packet was either sent with an earlier version of IBC, or the memo was + // intentionally left blank. Nothing to do here. Ignore the packet and pass it down the stack. + if len(memo) == 0 { + return false, jsonObject + } + + // the jsonObject must be a valid JSON object + err := json.Unmarshal([]byte(memo), &jsonObject) + if err != nil { + return false, jsonObject + } + + // If the key doesn't exist, there's nothing to do on this hook. Continue by passing the packet + // down the stack + _, ok := jsonObject[key] + if !ok { + return false, jsonObject + } + + return true, jsonObject +} + +func ValidateAndParseMemo(memo string, receiver string) (isWasmRouted bool, contractAddr sdk.AccAddress, msgBytes []byte, err error) { + isWasmRouted, metadata := jsonStringHasKey(memo, "wasm") + if !isWasmRouted { + return isWasmRouted, sdk.AccAddress{}, nil, nil + } + + wasmRaw := metadata["wasm"] + + // Make sure the wasm key is a map. If it isn't, ignore this packet + wasm, ok := wasmRaw.(map[string]interface{}) + if !ok { + return isWasmRouted, sdk.AccAddress{}, nil, + fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, "wasm metadata is not a valid JSON map object") + } + + // Get the contract + contract, ok := wasm["contract"].(string) + if !ok { + // The tokens will be returned + return isWasmRouted, sdk.AccAddress{}, nil, + fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `Could not find key wasm["contract"]`) + } + + contractAddr, err = sdk.AccAddressFromBech32(contract) + if err != nil { + return isWasmRouted, sdk.AccAddress{}, nil, + fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `wasm["contract"] is not a valid bech32 address`) + } + + // The contract and the receiver should be the same for the packet to be valid + if contract != receiver { + return isWasmRouted, sdk.AccAddress{}, nil, + fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `wasm["contract"] should be the same as the receiver of the packet`) + } + + // Ensure the message key is provided + if wasm["msg"] == nil { + return isWasmRouted, sdk.AccAddress{}, nil, + fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `Could not find key wasm["msg"]`) + } + + // Make sure the msg key is a map. If it isn't, return an error + _, ok = wasm["msg"].(map[string]interface{}) + if !ok { + return isWasmRouted, sdk.AccAddress{}, nil, + fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `wasm["msg"] is not a map object`) + } + + // Get the message string by serializing the map + msgBytes, err = json.Marshal(wasm["msg"]) + if err != nil { + // The tokens will be returned + return isWasmRouted, sdk.AccAddress{}, nil, + fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, err.Error()) + } + + return isWasmRouted, contractAddr, msgBytes, nil +} + +func (h WasmHooks) SendPacketOverride(i ICS4Middleware, ctx sdk.Context, chanCap *capabilitytypes.Capability, sourcePort string, sourceChannel string, timeoutHeight ibcclienttypes.Height, timeoutTimestamp uint64, data []byte) (sequence uint64, err error) { + isIcs20, ics20data := isIcs20Packet(data) + if !isIcs20 { + return i.channel.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) // continue + } + + isCallbackRouted, metadata := jsonStringHasKey(ics20data.GetMemo(), types.IBCCallbackKey) + if !isCallbackRouted { + return i.channel.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) // continue + } + + // We remove the callback metadata from the memo as it has already been processed. + + // If the only available key in the memo is the callback, we should remove the memo + // from the data completely so the packet is sent without it. + // This way receiver chains that are on old versions of IBC will be able to process the packet + callbackRaw := metadata[types.IBCCallbackKey] // This will be used later. + delete(metadata, types.IBCCallbackKey) + bzMetadata, err := json.Marshal(metadata) + if err != nil { + return 0, errors.Wrap(err, "ibc_callback marshall error") + } + stringMetadata := string(bzMetadata) + if stringMetadata == "{}" { + ics20data.Memo = "" + } else { + ics20data.Memo = stringMetadata + } + dataBytes, err := json.Marshal(ics20data) + if err != nil { + return 0, errors.Wrap(err, "ics20data marshall error") + } + + seq, err := i.channel.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, dataBytes) + if err != nil { + return 0, err + } + + // Make sure the callback contract is a string and a valid bech32 addr. If it isn't, ignore this packet + contract, ok := callbackRaw.(string) + if !ok { + return 0, nil + } + _, err = sdk.AccAddressFromBech32(contract) + if err != nil { + return 0, nil + } + + h.ibcHooksKeeper.StorePacketCallback(ctx, sourceChannel, seq, contract) + return seq, nil +} + +func (h WasmHooks) OnAcknowledgementPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress) error { + err := im.App.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer) + if err != nil { + return err + } + + if !h.ProperlyConfigured() { + // Not configured. Return from the underlying implementation + return nil + } + + contract := h.ibcHooksKeeper.GetPacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence()) + if contract == "" { + // No callback configured + return nil + } + + contractAddr, err := sdk.AccAddressFromBech32(contract) + if err != nil { + return errors.Wrap(err, "Ack callback error") // The callback configured is not a bech32. Error out + } + + success := "false" + if !IsJsonAckError(acknowledgement) { + success = "true" + } + + // Notify the sender that the ack has been received + ackAsJson, err := json.Marshal(acknowledgement) + if err != nil { + // If the ack is not a json object, error + return err + } + + sudoMsg := []byte(fmt.Sprintf( + `{"ibc_lifecycle_complete": {"ibc_ack": {"channel": "%s", "sequence": %d, "ack": %s, "success": %s}}}`, + packet.SourceChannel, packet.Sequence, ackAsJson, success)) + _, err = h.ContractKeeper.Sudo(ctx, contractAddr, sudoMsg) + if err != nil { + // error processing the callback + // ToDo: Open Question: Should we also delete the callback here? + return errors.Wrap(err, "Ack callback error") + } + h.ibcHooksKeeper.DeletePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence()) + return nil +} + +func (h WasmHooks) OnTimeoutPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error { + err := im.App.OnTimeoutPacket(ctx, packet, relayer) + if err != nil { + return err + } + + if !h.ProperlyConfigured() { + // Not configured. Return from the underlying implementation + return nil + } + + contract := h.ibcHooksKeeper.GetPacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence()) + if contract == "" { + // No callback configured + return nil + } + + contractAddr, err := sdk.AccAddressFromBech32(contract) + if err != nil { + return errors.Wrap(err, "Timeout callback error") // The callback configured is not a bech32. Error out + } + + sudoMsg := []byte(fmt.Sprintf( + `{"ibc_lifecycle_complete": {"ibc_timeout": {"channel": "%s", "sequence": %d}}}`, + packet.SourceChannel, packet.Sequence)) + _, err = h.ContractKeeper.Sudo(ctx, contractAddr, sudoMsg) + if err != nil { + // error processing the callback. This could be because the contract doesn't implement the message type to + // process the callback. Retrying this will not help, so we can delete the callback from storage. + // Since the packet has timed out, we don't expect any other responses that may trigger the callback. + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + "ibc-timeout-callback-error", + sdk.NewAttribute("contract", contractAddr.String()), + sdk.NewAttribute("message", string(sudoMsg)), + sdk.NewAttribute("error", err.Error()), + ), + }) + } + h.ibcHooksKeeper.DeletePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence()) + return nil +} + +// NewEmitErrorAcknowledgement creates a new error acknowledgement after having emitted an event with the +// details of the error. +func NewEmitErrorAcknowledgement(ctx sdk.Context, err error, errorContexts ...string) channeltypes.Acknowledgement { + errorType := "ibc-acknowledgement-error" + logger := ctx.Logger().With("module", errorType) + + attributes := make([]sdk.Attribute, len(errorContexts)+1) + attributes[0] = sdk.NewAttribute("error", err.Error()) + for i, s := range errorContexts { + attributes[i+1] = sdk.NewAttribute("error-context", s) + logger.Error(fmt.Sprintf("error-context: %v", s)) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + errorType, + attributes..., + ), + }) + + return channeltypes.NewErrorAcknowledgement(err) +} + +// IsJsonAckError checks an IBC acknowledgement to see if it's an error. +// This is a replacement for ack.Success() which is currently not working on some circumstances +func IsJsonAckError(acknowledgement []byte) bool { + var ackErr channeltypes.Acknowledgement_Error + if err := json.Unmarshal(acknowledgement, &ackErr); err == nil && len(ackErr.Error) > 0 { + return true + } + return false +} + +// MustExtractDenomFromPacketOnRecv takes a packet with a valid ICS20 token data in the Data field and returns the +// denom as represented in the local chain. +// If the data cannot be unmarshalled this function will panic +func MustExtractDenomFromPacketOnRecv(packet ibcexported.PacketI) string { + var data transfertypes.FungibleTokenPacketData + if err := json.Unmarshal(packet.GetData(), &data); err != nil { + panic("unable to unmarshal ICS20 packet data") + } + + var denom string + if transfertypes.ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data.Denom) { + // remove prefix added by sender chain + voucherPrefix := transfertypes.GetDenomPrefix(packet.GetSourcePort(), packet.GetSourceChannel()) + + unprefixedDenom := data.Denom[len(voucherPrefix):] + + // coin denomination used in sending from the escrow address + denom = unprefixedDenom + + // The denomination used to send the coins is either the native denom or the hash of the path + // if the denomination is not native. + denomTrace := transfertypes.ParseDenomTrace(unprefixedDenom) + if denomTrace.Path != "" { + denom = denomTrace.IBCDenom() + } + } else { + prefixedDenom := transfertypes.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel()) + data.Denom + denom = transfertypes.ParseDenomTrace(prefixedDenom).IBCDenom() + } + return denom +}