diff --git a/app/app.go b/app/app.go index 33e5554927..67c6bd7192 100644 --- a/app/app.go +++ b/app/app.go @@ -613,6 +613,7 @@ func NewStrideApp( appCodec, keys[autopilottypes.StoreKey], app.GetSubspace(autopilottypes.ModuleName), + app.BankKeeper, app.StakeibcKeeper, app.ClaimKeeper, app.TransferKeeper, diff --git a/x/autopilot/keeper/airdrop.go b/x/autopilot/keeper/airdrop.go index 54e04a1d02..934e4b037f 100644 --- a/x/autopilot/keeper/airdrop.go +++ b/x/autopilot/keeper/airdrop.go @@ -12,16 +12,15 @@ import ( channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" "github.com/Stride-Labs/stride/v16/utils" - "github.com/Stride-Labs/stride/v16/x/autopilot/types" claimtypes "github.com/Stride-Labs/stride/v16/x/claim/types" stakeibctypes "github.com/Stride-Labs/stride/v16/x/stakeibc/types" ) +// Attempt to link a host address with a stride address to enable airdrop claims func (k Keeper) TryUpdateAirdropClaim( ctx sdk.Context, packet channeltypes.Packet, - data transfertypes.FungibleTokenPacketData, - packetMetadata types.ClaimPacketMetadata, + transferMetadata transfertypes.FungibleTokenPacketData, ) error { params := k.GetParams(ctx) if !params.ClaimActive { @@ -39,11 +38,11 @@ func (k Keeper) TryUpdateAirdropClaim( } // grab relevant addresses - senderStrideAddress := utils.ConvertAddressToStrideAddress(data.Sender) + senderStrideAddress := utils.ConvertAddressToStrideAddress(transferMetadata.Sender) if senderStrideAddress == "" { - return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, fmt.Sprintf("invalid sender address (%s)", data.Sender)) + return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, fmt.Sprintf("invalid sender address (%s)", transferMetadata.Sender)) } - newStrideAddress := packetMetadata.StrideAddress + newStrideAddress := transferMetadata.Receiver // find the airdrop for this host chain ID airdrop, found := k.claimKeeper.GetAirdropByChainId(ctx, hostZone.ChainId) @@ -56,7 +55,7 @@ func (k Keeper) TryUpdateAirdropClaim( airdropId := airdrop.AirdropIdentifier k.Logger(ctx).Info(fmt.Sprintf("updating airdrop address %s (orig %s) to %s for airdrop %s", - senderStrideAddress, data.Sender, newStrideAddress, airdropId)) + senderStrideAddress, transferMetadata.Sender, newStrideAddress, airdropId)) return k.claimKeeper.UpdateAirdropAddress(ctx, senderStrideAddress, newStrideAddress, airdropId) } diff --git a/x/autopilot/keeper/keeper.go b/x/autopilot/keeper/keeper.go index 7335497277..a4c527842b 100644 --- a/x/autopilot/keeper/keeper.go +++ b/x/autopilot/keeper/keeper.go @@ -20,6 +20,7 @@ type ( Cdc codec.BinaryCodec storeKey storetypes.StoreKey paramstore paramtypes.Subspace + bankKeeper types.BankKeeper stakeibcKeeper stakeibckeeper.Keeper claimKeeper claimkeeper.Keeper transferKeeper types.IbcTransferKeeper @@ -30,6 +31,7 @@ func NewKeeper( Cdc codec.BinaryCodec, storeKey storetypes.StoreKey, ps paramtypes.Subspace, + bankKeeper types.BankKeeper, stakeibcKeeper stakeibckeeper.Keeper, claimKeeper claimkeeper.Keeper, transferKeeper types.IbcTransferKeeper, @@ -43,6 +45,7 @@ func NewKeeper( Cdc: Cdc, storeKey: storeKey, paramstore: ps, + bankKeeper: bankKeeper, stakeibcKeeper: stakeibcKeeper, claimKeeper: claimKeeper, transferKeeper: transferKeeper, diff --git a/x/autopilot/keeper/liquidstake.go b/x/autopilot/keeper/liquidstake.go index 0da9d78dde..ee1cec54dd 100644 --- a/x/autopilot/keeper/liquidstake.go +++ b/x/autopilot/keeper/liquidstake.go @@ -3,11 +3,11 @@ package keeper import ( "errors" "fmt" + "time" errorsmod "cosmossdk.io/errors" - + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" @@ -16,55 +16,71 @@ import ( stakeibctypes "github.com/Stride-Labs/stride/v16/x/stakeibc/types" ) +const ( + // If the forward transfer fails, the tokens are sent to the fallback address + // which is a less than ideal UX + // As a result, we decided to use a long timeout here such, even in the case + // of high activity, a timeout should be very unlikely to occur + // Empirically we found that times of high market stress took roughly + // 2 hours for transfers to complete + LiquidStakeForwardTransferTimeout = (time.Hour * 3) +) + +// Attempts to do an autopilot liquid stake (and optional forward) +// The liquid stake is only allowed if the inbound packet came along a trusted channel func (k Keeper) TryLiquidStaking( ctx sdk.Context, packet channeltypes.Packet, - newData transfertypes.FungibleTokenPacketData, - packetMetadata types.StakeibcPacketMetadata, + transferMetadata transfertypes.FungibleTokenPacketData, + autopilotMetadata types.StakeibcPacketMetadata, ) error { params := k.GetParams(ctx) if !params.StakeibcActive { return errorsmod.Wrapf(types.ErrPacketForwardingInactive, "autopilot stakeibc routing is inactive") } - // In this case, we can't process a liquid staking transaction, because we're dealing with STRD tokens - if transfertypes.ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), newData.Denom) { - return errors.New("the native token is not supported for liquid staking") - } - - amount, ok := sdk.NewIntFromString(newData.Amount) + // Verify the amount is valid + amount, ok := sdk.NewIntFromString(transferMetadata.Amount) if !ok { return errors.New("not a parsable amount field") } - // Note: newData.denom is base denom e.g. uatom - not ibc/xxx - var token = sdk.NewCoin(newData.Denom, amount) + // In this case, we can't process a liquid staking transaction, because we're dealing with native tokens (e.g. STRD, stATOM) + if transfertypes.ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), transferMetadata.Denom) { + return errors.New("the native token is not supported for liquid staking") + } - prefixedDenom := transfertypes.GetPrefixedDenom(packet.GetDestPort(), packet.GetDestChannel(), newData.Denom) + // Note: the denom in the packet is the base denom e.g. uatom - not ibc/xxx + // We need to use the port and channel to build the IBC denom + prefixedDenom := transfertypes.GetPrefixedDenom(packet.GetDestPort(), packet.GetDestChannel(), transferMetadata.Denom) ibcDenom := transfertypes.ParseDenomTrace(prefixedDenom).IBCDenom() - hostZone, err := k.stakeibcKeeper.GetHostZoneFromHostDenom(ctx, token.Denom) + hostZone, err := k.stakeibcKeeper.GetHostZoneFromHostDenom(ctx, transferMetadata.Denom) if err != nil { - return err + return fmt.Errorf("host zone not found for denom (%s)", transferMetadata.Denom) } + // Verify the IBC denom of the packet matches the host zone, to confirm the packet + // was sent over a trusted channel if hostZone.IbcDenom != ibcDenom { return fmt.Errorf("ibc denom %s is not equal to host zone ibc denom %s", ibcDenom, hostZone.IbcDenom) } - strideAddress, err := sdk.AccAddressFromBech32(packetMetadata.StrideAddress) - if err != nil { - return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid stride_address (%s) in autopilot memo", strideAddress) - } - - return k.RunLiquidStake(ctx, strideAddress, token, packetMetadata) + return k.RunLiquidStake(ctx, amount, transferMetadata, autopilotMetadata) } -func (k Keeper) RunLiquidStake(ctx sdk.Context, addr sdk.AccAddress, token sdk.Coin, packetMetadata types.StakeibcPacketMetadata) error { +// Submits a LiquidStake message from the transfer receiver +// If a forwarding recipient is specified, the stTokens are ibc transferred +func (k Keeper) RunLiquidStake( + ctx sdk.Context, + amount sdkmath.Int, + transferMetadata transfertypes.FungibleTokenPacketData, + autopilotMetadata types.StakeibcPacketMetadata, +) error { msg := &stakeibctypes.MsgLiquidStake{ - Creator: addr.String(), - Amount: token.Amount, - HostDenom: token.Denom, + Creator: transferMetadata.Receiver, + Amount: amount, + HostDenom: transferMetadata.Denom, } if err := msg.ValidateBasic(); err != nil { @@ -72,7 +88,7 @@ func (k Keeper) RunLiquidStake(ctx sdk.Context, addr sdk.AccAddress, token sdk.C } msgServer := stakeibckeeper.NewMsgServerImpl(k.stakeibcKeeper) - result, err := msgServer.LiquidStake( + msgResponse, err := msgServer.LiquidStake( sdk.WrapSDKContext(ctx), msg, ) @@ -80,35 +96,51 @@ func (k Keeper) RunLiquidStake(ctx sdk.Context, addr sdk.AccAddress, token sdk.C return errorsmod.Wrapf(err, "failed to liquid stake") } - if packetMetadata.IbcReceiver == "" { + // If the IBCReceiver is empty, there is no forwarding step + if autopilotMetadata.IbcReceiver == "" { return nil } - hostZone, err := k.stakeibcKeeper.GetHostZoneFromHostDenom(ctx, token.Denom) + // Otherwise, if there is forwarding info, submit the IBC transfer + return k.IBCTransferStToken(ctx, msgResponse.StToken, transferMetadata, autopilotMetadata) +} + +// Submits an IBC transfer of the stToken to a non-stride zone (either back to the host zone or to a different zone) +// The sender of the transfer is the hashed receiver of the original autopilot inbound transfer +func (k Keeper) IBCTransferStToken( + ctx sdk.Context, + stToken sdk.Coin, + transferMetadata transfertypes.FungibleTokenPacketData, + autopilotMetadata types.StakeibcPacketMetadata, +) error { + hostZone, err := k.stakeibcKeeper.GetHostZoneFromHostDenom(ctx, transferMetadata.Denom) if err != nil { return err } - return k.IBCTransferStAsset(ctx, result.StToken, addr.String(), hostZone, packetMetadata) -} - -func (k Keeper) IBCTransferStAsset(ctx sdk.Context, stAsset sdk.Coin, sender string, hostZone *stakeibctypes.HostZone, packetMetadata types.StakeibcPacketMetadata) error { - ibcTransferTimeoutNanos := k.stakeibcKeeper.GetParam(ctx, stakeibctypes.KeyIBCTransferTimeoutNanos) - timeoutTimestamp := uint64(ctx.BlockTime().UnixNano()) + ibcTransferTimeoutNanos - channelId := packetMetadata.TransferChannel + // If there's no channelID specified in the packet, default to the channel on the host zone + channelId := autopilotMetadata.TransferChannel if channelId == "" { channelId = hostZone.TransferChannelId } + + // Use a long timeout for the transfer + timeoutTimestamp := uint64(ctx.BlockTime().UnixNano() + LiquidStakeForwardTransferTimeout.Nanoseconds()) + + // Submit the transfer from the hashed address transferMsg := &transfertypes.MsgTransfer{ SourcePort: transfertypes.PortID, SourceChannel: channelId, - Token: stAsset, - Sender: sender, - Receiver: packetMetadata.IbcReceiver, + Token: stToken, + Sender: transferMetadata.Receiver, + Receiver: autopilotMetadata.IbcReceiver, TimeoutTimestamp: timeoutTimestamp, - // TimeoutHeight and Memo are unused + Memo: "autopilot-liquid-stake-and-forward", + } + _, err = k.transferKeeper.Transfer(sdk.WrapSDKContext(ctx), transferMsg) + if err != nil { + return errorsmod.Wrapf(err, "failed to submit transfer during autopilot liquid stake and forward") } - _, err := k.transferKeeper.Transfer(sdk.WrapSDKContext(ctx), transferMsg) return err } diff --git a/x/autopilot/module_ibc.go b/x/autopilot/module_ibc.go index 9412efeedc..d0fe5c72b5 100644 --- a/x/autopilot/module_ibc.go +++ b/x/autopilot/module_ibc.go @@ -7,7 +7,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" - ibctransfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types" @@ -127,53 +126,73 @@ func (im IBCModule) OnRecvPacket( packet.Sequence, packet.SourcePort, packet.SourceChannel, packet.DestinationPort, packet.DestinationChannel)) // NOTE: acknowledgement will be written synchronously during IBC handler execution. - var data transfertypes.FungibleTokenPacketData - if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil { + var tokenPacketData transfertypes.FungibleTokenPacketData + if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &tokenPacketData); err != nil { return channeltypes.NewErrorAcknowledgement(err) } // Error any transactions with a Memo or Receiver field are greater than the max characters - if len(data.Memo) > MaxMemoCharLength { - return channeltypes.NewErrorAcknowledgement(errorsmod.Wrapf(types.ErrInvalidMemoSize, "memo length: %d", len(data.Memo))) + if len(tokenPacketData.Memo) > MaxMemoCharLength { + return channeltypes.NewErrorAcknowledgement(errorsmod.Wrapf(types.ErrInvalidMemoSize, "memo length: %d", len(tokenPacketData.Memo))) } - if len(data.Receiver) > MaxMemoCharLength { - return channeltypes.NewErrorAcknowledgement(errorsmod.Wrapf(types.ErrInvalidMemoSize, "receiver length: %d", len(data.Receiver))) + if len(tokenPacketData.Receiver) > MaxMemoCharLength { + return channeltypes.NewErrorAcknowledgement(errorsmod.Wrapf(types.ErrInvalidMemoSize, "receiver length: %d", len(tokenPacketData.Receiver))) } // ibc-go v5 has a Memo field that can store forwarding info // For older version of ibc-go, the data must be stored in the receiver field var metadata string - if data.Memo != "" { // ibc-go v5+ - metadata = data.Memo + if tokenPacketData.Memo != "" { // ibc-go v5+ + metadata = tokenPacketData.Memo } else { // before ibc-go v5 - metadata = data.Receiver + metadata = tokenPacketData.Receiver } // If a valid receiver address has been provided and no memo, // this is clearly just an normal IBC transfer // Pass down the stack immediately instead of parsing - _, err := sdk.AccAddressFromBech32(data.Receiver) - if err == nil && data.Memo == "" { + _, err := sdk.AccAddressFromBech32(tokenPacketData.Receiver) + if err == nil && tokenPacketData.Memo == "" { return im.app.OnRecvPacket(ctx, packet, relayer) } - // parse out any forwarding info - packetForwardMetadata, err := types.ParsePacketMetadata(metadata) + // parse out any autopilot forwarding info + autopilotMetadata, err := types.ParseAutopilotMetadata(metadata) if err != nil { return channeltypes.NewErrorAcknowledgement(err) } - // If the parsed metadata is nil, that means there is no forwarding logic + // If the parsed metadata is nil, that means there is no autopilot forwarding logic // Pass the packet down to the next middleware - if packetForwardMetadata == nil { + // PFM packets will also go down this path + if autopilotMetadata == nil { return im.app.OnRecvPacket(ctx, packet, relayer) } - // Modify the packet data by replacing the JSON metadata field with a receiver address - // to allow the packet to continue down the stack - newData := data - newData.Receiver = packetForwardMetadata.Receiver - bz, err := transfertypes.ModuleCdc.MarshalJSON(&newData) + //// At this point, we are officially dealing with an autopilot packet + + // Update the reciever in the packet data so that we can pass the packet down the stack + // (since the "receiver" may have technically been a full JSON memo) + tokenPacketData.Receiver = autopilotMetadata.Receiver + + // For autopilot liquid stake and forward, we'll override the receiver with a hashed address + // The hashed address will also be the sender of the outbound transfer + // This is to prevent impersonation at downstream zones + // We can identify the forwarding step by whether there's a non-empty IBC receiver field + if routingInfo, ok := autopilotMetadata.RoutingInfo.(types.StakeibcPacketMetadata); ok && + routingInfo.Action == types.LiquidStake && routingInfo.IbcReceiver != "" { + + var err error + hashedReceiver, err := types.GenerateHashedAddress(packet.DestinationChannel, tokenPacketData.Sender) + if err != nil { + return channeltypes.NewErrorAcknowledgement(err) + } + tokenPacketData.Receiver = hashedReceiver + } + + // Now that the receiver's been updated on the transfer metadata, + // modify the original packet so that we can send it down the stack + bz, err := transfertypes.ModuleCdc.MarshalJSON(&tokenPacketData) if err != nil { return channeltypes.NewErrorAcknowledgement(err) } @@ -187,28 +206,29 @@ func (im IBCModule) OnRecvPacket( } autopilotParams := im.keeper.GetParams(ctx) + sender := tokenPacketData.Sender // If the transfer was successful, then route to the corresponding module, if applicable - switch routingInfo := packetForwardMetadata.RoutingInfo.(type) { + switch routingInfo := autopilotMetadata.RoutingInfo.(type) { case types.StakeibcPacketMetadata: // If stakeibc routing is inactive (but the packet had routing info in the memo) return an ack error if !autopilotParams.StakeibcActive { - im.keeper.Logger(ctx).Error(fmt.Sprintf("Packet from %s had stakeibc routing info but autopilot stakeibc routing is disabled", newData.Sender)) + im.keeper.Logger(ctx).Error(fmt.Sprintf("Packet from %s had stakeibc routing info but autopilot stakeibc routing is disabled", sender)) return channeltypes.NewErrorAcknowledgement(types.ErrPacketForwardingInactive) } - im.keeper.Logger(ctx).Info(fmt.Sprintf("Forwaring packet from %s to stakeibc", newData.Sender)) + im.keeper.Logger(ctx).Info(fmt.Sprintf("Forwaring packet from %s to stakeibc", sender)) switch routingInfo.Action { case types.LiquidStake: // Try to liquid stake - return an ack error if it fails, otherwise return the ack generated from the earlier packet propogation - if err := im.keeper.TryLiquidStaking(ctx, packet, newData, routingInfo); err != nil { - im.keeper.Logger(ctx).Error(fmt.Sprintf("Error liquid staking packet from autopilot for %s: %s", newData.Sender, err.Error())) + if err := im.keeper.TryLiquidStaking(ctx, packet, tokenPacketData, routingInfo); err != nil { + im.keeper.Logger(ctx).Error(fmt.Sprintf("Error liquid staking packet from autopilot for %s: %s", sender, err.Error())) return channeltypes.NewErrorAcknowledgement(err) } case types.RedeemStake: // Try to redeem stake - return an ack error if it fails, otherwise return the ack generated from the earlier packet propogation - if err := im.keeper.TryRedeemStake(ctx, packet, newData, routingInfo); err != nil { - im.keeper.Logger(ctx).Error(fmt.Sprintf("Error redeem staking packet from autopilot for %s: %s", newData.Sender, err.Error())) + if err := im.keeper.TryRedeemStake(ctx, packet, tokenPacketData, routingInfo); err != nil { + im.keeper.Logger(ctx).Error(fmt.Sprintf("Error redeem staking packet from autopilot for %s: %s", sender, err.Error())) return channeltypes.NewErrorAcknowledgement(err) } } @@ -218,13 +238,13 @@ func (im IBCModule) OnRecvPacket( case types.ClaimPacketMetadata: // If claim routing is inactive (but the packet had routing info in the memo) return an ack error if !autopilotParams.ClaimActive { - im.keeper.Logger(ctx).Error(fmt.Sprintf("Packet from %s had claim routing info but autopilot claim routing is disabled", newData.Sender)) + im.keeper.Logger(ctx).Error(fmt.Sprintf("Packet from %s had claim routing info but autopilot claim routing is disabled", sender)) return channeltypes.NewErrorAcknowledgement(types.ErrPacketForwardingInactive) } - im.keeper.Logger(ctx).Info(fmt.Sprintf("Forwaring packet from %s to claim", newData.Sender)) + im.keeper.Logger(ctx).Info(fmt.Sprintf("Forwaring packet from %s to claim", sender)) - if err := im.keeper.TryUpdateAirdropClaim(ctx, packet, newData, routingInfo); err != nil { - im.keeper.Logger(ctx).Error(fmt.Sprintf("Error updating airdrop claim from autopilot for %s: %s", newData.Sender, err.Error())) + if err := im.keeper.TryUpdateAirdropClaim(ctx, packet, tokenPacketData); err != nil { + im.keeper.Logger(ctx).Error(fmt.Sprintf("Error updating airdrop claim from autopilot for %s: %s", sender, err.Error())) return channeltypes.NewErrorAcknowledgement(err) } @@ -280,5 +300,5 @@ func (im IBCModule) WriteAcknowledgement( // GetAppVersion returns the interchain accounts metadata. func (im IBCModule) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { - return ibctransfertypes.Version, true // im.keeper.GetAppVersion(ctx, portID, channelID) + return transfertypes.Version, true // im.keeper.GetAppVersion(ctx, portID, channelID) } diff --git a/x/autopilot/types/autopilot.go b/x/autopilot/types/autopilot.go new file mode 100644 index 0000000000..44e8eaf67b --- /dev/null +++ b/x/autopilot/types/autopilot.go @@ -0,0 +1,50 @@ +package types + +import ( + fmt "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" +) + +// RawPacketMetadata defines the raw JSON memo that's used in an autopilot transfer +// The PFM forward key is also used here to validate that the packet is not trying +// to use autopilot and PFM at the same time +// As a result, only the forward key is needed, cause the actual parsing of the PFM +// packet will occur in the PFM module +type RawPacketMetadata struct { + Autopilot *struct { + Receiver string `json:"receiver"` + Stakeibc *StakeibcPacketMetadata `json:"stakeibc,omitempty"` + Claim *ClaimPacketMetadata `json:"claim,omitempty"` + } `json:"autopilot"` + Forward *interface{} `json:"forward"` +} + +// AutopilotActionMetadata stores the metadata that's specific to the autopilot action +// e.g. Fields required for LiquidStake +type AutopilotMetadata struct { + Receiver string + RoutingInfo ModuleRoutingInfo +} + +// ModuleRoutingInfo defines the interface required for each autopilot action +type ModuleRoutingInfo interface { + Validate() error +} + +// GenerateHashedSender generates a new address for a packet, by hashing +// the channel and original sender. +// This makes the address deterministic and can used to identify the sender +// from the preivous hop +// Additionally, this prevents a forwarded packet from impersonating a different account +// when moving to the next hop (i.e. receiver of one hop, becomes sender of next) +// +// This function was borrowed from PFM +func GenerateHashedAddress(channelId, originalSender string) (string, error) { + senderStr := fmt.Sprintf("%s/%s", channelId, originalSender) + senderHash32 := address.Hash(ModuleName, []byte(senderStr)) + sender := sdk.AccAddress(senderHash32[:20]) + bech32Prefix := sdk.GetConfig().GetBech32AccountAddrPrefix() + return sdk.Bech32ifyAddressBytes(bech32Prefix, sender) +} diff --git a/x/autopilot/types/expected_keepers.go b/x/autopilot/types/expected_keepers.go index 4ee6298623..cf81d20bc2 100644 --- a/x/autopilot/types/expected_keepers.go +++ b/x/autopilot/types/expected_keepers.go @@ -3,10 +3,14 @@ package types import ( context "context" + sdk "github.com/cosmos/cosmos-sdk/types" transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" ) -// AccountKeeper defines the expected account keeper used for simulations (noalias) +type BankKeeper interface { + SendCoins(ctx sdk.Context, senderAddr sdk.AccAddress, recipientAddr sdk.AccAddress, amt sdk.Coins) error +} + type IbcTransferKeeper interface { Transfer(goCtx context.Context, msg *transfertypes.MsgTransfer) (*transfertypes.MsgTransferResponse, error) } diff --git a/x/autopilot/types/parser.go b/x/autopilot/types/parser.go index c515fb5743..bb540ff8fa 100644 --- a/x/autopilot/types/parser.go +++ b/x/autopilot/types/parser.go @@ -4,27 +4,9 @@ import ( "encoding/json" errorsmod "cosmossdk.io/errors" - sdk "github.com/cosmos/cosmos-sdk/types" ) -type RawPacketMetadata struct { - Autopilot *struct { - Receiver string `json:"receiver"` - Stakeibc *StakeibcPacketMetadata `json:"stakeibc,omitempty"` - Claim *ClaimPacketMetadata `json:"claim,omitempty"` - } `json:"autopilot"` -} - -type PacketForwardMetadata struct { - Receiver string - RoutingInfo ModuleRoutingInfo -} - -type ModuleRoutingInfo interface { - Validate() error -} - const LiquidStake = "LiquidStake" const RedeemStake = "RedeemStake" @@ -73,10 +55,10 @@ func (m ClaimPacketMetadata) Validate() error { // Parse packet metadata intended for autopilot // In the ICS-20 packet, the metadata can optionally indicate a module to route to (e.g. stakeibc) -// The PacketForwardMetadata returned from this function contains attributes for each autopilot supported module +// The AutopilotMetadata returned from this function contains attributes for each autopilot supported module // It can only be forward to one module per packet -// Returns nil if there was no metadata found -func ParsePacketMetadata(metadata string) (*PacketForwardMetadata, error) { +// Returns nil if there was no autopilot metadata found +func ParseAutopilotMetadata(metadata string) (*AutopilotMetadata, error) { // If we can't unmarshal the metadata into a PacketMetadata struct, // assume packet forwarding was no used and pass back nil so that autopilot is ignored var raw RawPacketMetadata @@ -84,7 +66,14 @@ func ParsePacketMetadata(metadata string) (*PacketForwardMetadata, error) { return nil, nil } - // If no forwarding logic was used for autopilot, return the metadata with each disabled + // Packets cannot be used for both autopilot and PFM at the same time + // If both fields were provided, reject the packet + if raw.Autopilot != nil && raw.Forward != nil { + return nil, errorsmod.Wrapf(ErrInvalidPacketMetadata, "autopilot and pfm cannot both be used in the same packet") + } + + // If no forwarding logic was used for autopilot, return nil to indicate that + // there's no autopilot action needed if raw.Autopilot == nil { return nil, nil } @@ -119,7 +108,7 @@ func ParsePacketMetadata(metadata string) (*PacketForwardMetadata, error) { return nil, errorsmod.Wrapf(err, ErrInvalidPacketMetadata.Error()) } - return &PacketForwardMetadata{ + return &AutopilotMetadata{ Receiver: raw.Autopilot.Receiver, RoutingInfo: routingInfo, }, nil diff --git a/x/autopilot/types/parser_test.go b/x/autopilot/types/parser_test.go index 4679e77445..8d83afd403 100644 --- a/x/autopilot/types/parser_test.go +++ b/x/autopilot/types/parser_test.go @@ -125,6 +125,11 @@ func TestParsePacketMetadata(t *testing.T) { metadata: validAddress, // normal address - not autopilot JSON expectedNilMetadata: true, }, + { + name: "PFM transfer", + metadata: `{"forward": {}}`, + expectedNilMetadata: true, + }, { name: "empty memo", metadata: "", @@ -140,6 +145,11 @@ func TestParsePacketMetadata(t *testing.T) { metadata: `{ "other_module": { } }`, expectedNilMetadata: true, }, + { + name: "both autopilot and pfm in the memo", + metadata: `{"autopilot": {}, "forward": {}}`, + expectedErr: "autopilot and pfm cannot both be used in the same packet", + }, { name: "empty receiver address", metadata: `{ "autopilot": { } }`, @@ -174,7 +184,7 @@ func TestParsePacketMetadata(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - parsedData, actualErr := types.ParsePacketMetadata(tc.metadata) + parsedData, actualErr := types.ParseAutopilotMetadata(tc.metadata) if tc.expectedErr == "" { require.NoError(t, actualErr) diff --git a/x/stakeibc/keeper/msg_server_redeem_stake.go b/x/stakeibc/keeper/msg_server_redeem_stake.go index cc615694c4..9080f66ead 100644 --- a/x/stakeibc/keeper/msg_server_redeem_stake.go +++ b/x/stakeibc/keeper/msg_server_redeem_stake.go @@ -49,7 +49,6 @@ func (k msgServer) RedeemStake(goCtx context.Context, msg *types.MsgRedeemStake) // construct desired unstaking amount from host zone stDenom := types.StAssetDenomFromHostZoneDenom(hostZone.HostDenom) - // ASSUMPTION (CHECK ME): it doesn't matter that RRs can change _within_ an epoch (due to LSM) nativeAmount := sdk.NewDecFromInt(msg.Amount).Mul(hostZone.RedemptionRate).RoundInt() if nativeAmount.GT(hostZone.TotalDelegations) {