Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

imp(dymns): patch x/dymns follow audit report #1265

Merged
merged 22 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/keepers/keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,6 @@ func (a *AppKeepers) SetupHooks() {
a.IncentivesKeeper.Hooks(),
a.TxFeesKeeper.Hooks(),
a.DelayedAckKeeper.GetEpochHooks(),
a.DymNSKeeper.GetEpochHooks(),
a.RollappKeeper.GetEpochHooks(),
),
)
Expand Down
19 changes: 2 additions & 17 deletions proto/dymensionxyz/dymension/dymns/market.proto
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ option go_package = "github.com/dymensionxyz/dymension/v3/x/dymns/types";
// SellOrder defines a sell order, placed by owner, to sell a Dym-Name/Alias.
// Sell-Order has an expiry date.
// After expiry date, if no one has placed a bid, this Sell-Order will be closed, no change.
// If there is a bid, the highest bid will win, and the Dym-Name/Alias ownership will be transferred to the winner.
// If the bid matches the sell price, the Dym-Name/Alias ownership will be transferred to the bidder immediately.
// - If there is a bid, the highest bid will win, and the Dym-Name/Alias ownership will be transferred to the winner.
// - If the bid matches the sell price, the Dym-Name/Alias ownership will be transferred to the bidder immediately.
message SellOrder {
// asset_id is the Dym-Name/Alias being opened to be sold.
string asset_id = 1;
Expand All @@ -33,21 +33,6 @@ message SellOrder {
SellOrderBid highest_bid = 6;
}

// ActiveSellOrdersExpiration contains list of active SOs, store expiration date mapped by asset identity.
// Used by hook to find out expired SO instead of iterating through all records.
message ActiveSellOrdersExpiration {
repeated ActiveSellOrdersExpirationRecord records = 1 [(gogoproto.nullable) = false];
}

// ActiveSellOrdersExpirationRecord contains the expiration date of an active Sell-Order.
message ActiveSellOrdersExpirationRecord {
// asset_id is the Dym-Name/Alias being opened to be sold.
string asset_id = 1;

// expire_at is the last effective date of this Sell-Order.
int64 expire_at = 2;
}

// SellOrderBid defines a bid placed by an account on a Sell-Order.
message SellOrderBid {
// bidder is the account address of the account which placed the bid.
Expand Down
9 changes: 7 additions & 2 deletions proto/dymensionxyz/dymension/dymns/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ message PriceParams {
(gogoproto.moretags) = "yaml:\"min_offer_price\"",
(gogoproto.nullable) = false
];

// min_bid_increment_percent is the minimum percent raised compare to previous bid of a Sell-Order.
// The valid range from 0% to 100%, but capped at 10%.
uint32 min_bid_increment_percent = 6 [
(gogoproto.moretags) = "yaml:\"min_bid_increment_percent\""
];
}

// ChainsParams defines setting for prioritized aliases mapping.
Expand Down Expand Up @@ -127,7 +133,6 @@ message MiscParams {
bool enable_trading_name = 4;

// enable_trading_alias is the flag to enable trading of Alias.
// To be used in the future when Alias trading implementation is ready
// or disable trading of Alias when needed.
// To be used to stop trading of Alias when needed.
bool enable_trading_alias = 5;
}
22 changes: 22 additions & 0 deletions proto/dymensionxyz/dymension/dymns/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ service Msg {
// This will stop the advertisement and remove the Dym-Name/Alias sale from the market.
// Can only be performed if no one has placed a bid on the asset.
rpc CancelSellOrder(MsgCancelSellOrder) returns (MsgCancelSellOrderResponse) {}
// CompleteSellOrder is message handler,
// handles Sell-Order completion action, can be performed by either asset owner or the person who placed the highest bid.
// Can only be performed when Sell-Order expired and has a bid placed.
// If the asset was expired or prohibited trading, bid placed will be force to return to the bidder, ownership will not be transferred.
rpc CompleteSellOrder(MsgCompleteSellOrder) returns (MsgCompleteSellOrderResponse) {}
// PurchaseOrder is message handler,
// handles purchasing a Dym-Name/Alias from a Sell-Order, performed by the buyer.
rpc PurchaseOrder(MsgPurchaseOrder) returns (MsgPurchaseOrderResponse) {}
Expand Down Expand Up @@ -219,6 +224,23 @@ message MsgCancelSellOrder {
// MsgCancelSellOrderResponse defines the response for the Sell-Order cancellation.
message MsgCancelSellOrderResponse {}

// MsgCompleteSellOrder defines the message used for user to complete a Sell-Order.
message MsgCompleteSellOrder {
option (cosmos.msg.v1.signer) = "participant";

// asset_id is the Dym-Name/Alias about to perform Sell Order completion action.
string asset_id = 1;

// asset_type is the type of the asset of the order, is Dym-Name/Alias.
AssetType asset_type = 2;

// participant is the bech32-encoded address of either asset owner or highest bidder account.
string participant = 3;
}

// MsgCompleteSellOrderResponse defines the response for the Sell-Order completion.
message MsgCompleteSellOrderResponse {}

// MsgPurchaseOrder defines the message used for user to bid/purchase a Sell-Order.
message MsgPurchaseOrder {
option (cosmos.msg.v1.signer) = "buyer";
Expand Down
2 changes: 2 additions & 0 deletions x/dymns/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ func GetTxCmd() *cobra.Command {
NewUpdateDetailsTxCmd(),
NewPlaceDymNameSellOrderTxCmd(),
NewPlaceAliasSellOrderTxCmd(),
NewCancelSellOrderTxCmd(),
NewCompleteSellOrderTxCmd(),
NewPlaceBidOnDymNameOrderTxCmd(),
NewPlaceBidOnAliasOrderTxCmd(),
NewOfferBuyDymNameTxCmd(),
Expand Down
71 changes: 71 additions & 0 deletions x/dymns/client/cli/tx_cancel_sell_order.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package cli

import (
"fmt"
"strings"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/version"
dymnstypes "github.com/dymensionxyz/dymension/v3/x/dymns/types"
dymnsutils "github.com/dymensionxyz/dymension/v3/x/dymns/utils"
"github.com/spf13/cobra"
)

// NewCancelSellOrderTxCmd is the CLI command for close a Sell-Order.
func NewCancelSellOrderTxCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "cancel-sell-order [Name/Alias/Handle] [myname/x]",
Aliases: []string{"cancel-so"},
Short: "Cancel a sell-order (only when no bid placed)",
Example: fmt.Sprintf(
`$ %s tx %s cancel-sell-order name myname --%s owner
$ %s tx %s cancel-sell-order alias x --%s owner`,
version.AppName, dymnstypes.ModuleName, flags.FlagFrom,
version.AppName, dymnstypes.ModuleName, flags.FlagFrom,
),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) (err error) {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

target := args[1]
var assetType dymnstypes.AssetType

switch strings.ToLower(args[0]) {
case "name", "dym-name", "dymname", "n":
assetType = dymnstypes.TypeName
if !dymnsutils.IsValidDymName(target) {
return fmt.Errorf("input is not a valid Dym-Name: %s", target)
}
case "alias", "handle", "handles", "a":
assetType = dymnstypes.TypeAlias
if !dymnsutils.IsValidAlias(target) {
return fmt.Errorf("input is not a valid Alias: %s", target)
}
default:
return fmt.Errorf("invalid asset type: %s, must be 'Name' or 'Alias'/'Handle'", args[0])
}

owner := clientCtx.GetFromAddress().String()
if owner == "" {
return fmt.Errorf("flag --%s is required", flags.FlagFrom)
}

msg := &dymnstypes.MsgCancelSellOrder{
AssetId: target,
AssetType: assetType,
Owner: owner,
}

return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)

return cmd
}
72 changes: 72 additions & 0 deletions x/dymns/client/cli/tx_complete_sell_order.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package cli

import (
"fmt"
"strings"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/version"
dymnstypes "github.com/dymensionxyz/dymension/v3/x/dymns/types"
dymnsutils "github.com/dymensionxyz/dymension/v3/x/dymns/utils"
"github.com/spf13/cobra"
)

// NewCompleteSellOrderTxCmd is the CLI command for completing a Sell-Order.
func NewCompleteSellOrderTxCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "complete-sell-order [Name/Alias/Handle] [myname/x]",
Aliases: []string{"complete-so"},
Short: "Complete a sell-order (must be expired and has at least one bid)",
Long: "Request to complete a sell-order (must be expired and has at least one bid). Can be submitted by either the owner or the highest bidder.",
Example: fmt.Sprintf(
`$ %s tx %s complete-sell-order name myname --%s owner/bidder
$ %s tx %s complete-sell-order alias x --%s owner/bidder`,
version.AppName, dymnstypes.ModuleName, flags.FlagFrom,
version.AppName, dymnstypes.ModuleName, flags.FlagFrom,
),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) (err error) {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

target := args[1]
var assetType dymnstypes.AssetType

switch strings.ToLower(args[0]) {
case "name", "dym-name", "dymname", "n":
assetType = dymnstypes.TypeName
if !dymnsutils.IsValidDymName(target) {
return fmt.Errorf("input is not a valid Dym-Name: %s", target)
}
case "alias", "handle", "handles", "a":
assetType = dymnstypes.TypeAlias
if !dymnsutils.IsValidAlias(target) {
return fmt.Errorf("input is not a valid Alias: %s", target)
}
default:
return fmt.Errorf("invalid asset type: %s, must be 'Name' or 'Alias'/'Handle'", args[0])
}

participant := clientCtx.GetFromAddress().String()
if participant == "" {
return fmt.Errorf("flag --%s is required", flags.FlagFrom)
}

msg := &dymnstypes.MsgCompleteSellOrder{
AssetId: target,
AssetType: assetType,
Participant: participant,
}

return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)

return cmd
}
4 changes: 2 additions & 2 deletions x/dymns/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ func TestExportThenInitGenesis(t *testing.T) {
// Init genesis state

genState.Params.Misc.EndEpochHookIdentifier = "week" // Change the epoch identifier to test if it is imported correctly
genState.Params.Misc.SellOrderDuration = 9999 * time.Hour
genState.Params.Misc.SellOrderDuration = 6 * 24 * time.Hour

newDymNsKeeper, newBankKeeper, newRollAppKeeper, newCtx := testkeeper.DymNSKeeper(t)
newCtx = newCtx.WithBlockTime(now)
Expand All @@ -290,7 +290,7 @@ func TestExportThenInitGenesis(t *testing.T) {
importedParams := newDymNsKeeper.GetParams(newCtx)
require.Equal(t, genState.Params, importedParams)
require.Equal(t, "week", importedParams.Misc.EndEpochHookIdentifier)
require.Equal(t, 9999*time.Hour, importedParams.Misc.SellOrderDuration)
require.Equal(t, 6*24*time.Hour, importedParams.Misc.SellOrderDuration)
})

t.Run("Dym-Names should be imported correctly", func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion x/dymns/keeper/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func (k Keeper) GetEffectiveAliasesByChainId(ctx sdk.Context, chainId string) []
aliasesOfRollApp := k.GetAliasesOfRollAppId(ctx, chainId)

// If the chain-id is a RollApp, must exclude the aliases which being reserved in params.
// Please read the `processActiveAliasSellOrders` method (hooks.go) for more information.
// Please read the `processCompleteSellOrderWithAssetTypeAlias` method (msg_server_complete_sell_order.go) for more information.
reservedAliases := k.GetAllAliasAndChainIdInParams(ctx)
aliasesOfRollApp = slices.DeleteFunc(aliasesOfRollApp, func(a string) bool {
_, found := reservedAliases[a]
Expand Down
11 changes: 9 additions & 2 deletions x/dymns/keeper/dym_name.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,15 @@ func (k Keeper) GetAllDymNames(ctx sdk.Context) (list []dymnstypes.DymName) {

// PruneDymName removes a Dym-Name from the KVStore, as well as all related records.
func (k Keeper) PruneDymName(ctx sdk.Context, name string) error {
// remove SO (force, ignore active SO)
k.DeleteSellOrder(ctx, name, dymnstypes.TypeName)
// Force removing any existing SO.
if so := k.GetSellOrder(ctx, name, dymnstypes.TypeName); so != nil {
if so.HighestBid != nil {
if err := k.RefundBid(ctx, *so.HighestBid, dymnstypes.TypeName); err != nil {
return err
}
}
k.DeleteSellOrder(ctx, name, dymnstypes.TypeName)
}

dymName := k.GetDymName(ctx, name)
if dymName == nil {
Expand Down
35 changes: 21 additions & 14 deletions x/dymns/keeper/generic_reverse_lookup.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package keeper

import (
"slices"
"strings"

sdk "github.com/cosmos/cosmos-sdk/types"
dymnstypes "github.com/dymensionxyz/dymension/v3/x/dymns/types"
)

// GenericAddReverseLookupRecord is a utility method that help to add a reverse lookup record.
Expand All @@ -12,26 +14,22 @@ func (k Keeper) GenericAddReverseLookupRecord(
marshaller func([]string) []byte,
unMarshaller func([]byte) []string,
) error {
modifiedRecord := dymnstypes.StringList{
newElement,
}
var modifiedRecord []string

store := ctx.KVStore(k.storeKey)
bz := store.Get(key)
if bz != nil {
existingRecord := unMarshaller(bz)

modifiedRecord = dymnstypes.StringList(existingRecord).Combine(
modifiedRecord,
)

if len(modifiedRecord) == len(existingRecord) {
// no new mapping to add
if slices.Contains(existingRecord, newElement) {
// already exist
return nil
}
}

modifiedRecord = modifiedRecord.Sort()
modifiedRecord = append(existingRecord, newElement)
} else {
modifiedRecord = []string{newElement}
}

bz = marshaller(modifiedRecord)
store.Set(key, bz)
Expand Down Expand Up @@ -69,16 +67,25 @@ func (k Keeper) GenericRemoveReverseLookupRecord(
}

existingRecord := unMarshaller(bz)
modifiedRecord := slices.DeleteFunc(existingRecord, func(r string) bool {
return r == elementToRemove
})

modifiedRecord := dymnstypes.StringList(existingRecord).Exclude([]string{elementToRemove})
if len(existingRecord) == len(modifiedRecord) {
// not found
return nil
}

if len(modifiedRecord) == 0 {
// no more, remove record
store.Delete(key)
return nil
}

modifiedRecord = modifiedRecord.Sort()
// just for safety, sort the records
slices.SortFunc(modifiedRecord, func(a, b string) int {
return strings.Compare(a, b)
})

bz = marshaller(modifiedRecord)
store.Set(key, bz)
Expand Down
Loading
Loading