diff --git a/README.md b/README.md index 3ccd7ce..daffb29 100755 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ +# Modifications +This is a fork of the Cosmos DEX for the Cosmos DeFi hackathon at SF Blockchain Week 2019, for which we came 4th overall out of 50 or so teams. This is one of the 2 submitted projects, which adds the ability to do things in the future to a DEX, which is part of my overall idea for 'Active Smart Contracts' (ASCs). The general problem being solved is that blockchains are passive and require a user to submit a transaction every time they want to do something on-chain. In terms of trading, this requires a user to be online 24/7 or have a bot trading for them because trades almost always depend on certain conditions: limit orders, stop losses etc. This lack of basic trading features puts DEXs far behind CEXs in terms of usability and is severely inhibiting the growth of the DEX space. A user needs to be able to set some conditions for some actions (if the price of X token falls below Y value, buy etc) in the future, go offline, and have those actions executed under the right conditions. + +The solution to allow actions on a blockchain to happen in the future is a cryptoeconomic one - essentially incentivising others to submit your transactions under certain conditions for you. In Cosmos, however, you can 'register' events to happen at the end of every block. I added a new type of action here, a stop loss, which constantly checks the registered stop losses to see if any of the conditions are true which enable them to be executed. + +The rest of this README is the original. + + + + + + # Disclaimer The code hosted in this repository is a **technology preview** and is suitable for **demo purposes only**. The features provided by this draft implementation are not meant to be functionally complete and are not suitable for deployment in production. diff --git a/app/app.go b/app/app.go index 54ef431..de3fa84 100755 --- a/app/app.go +++ b/app/app.go @@ -148,7 +148,7 @@ func NewDexApp( bam.MainStoreKey, auth.StoreKey, staking.StoreKey, supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey, params.StoreKey, assettypes.StoreKey, markettypes.StoreKey, - ordertypes.StoreKey, + ordertypes.StoreKey, ordertypes.LastPriceKey, ) tkeys := sdk.NewTransientStoreKeys(staking.TStoreKey, params.TStoreKey) @@ -199,6 +199,7 @@ func NewDexApp( app.MarketKeeper, app.AssetKeeper, keys[ordertypes.StoreKey], + keys[ordertypes.LastPriceKey], queue, app.Cdc, ) diff --git a/x/order/client/cli/cli.go b/x/order/client/cli/cli.go index 0d1f9ae..3879a59 100755 --- a/x/order/client/cli/cli.go +++ b/x/order/client/cli/cli.go @@ -24,6 +24,7 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { Short: "manages orders", } txCmd.AddCommand(client.PostCommands( + GetCmdStop(cdc), GetCmdPost(cdc), GetCmdCancel(cdc), )...) diff --git a/x/order/client/cli/tx.go b/x/order/client/cli/tx.go index c85e163..9c23ab1 100755 --- a/x/order/client/cli/tx.go +++ b/x/order/client/cli/tx.go @@ -2,6 +2,7 @@ package cli import ( "errors" + "fmt" "math" "strconv" "strings" @@ -17,6 +18,68 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +func GetCmdStop(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "stop [market-id] [direction] [price] [quantity] [time-in-force-blocks] [init-price] [relayer-address-hex] [relayer-fee]", + Short: "posts an order under certain price conditions, i.e. a stop-loss or stop-buy", + Args: cobra.ExactArgs(8), + RunE: func(cmd *cobra.Command, args []string) error { + ctx, _, err := cliutil.BuildEnsuredCtx(cdc) + if err != nil { + return err + } + + marketID := store.NewEntityIDFromString(args[0]) + var direction matcheng.Direction + dirArg := strings.ToLower(args[1]) + if dirArg == "bid" { + direction = matcheng.Bid + } else if dirArg == "ask" { + direction = matcheng.Ask + } else { + return errors.New("invalid direction") + } + + price, err := sdk.ParseUint(args[2]) + if err != nil { + return err + } + quantity, err := sdk.ParseUint(args[3]) + if err != nil { + return err + } + tif, err := strconv.ParseUint(args[4], 10, 64) + if err != nil { + return err + } + if tif > math.MaxUint16 { + return errors.New("time in force too large") + } + + initPrice, err := sdk.ParseUint(args[5]) + if err != nil { + return err + } + relayedAddress, err := sdk.AccAddressFromHex(args[6]) + if err != nil { + return err + } + err = sdk.VerifyAddressFormat(relayedAddress) + if err != nil { + return errors.New("invalid address format") + } + relayerFee, err := sdk.ParseCoins(args[7]) + if err != nil { + return err + } + + msg := types.NewMsgStop(ctx.GetFromAddress(), marketID, direction, price, quantity, uint16(tif), initPrice, relayedAddress, relayerFee) + fmt.Println(msg) + return nil + }, + } +} + func GetCmdPost(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ Use: "post [market-id] [direction] [price] [quantity] [time-in-force-blocks]", diff --git a/x/order/handler.go b/x/order/handler.go index d171891..a7cc7fd 100755 --- a/x/order/handler.go +++ b/x/order/handler.go @@ -15,6 +15,8 @@ var logger = log.WithModule("order") func NewHandler(keeper Keeper) sdk.Handler { return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { switch msg := msg.(type) { + case types.MsgStop: + return handleMsgStop(ctx, keeper, msg) case types.MsgPost: return handleMsgPost(ctx, keeper, msg) case types.MsgCancel: @@ -25,6 +27,59 @@ func NewHandler(keeper Keeper) sdk.Handler { } } +func handleMsgStop(ctx sdk.Context, keeper Keeper, msg types.MsgStop) sdk.Result { + currentPrice := keeper.GetPrice(ctx, msg.Post.MarketID) + // Shouldn't be triggered in these ranges + if msg.Post.Price.GT(msg.InitPrice) && currentPrice.LT(msg.Post.Price) { + return sdk.Result{ + Log: fmt.Sprintf("current price not in triggering range"), + } + } + if msg.Post.Price.LT(msg.InitPrice) && currentPrice.GT(msg.Post.Price) { + return sdk.Result{ + Log: fmt.Sprintf("current price not in triggering range"), + } + } + + order, err := keeper.Post( + ctx, + msg.Post.Owner, + msg.Post.MarketID, + msg.Post.Direction, + msg.Post.Price, + msg.Post.Quantity, + msg.Post.TimeInForce, + ) + if err != nil { + return err.Result() + } + logger.Info( + "stop order", + "id", order.ID.String(), + "market_id", order.MarketID.String(), + "price", order.Price.String(), + "quantity", order.Quantity.String(), + "direction", order.Direction.String(), + ) + + err = keeper.bankKeeper.SendCoins(ctx, msg.Post.Owner, msg.Relayer, msg.RelayFee) + if err == nil { + return err.Result() + } + logger.Info( + "stop order", + "id", order.ID.String(), + "market_id", order.MarketID.String(), + "price", order.Price.String(), + "quantity", order.Quantity.String(), + "direction", order.Direction.String(), + ) + + return sdk.Result{ + Log: fmt.Sprintf("order_id:%s", order.ID), + } +} + func handleMsgPost(ctx sdk.Context, keeper Keeper, msg types.MsgPost) sdk.Result { order, err := keeper.Post( ctx, diff --git a/x/order/keeper.go b/x/order/keeper.go index a67482e..e40997d 100755 --- a/x/order/keeper.go +++ b/x/order/keeper.go @@ -26,16 +26,18 @@ type Keeper struct { marketKeeper market.Keeper assetKeeper asset.Keeper storeKey sdk.StoreKey + latestPrices sdk.StoreKey queue types.Backend cdc *codec.Codec } -func NewKeeper(bk bank.Keeper, mk market.Keeper, ak asset.Keeper, sk sdk.StoreKey, queue types.Backend, cdc *codec.Codec) Keeper { +func NewKeeper(bk bank.Keeper, mk market.Keeper, ak asset.Keeper, sk, lp sdk.StoreKey, queue types.Backend, cdc *codec.Codec) Keeper { return Keeper{ bankKeeper: bk, marketKeeper: mk, assetKeeper: ak, storeKey: sk, + latestPrices: lp, queue: queue, cdc: cdc, } @@ -205,3 +207,18 @@ func (k Keeper) doIterator(iter sdk.Iterator, cb IteratorCB) { func orderKey(id store.EntityID) []byte { return store.PrefixKeyString(valKey, id.Bytes()) } + +func (k Keeper) SetPrice(ctx sdk.Context, mID store.EntityID, price sdk.Uint) { + store := ctx.KVStore(k.latestPrices) + mn := mID.String() + stringPrice := price.String() + store.Set([]byte(mn), []byte(stringPrice)) +} + +func (k Keeper) GetPrice(ctx sdk.Context, mID store.EntityID) sdk.Uint { + store := ctx.KVStore(k.latestPrices) + mn := mID.String() + currentPriceBytes := store.Get([]byte(mn)) + currentPriceString := string(currentPriceBytes) + return sdk.NewUintFromString(currentPriceString) +} diff --git a/x/order/types/codec.go b/x/order/types/codec.go index 3bea186..68d15c7 100755 --- a/x/order/types/codec.go +++ b/x/order/types/codec.go @@ -5,6 +5,7 @@ import "github.com/cosmos/cosmos-sdk/codec" var ModuleCdc = codec.New() func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(MsgStop{}, "order/Stop", nil) cdc.RegisterConcrete(MsgPost{}, "order/Post", nil) cdc.RegisterConcrete(MsgCancel{}, "order/Cancel", nil) } diff --git a/x/order/types/msgs.go b/x/order/types/msgs.go index b24aed2..31423c7 100755 --- a/x/order/types/msgs.go +++ b/x/order/types/msgs.go @@ -8,6 +8,74 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +type MsgStop struct { + Post MsgPost + // The price at the time the tx is signed. + // Needed to determine which direction the price travels in order to + // know whether to trigger above or below MsgPost.Price + InitPrice sdk.Uint + Relayer sdk.AccAddress + RelayFee sdk.Coins +} + +func NewMsgStop(owner sdk.AccAddress, marketID store.EntityID, direction matcheng.Direction, price sdk.Uint, quantity sdk.Uint, tif uint16, initPrice sdk.Uint, relayer sdk.AccAddress, relayFee sdk.Coins) MsgStop { + return MsgStop{ + Post: MsgPost{ + Owner: owner, + MarketID: marketID, + Direction: direction, + Price: price, + Quantity: quantity, + TimeInForce: tif, + }, + InitPrice: initPrice, + Relayer: relayer, + RelayFee: relayFee, + } +} + +func (msg MsgStop) Route() string { + return "order" +} + +func (msg MsgStop) Type() string { + return "stop" +} + +func (msg MsgStop) ValidateBasic() sdk.Error { + // Don't need to check a bool like Buy because t & f should be able to be used here? + if !msg.Post.MarketID.IsDefined() { + return sdk.ErrUnauthorized("invalid market ID") + } + if msg.Post.Price.IsZero() { + return sdk.ErrInvalidCoins("price cannot be zero") + } + if msg.Post.Quantity.IsZero() { + return sdk.ErrInvalidCoins("quantity cannot be zero") + } + if msg.Post.TimeInForce == 0 { + return sdk.ErrInternal("time in force cannot be zero") + } + if msg.Post.TimeInForce > MaxTimeInForce { + return sdk.ErrInternal("time in force cannot be larger than 600") + } + if msg.InitPrice.IsZero() { + return sdk.ErrInvalidCoins("pastPrice cannot be zero") + } + if msg.RelayFee.IsZero() { + return sdk.ErrInvalidCoins("relayFee cannot be zero") + } + return nil +} + +func (msg MsgStop) GetSignBytes() []byte { + return serde.MustMarshalSortedJSON(msg) +} + +func (msg MsgStop) GetSigners() []sdk.AccAddress { + return msg.Post.GetSigners() +} + type MsgPost struct { Owner sdk.AccAddress MarketID store.EntityID diff --git a/x/order/types/types.go b/x/order/types/types.go index 8134cd2..abfd6e4 100755 --- a/x/order/types/types.go +++ b/x/order/types/types.go @@ -8,9 +8,10 @@ import ( ) const ( - ModuleName = "order" - RouterKey = ModuleName - StoreKey = ModuleName + ModuleName = "order" + RouterKey = ModuleName + StoreKey = ModuleName + LastPriceKey = "last_price" ) const MaxTimeInForce = 600