Skip to content

Commit

Permalink
feat(eibc): update demand order (#915)
Browse files Browse the repository at this point in the history
  • Loading branch information
mtsitrin authored Jun 18, 2024
1 parent 7654979 commit 6721d98
Show file tree
Hide file tree
Showing 16 changed files with 1,033 additions and 253 deletions.
6 changes: 2 additions & 4 deletions ibctesting/eibc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ func (suite *EIBCTestSuite) TestEIBCDemandOrderFulfillment() {
msgFulfillDemandOrder := &eibctypes.MsgFulfillOrder{
FulfillerAddress: fulfiller.String(),
OrderId: lastDemandOrder.Id,
ExpectedFee: tc.EIBCTransferFee,
}
// Validate demand order status based on fulfillment success
_, err = suite.msgServer.FulfillOrder(suite.hubChain.GetContext(), msgFulfillDemandOrder)
Expand Down Expand Up @@ -477,10 +478,7 @@ func (suite *EIBCTestSuite) TestTimeoutEIBCDemandOrderFulfillment() {
suite.Require().Equal(expectedPrice, lastDemandOrder.Price[0].Amount)
suite.Require().Equal(coinToSendToB.Denom, lastDemandOrder.Price[0].Denom)
// Fulfill the demand order
msgFulfillDemandOrder := &eibctypes.MsgFulfillOrder{
FulfillerAddress: fulfillerAccount.String(),
OrderId: lastDemandOrder.Id,
}
msgFulfillDemandOrder := eibctypes.NewMsgFulfillOrder(fulfillerAccount.String(), lastDemandOrder.Id, lastDemandOrder.Fee[0].Amount.String())
_, err = suite.msgServer.FulfillOrder(suite.hubChain.GetContext(), msgFulfillDemandOrder)
suite.Require().NoError(err)
// Validate balances of fulfiller and sender are updated while the original recipient is not
Expand Down
2 changes: 1 addition & 1 deletion proto/dymension/eibc/demand_order.proto
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ option go_package = "github.com/dymensionxyz/dymension/v3/x/eibc/types";

message DemandOrder {
// id is a hash of the form generated by GetRollappPacketKey,
// e.g status/rollappid/packetProofHeight/packetDestinationChannel-PacketSequence which gurantees uniqueness
// e.g status/rollappid/packetProofHeight/packetDestinationChannel-PacketSequence which guarantees uniqueness
string id = 1;
// tracking_packet_key is the key of the packet that is being tracked.
// This key can change depends on the packet status.
Expand Down
22 changes: 21 additions & 1 deletion proto/dymension/eibc/tx.proto
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
syntax = "proto3";
package dymensionxyz.dymension.eibc;

import "cosmos/msg/v1/msg.proto";

option go_package = "github.com/dymensionxyz/dymension/v3/x/eibc/types";

// Msg defines the Msg service.
service Msg {
rpc FulfillOrder(MsgFulfillOrder) returns (MsgFulfillOrderResponse) {}
rpc UpdateDemandOrder(MsgUpdateDemandOrder) returns (MsgUpdateDemandOrderResponse) {}
}

// MsgFulfillOrder defines the FulfillOrder request type.
message MsgFulfillOrder {
option (cosmos.msg.v1.signer) = "fulfiller_address";
// fulfiller_address is the bech32-encoded address of the account which the message was sent from.
string fulfiller_address = 1;
// order_id is the unique identifier of the order to be fulfilled.
string order_id = 2;
// expected_fee is the nominal fee set in the order.
string expected_fee = 3;
}

// MsgFulfillOrderResponse defines the FulfillOrder response type.
message MsgFulfillOrderResponse {}
message MsgFulfillOrderResponse {}

message MsgUpdateDemandOrder {
option (cosmos.msg.v1.signer) = "owner_address";
// owner_address is the bech32-encoded address of the account owns the order.
// This is expected to be the address of the order recipient.
string owner_address = 1;
// order_id is the unique identifier of the order to be updated.
string order_id = 2;
// new_fee is the new fee amount to be set in the order.
string new_fee = 3;
}

message MsgUpdateDemandOrderResponse {}
47 changes: 42 additions & 5 deletions x/eibc/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,64 @@ func GetTxCmd() *cobra.Command {
RunE: client.ValidateCmd,
}

cmd.AddCommand(NewFullfilOrderTxCmd())
cmd.AddCommand(NewFulfillOrderTxCmd())

return cmd
}

func NewFullfilOrderTxCmd() *cobra.Command {
func NewFulfillOrderTxCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "fulfill-order [order-id]",
Short: "Fullfil a new eibc order",
Example: "dymd tx eibc fulfill-order <order-id>",
Args: cobra.ExactArgs(1),
Short: "Fulfill a new eibc order",
Example: "dymd tx eibc fulfill-order <order-id> <expected-fee-amount>",
Long: `Fulfill a new eibc order by providing the order ID and the expected fee amount.
The expected fee amount is the amount of fee that the user expects to pay for fulfilling the order.
`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) (err error) {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
orderId := args[0]
fee := args[1]

msg := types.NewMsgFulfillOrder(
clientCtx.GetFromAddress().String(),
orderId,
fee,
)

if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)

return cmd
}

func NewUpdateDemandOrderTxCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "update-demand-order [order-id]",
Short: "Update a demand order",
Example: "dymd tx eibc update-demand-order <order-id> <new-fee-amount>",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) (err error) {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
orderId := args[0]
newFee := args[1]

msg := types.NewMsgUpdateDemandOrder(
clientCtx.GetFromAddress().String(),
orderId,
newFee,
)

if err := msg.ValidateBasic(); err != nil {
Expand Down
33 changes: 13 additions & 20 deletions x/eibc/keeper/demand_order.go → x/eibc/keeper/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (

"github.com/dymensionxyz/dymension/v3/utils"
commontypes "github.com/dymensionxyz/dymension/v3/x/common/types"
"github.com/dymensionxyz/dymension/v3/x/delayedack/types"
eibctypes "github.com/dymensionxyz/dymension/v3/x/eibc/types"
dacktypes "github.com/dymensionxyz/dymension/v3/x/delayedack/types"
"github.com/dymensionxyz/dymension/v3/x/eibc/types"
)

// EIBCDemandOrderHandler handles the eibc packet by creating a demand order from the packet data and saving it in the store.
Expand All @@ -20,7 +20,7 @@ import (
// If the rollapp packet is of type ON_TIMEOUT/ON_ACK, the function will calculate the fee and create a demand order from the packet data.
func (k Keeper) EIBCDemandOrderHandler(ctx sdk.Context, rollappPacket commontypes.RollappPacket, data transfertypes.FungibleTokenPacketData) error {
var (
eibcDemandOrder *eibctypes.DemandOrder
eibcDemandOrder *types.DemandOrder
err error
)
// Validate the fungible token packet data as we're going to use it to create the demand order
Expand All @@ -29,7 +29,7 @@ func (k Keeper) EIBCDemandOrderHandler(ctx sdk.Context, rollappPacket commontype
}
// Verify the original recipient is not a blocked sender otherwise could potentially use eibc to bypass it
if k.BlockedAddr(data.Receiver) {
return fmt.Errorf("not allowed to receive funds: receiver: %s", data.Receiver)
return types.ErrBlockedAddress
}

switch t := rollappPacket.Type; t {
Expand Down Expand Up @@ -60,9 +60,9 @@ func (k Keeper) EIBCDemandOrderHandler(ctx sdk.Context, rollappPacket commontype
// It returns the created demand order or an error if there is any.
func (k *Keeper) CreateDemandOrderOnRecv(ctx sdk.Context, fungibleTokenPacketData transfertypes.FungibleTokenPacketData,
rollappPacket *commontypes.RollappPacket,
) (*eibctypes.DemandOrder, error) {
packetMetaData, err := types.ParsePacketMetadata(fungibleTokenPacketData.Memo)
if errors.Is(err, types.ErrMemoUnmarshal) || errors.Is(err, types.ErrMemoEibcEmpty) {
) (*types.DemandOrder, error) {
packetMetaData, err := dacktypes.ParsePacketMetadata(fungibleTokenPacketData.Memo)
if errors.Is(err, dacktypes.ErrMemoUnmarshal) || errors.Is(err, dacktypes.ErrMemoEibcEmpty) {
ctx.Logger().Debug("skipping demand order creation - no eibc memo provided")
return nil, nil
}
Expand All @@ -78,30 +78,23 @@ func (k *Keeper) CreateDemandOrderOnRecv(ctx sdk.Context, fungibleTokenPacketDat
// Calculate the demand order price and validate it,
amt, _ := sdk.NewIntFromString(fungibleTokenPacketData.Amount) // guaranteed ok and positive by above validation
fee, _ := eibcMetaData.FeeInt() // guaranteed ok by above validation
if amt.LT(fee) {
return nil, fmt.Errorf("fee cannot be larger than amount: fee: %s: amt :%s", fee, fungibleTokenPacketData.Amount)
}

// Get the bridging fee from the amount
bridgingFee := k.dack.BridgingFeeFromAmt(ctx, amt)
demandOrderPrice := amt.Sub(fee).Sub(bridgingFee)
if !demandOrderPrice.IsPositive() {
return nil, fmt.Errorf("remaining price is not positive: price: %s, bridging fee: %s, fee: %s, amount: %s",
demandOrderPrice, bridgingFee, fee, amt)
demandOrderPrice, err := types.CalcPriceWithBridgingFee(amt, fee, k.dack.BridgingFee(ctx))
if err != nil {
return nil, err
}

demandOrderDenom := k.getEIBCTransferDenom(*rollappPacket.Packet, fungibleTokenPacketData)
demandOrderRecipient := fungibleTokenPacketData.Receiver // who we tried to send to

order := eibctypes.NewDemandOrder(*rollappPacket, demandOrderPrice, fee, demandOrderDenom, demandOrderRecipient)
order := types.NewDemandOrder(*rollappPacket, demandOrderPrice, fee, demandOrderDenom, demandOrderRecipient)
return order, nil
}

// CreateDemandOrderOnErrAckOrTimeout creates a demand order for a timeout or errack packet.
// The fee multiplier is read from params and used to calculate the fee.
func (k Keeper) CreateDemandOrderOnErrAckOrTimeout(ctx sdk.Context, fungibleTokenPacketData transfertypes.FungibleTokenPacketData,
rollappPacket *commontypes.RollappPacket,
) (*eibctypes.DemandOrder, error) {
) (*types.DemandOrder, error) {
// Calculate the demand order price and validate it,
amt, _ := sdk.NewIntFromString(fungibleTokenPacketData.Amount) // guaranteed ok and positive by above validation

Expand All @@ -124,7 +117,7 @@ func (k Keeper) CreateDemandOrderOnErrAckOrTimeout(ctx sdk.Context, fungibleToke
demandOrderDenom := trace.IBCDenom()
demandOrderRecipient := fungibleTokenPacketData.Sender // and who tried to send it (refund because it failed)

order := eibctypes.NewDemandOrder(*rollappPacket, demandOrderPrice, fee, demandOrderDenom, demandOrderRecipient)
order := types.NewDemandOrder(*rollappPacket, demandOrderPrice, fee, demandOrderDenom, demandOrderRecipient)
return order, nil
}

Expand Down
2 changes: 1 addition & 1 deletion x/eibc/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var (
timeoutTimestamp = uint64(100)
transferPacketData = transfertypes.NewFungibleTokenPacketData(
sdk.DefaultBondDenom,
"100",
"1000",
eibcSenderAddr.String(),
eibcReceiverAddr.String(),
"",
Expand Down
96 changes: 82 additions & 14 deletions x/eibc/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package keeper
import (
"context"

errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
transfertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types"
commontypes "github.com/dymensionxyz/dymension/v3/x/common/types"
"github.com/dymensionxyz/dymension/v3/x/eibc/types"
)
Expand All @@ -23,29 +26,24 @@ var _ types.MsgServer = msgServer{}
func (m msgServer) FulfillOrder(goCtx context.Context, msg *types.MsgFulfillOrder) (*types.MsgFulfillOrderResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
logger := ctx.Logger()
// Check that the msg is valid

err := msg.ValidateBasic()
if err != nil {
return nil, err
}
// Check that the order exists in status PENDING
demandOrder, err := m.GetDemandOrder(ctx, commontypes.Status_PENDING, msg.OrderId)

demandOrder, err := m.GetOutstandingOrder(ctx, msg.OrderId)
if err != nil {
return nil, err
}
// Check that the order is not fulfilled yet
if demandOrder.IsFulfilled {
return nil, types.ErrDemandAlreadyFulfilled
}

// Check the underlying packet is still relevant (i.e not expired, rejected, reverted)
if demandOrder.TrackingPacketStatus != commontypes.Status_PENDING {
return nil, types.ErrDemandOrderInactive
}
// Check for blocked address
if m.bk.BlockedAddr(demandOrder.GetRecipientBech32Address()) {
return nil, types.ErrBlockedAddress
// Check that the fulfiller expected fee is equal to the demand order fee
expectedFee, _ := sdk.NewIntFromString(msg.ExpectedFee)
orderFee := demandOrder.GetFeeAmount()
if !orderFee.Equal(expectedFee) {
return nil, types.ErrExpectedFeeNotMet
}

// Check that the fulfiller has enough balance to fulfill the order
fulfillerAccount := m.ak.GetAccount(ctx, msg.GetFulfillerBech32Address())
if fulfillerAccount == nil {
Expand All @@ -66,3 +64,73 @@ func (m msgServer) FulfillOrder(goCtx context.Context, msg *types.MsgFulfillOrde

return &types.MsgFulfillOrderResponse{}, err
}

// UpdateDemandOrder implements types.MsgServer.
func (m msgServer) UpdateDemandOrder(goCtx context.Context, msg *types.MsgUpdateDemandOrder) (*types.MsgUpdateDemandOrderResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

err := msg.ValidateBasic()
if err != nil {
return nil, err
}

// Check that the order exists in status PENDING
demandOrder, err := m.GetOutstandingOrder(ctx, msg.OrderId)
if err != nil {
return nil, err
}

// Check that the signer is the order owner
orderOwner := demandOrder.GetRecipientBech32Address()
msgSigner := msg.GetSignerAddr()
if !msgSigner.Equals(orderOwner) {
return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "only the recipient can update the order")
}

raPacket, err := m.dack.GetRollappPacket(ctx, demandOrder.TrackingPacketKey)
if err != nil {
return nil, err
}

var data transfertypes.FungibleTokenPacketData
if err := transfertypes.ModuleCdc.UnmarshalJSON(raPacket.GetPacket().GetData(), &data); err != nil {
return nil, err
}

// Get the bridging fee multiplier
// ErrAck or Timeout packets do not incur bridging fees
bridgingFeeMultiplier := m.dack.BridgingFee(ctx)
raPacketType := raPacket.GetType()
if raPacketType != commontypes.RollappPacket_ON_RECV {
bridgingFeeMultiplier = sdk.ZeroDec()
}

// calculate the new price: transferTotal - newFee - bridgingFee
newFeeInt, _ := sdk.NewIntFromString(msg.NewFee)
transferTotal, _ := sdk.NewIntFromString(data.Amount)
newPrice, err := types.CalcPriceWithBridgingFee(transferTotal, newFeeInt, bridgingFeeMultiplier)
if err != nil {
return nil, err
}

denom := demandOrder.Price[0].Denom
demandOrder.Fee = sdk.NewCoins(sdk.NewCoin(denom, newFeeInt))
demandOrder.Price = sdk.NewCoins(sdk.NewCoin(denom, newPrice))

err = m.SetDemandOrder(ctx, demandOrder)
if err != nil {
return nil, err
}

return &types.MsgUpdateDemandOrderResponse{}, nil
}

func (m msgServer) GetOutstandingOrder(ctx sdk.Context, orderId string) (*types.DemandOrder, error) {
// Check that the order exists in status PENDING
demandOrder, err := m.GetDemandOrder(ctx, commontypes.Status_PENDING, orderId)
if err != nil {
return nil, err
}

return demandOrder, demandOrder.ValidateOrderIsOutstanding()
}
Loading

0 comments on commit 6721d98

Please sign in to comment.