Merge pull request #271 from Kava-Labs/kd-migrate-modules

feat: migrate modules from kava-devnet
This commit is contained in:
Kevin Davis 2019-11-25 16:27:12 -05:00 committed by GitHub
commit ab913ceadb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 7655 additions and 0 deletions

23
x/auction/abci.go Normal file
View File

@ -0,0 +1,23 @@
package auction
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// EndBlocker runs at the end of every block.
func EndBlocker(ctx sdk.Context, k Keeper) {
// get an iterator of expired auctions
expiredAuctions := k.GetQueueIterator(ctx, EndTime(ctx.BlockHeight()))
defer expiredAuctions.Close()
// loop through and close them - distribute funds, delete from store (and queue)
for ; expiredAuctions.Valid(); expiredAuctions.Next() {
auctionID := k.DecodeAuctionID(ctx, expiredAuctions.Value())
err := k.CloseAuction(ctx, auctionID)
if err != nil {
panic(err) // TODO how should errors be handled here?
}
}
}

28
x/auction/abci_test.go Normal file
View File

@ -0,0 +1,28 @@
package auction
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
)
func TestKeeper_EndBlocker(t *testing.T) {
// setup keeper and auction
mapp, keeper, addresses, _ := setUpMockApp()
header := abci.Header{Height: mapp.LastBlockHeight() + 1}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header)
seller := addresses[0]
keeper.StartForwardAuction(ctx, seller, sdk.NewInt64Coin("token1", 20), sdk.NewInt64Coin("token2", 0))
// run the endblocker, simulating a block height after auction expiry
expiryBlock := ctx.BlockHeight() + int64(DefaultMaxAuctionDuration)
EndBlocker(ctx.WithBlockHeight(expiryBlock), keeper)
// check auction has been closed
_, found := keeper.GetAuction(ctx, 0)
require.False(t, found)
}

66
x/auction/alias.go Normal file
View File

@ -0,0 +1,66 @@
// nolint
// autogenerated code using github.com/rigelrozanski/multitool
// aliases generated for the following subdirectories:
// ALIASGEN: github.com/kava-labs/kava/x/auction/types/
// ALIASGEN: github.com/kava-labs/kava/x/auction/keeper/
package auction
import (
"github.com/kava-labs/kava/x/auction/keeper"
"github.com/kava-labs/kava/x/auction/types"
)
const (
ModuleName = types.ModuleName
StoreKey = types.StoreKey
RouterKey = types.RouterKey
DefaultParamspace = types.DefaultParamspace
DefaultMaxAuctionDuration = types.DefaultMaxAuctionDuration
DefaultMaxBidDuration = types.DefaultMaxBidDuration
DefaultStartingAuctionID = types.DefaultStartingAuctionID
QueryGetAuction = types.QueryGetAuction
)
var (
// functions aliases
NewIDFromString = types.NewIDFromString
NewBaseAuction = types.NewBaseAuction
NewForwardAuction = types.NewForwardAuction
NewReverseAuction = types.NewReverseAuction
NewForwardReverseAuction = types.NewForwardReverseAuction
RegisterCodec = types.RegisterCodec
NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState
ValidateGenesis = types.ValidateGenesis
NewMsgPlaceBid = types.NewMsgPlaceBid
NewAuctionParams = types.NewAuctionParams
DefaultAuctionParams = types.DefaultAuctionParams
ParamKeyTable = types.ParamKeyTable
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
// variable aliases
ModuleCdc = types.ModuleCdc
KeyAuctionBidDuration = types.KeyAuctionBidDuration
KeyAuctionDuration = types.KeyAuctionDuration
KeyAuctionStartingID = types.KeyAuctionStartingID
)
type (
Auction = types.Auction
BaseAuction = types.BaseAuction
ID = types.ID
EndTime = types.EndTime
BankInput = types.BankInput
BankOutput = types.BankOutput
ForwardAuction = types.ForwardAuction
ReverseAuction = types.ReverseAuction
ForwardReverseAuction = types.ForwardReverseAuction
BankKeeper = types.BankKeeper
GenesisAuctions = types.GenesisAuctions
GenesisState = types.GenesisState
MsgPlaceBid = types.MsgPlaceBid
AuctionParams = types.AuctionParams
QueryResAuctions = types.QueryResAuctions
Keeper = keeper.Keeper
)

171
x/auction/app_test.go Normal file
View File

@ -0,0 +1,171 @@
package auction
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/mock"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
)
// TestApp contans several basic integration tests of creating an auction, placing a bid, and the auction closing.
func TestApp_ForwardAuction(t *testing.T) {
// Setup
mapp, keeper, addresses, privKeys := setUpMockApp()
seller := addresses[0]
//sellerKey := privKeys[0]
buyer := addresses[1]
buyerKey := privKeys[1]
// Create a block where an auction is started (lot: 20 t1, initialBid: 0 t2)
header := abci.Header{Height: mapp.LastBlockHeight() + 1}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header) // make sure first arg is false, otherwise no db writes
keeper.StartForwardAuction(ctx, seller, sdk.NewInt64Coin("token1", 20), sdk.NewInt64Coin("token2", 0)) // lot, initialBid
mapp.EndBlock(abci.RequestEndBlock{})
mapp.Commit()
// Check seller's coins have decreased
mock.CheckBalance(t, mapp, seller, sdk.NewCoins(sdk.NewInt64Coin("token1", 80), sdk.NewInt64Coin("token2", 100)))
// Deliver a block that contains a PlaceBid tx (bid: 10 t2, lot: same as starting)
msgs := []sdk.Msg{NewMsgPlaceBid(0, buyer, sdk.NewInt64Coin("token2", 10), sdk.NewInt64Coin("token1", 20))} // bid, lot
header = abci.Header{Height: mapp.LastBlockHeight() + 1}
mock.SignCheckDeliver(t, mapp.Cdc, mapp.BaseApp, header, msgs, []uint64{1}, []uint64{0}, true, true, buyerKey) // account number for the buyer account is 1
// Check buyer's coins have decreased
mock.CheckBalance(t, mapp, buyer, sdk.NewCoins(sdk.NewInt64Coin("token1", 100), sdk.NewInt64Coin("token2", 90)))
// Check seller's coins have increased
mock.CheckBalance(t, mapp, seller, sdk.NewCoins(sdk.NewInt64Coin("token1", 80), sdk.NewInt64Coin("token2", 110)))
// Deliver empty blocks until the auction should be closed (bid placed on block 3)
// TODO is there a way of skipping ahead? This takes a while and prints a lot.
for h := mapp.LastBlockHeight() + 1; h < int64(DefaultMaxBidDuration)+4; h++ {
mapp.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: h}})
mapp.EndBlock(abci.RequestEndBlock{Height: h})
mapp.Commit()
}
// Check buyer's coins increased
mock.CheckBalance(t, mapp, buyer, sdk.NewCoins(sdk.NewInt64Coin("token1", 120), sdk.NewInt64Coin("token2", 90)))
}
func TestApp_ReverseAuction(t *testing.T) {
// Setup
mapp, keeper, addresses, privKeys := setUpMockApp()
seller := addresses[0]
sellerKey := privKeys[0]
buyer := addresses[1]
//buyerKey := privKeys[1]
// Create a block where an auction is started
header := abci.Header{Height: mapp.LastBlockHeight() + 1}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header)
keeper.StartReverseAuction(ctx, buyer, sdk.NewInt64Coin("token1", 20), sdk.NewInt64Coin("token2", 99)) // buyer, bid, initialLot
mapp.EndBlock(abci.RequestEndBlock{})
mapp.Commit()
// Check buyer's coins have decreased
mock.CheckBalance(t, mapp, buyer, sdk.NewCoins(sdk.NewInt64Coin("token1", 100), sdk.NewInt64Coin("token2", 1)))
// Deliver a block that contains a PlaceBid tx
msgs := []sdk.Msg{NewMsgPlaceBid(0, seller, sdk.NewInt64Coin("token1", 20), sdk.NewInt64Coin("token2", 10))} // bid, lot
header = abci.Header{Height: mapp.LastBlockHeight() + 1}
mock.SignCheckDeliver(t, mapp.Cdc, mapp.BaseApp, header, msgs, []uint64{0}, []uint64{0}, true, true, sellerKey)
// Check seller's coins have decreased
mock.CheckBalance(t, mapp, seller, sdk.NewCoins(sdk.NewInt64Coin("token1", 80), sdk.NewInt64Coin("token2", 100)))
// Check buyer's coins have increased
mock.CheckBalance(t, mapp, buyer, sdk.NewCoins(sdk.NewInt64Coin("token1", 120), sdk.NewInt64Coin("token2", 90)))
// Deliver empty blocks until the auction should be closed (bid placed on block 3)
for h := mapp.LastBlockHeight() + 1; h < int64(DefaultMaxBidDuration)+4; h++ {
mapp.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: h}})
mapp.EndBlock(abci.RequestEndBlock{Height: h})
mapp.Commit()
}
// Check seller's coins increased
mock.CheckBalance(t, mapp, seller, sdk.NewCoins(sdk.NewInt64Coin("token1", 80), sdk.NewInt64Coin("token2", 110)))
}
func TestApp_ForwardReverseAuction(t *testing.T) {
// Setup
mapp, keeper, addresses, privKeys := setUpMockApp()
seller := addresses[0]
//sellerKey := privKeys[0]
buyer := addresses[1]
buyerKey := privKeys[1]
recipient := addresses[2]
// Create a block where an auction is started
header := abci.Header{Height: mapp.LastBlockHeight() + 1}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header)
keeper.StartForwardReverseAuction(ctx, seller, sdk.NewInt64Coin("token1", 20), sdk.NewInt64Coin("token2", 50), recipient) // seller, lot, maxBid, otherPerson
mapp.EndBlock(abci.RequestEndBlock{})
mapp.Commit()
// Check seller's coins have decreased
mock.CheckBalance(t, mapp, seller, sdk.NewCoins(sdk.NewInt64Coin("token1", 80), sdk.NewInt64Coin("token2", 100)))
// Deliver a block that contains a PlaceBid tx
msgs := []sdk.Msg{NewMsgPlaceBid(0, buyer, sdk.NewInt64Coin("token2", 50), sdk.NewInt64Coin("token1", 15))} // bid, lot
header = abci.Header{Height: mapp.LastBlockHeight() + 1}
mock.SignCheckDeliver(t, mapp.Cdc, mapp.BaseApp, header, msgs, []uint64{1}, []uint64{0}, true, true, buyerKey)
// Check bidder's coins have decreased
mock.CheckBalance(t, mapp, buyer, sdk.NewCoins(sdk.NewInt64Coin("token1", 100), sdk.NewInt64Coin("token2", 50)))
// Check seller's coins have increased
mock.CheckBalance(t, mapp, seller, sdk.NewCoins(sdk.NewInt64Coin("token1", 80), sdk.NewInt64Coin("token2", 150)))
// Check "recipient" has received coins
mock.CheckBalance(t, mapp, recipient, sdk.NewCoins(sdk.NewInt64Coin("token1", 105), sdk.NewInt64Coin("token2", 100)))
// Deliver empty blocks until the auction should be closed (bid placed on block 3)
for h := mapp.LastBlockHeight() + 1; h < int64(DefaultMaxBidDuration)+4; h++ {
mapp.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: h}})
mapp.EndBlock(abci.RequestEndBlock{Height: h})
mapp.Commit()
}
// Check buyer's coins increased
mock.CheckBalance(t, mapp, buyer, sdk.NewCoins(sdk.NewInt64Coin("token1", 115), sdk.NewInt64Coin("token2", 50)))
}
func setUpMockApp() (*mock.App, Keeper, []sdk.AccAddress, []crypto.PrivKey) {
// Create uninitialized mock app
mapp := mock.NewApp()
// Register codecs
RegisterCodec(mapp.Cdc)
// Create keepers
keyAuction := sdk.NewKVStoreKey("auction")
blacklistedAddrs := make(map[string]bool)
bankKeeper := bank.NewBaseKeeper(mapp.AccountKeeper, mapp.ParamsKeeper.Subspace(bank.DefaultParamspace), bank.DefaultCodespace, blacklistedAddrs)
auctionKeeper := NewKeeper(mapp.Cdc, bankKeeper, keyAuction, mapp.ParamsKeeper.Subspace(DefaultParamspace))
// Register routes
mapp.Router().AddRoute("auction", NewHandler(auctionKeeper))
// Add endblocker
mapp.SetEndBlocker(
func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock {
EndBlocker(ctx, auctionKeeper)
return abci.ResponseEndBlock{}
},
)
// Mount and load the stores
err := mapp.CompleteSetup(keyAuction)
if err != nil {
panic("mock app setup failed")
}
// Create a bunch (ie 10) of pre-funded accounts to use for tests
genAccs, addrs, _, privKeys := mock.CreateGenAccounts(10, sdk.NewCoins(sdk.NewInt64Coin("token1", 100), sdk.NewInt64Coin("token2", 100)))
mock.SetGenesis(mapp, genAccs)
return mapp, auctionKeeper, addrs, privKeys
}

View File

@ -0,0 +1,49 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/kava-labs/kava/x/auction/types"
)
// GetQueryCmd returns the cli query commands for this module
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
// Group nameservice queries under a subcommand
auctionQueryCmd := &cobra.Command{
Use: "auction",
Short: "Querying commands for the auction module",
}
auctionQueryCmd.AddCommand(client.GetCommands(
GetCmdGetAuctions(queryRoute, cdc),
)...)
return auctionQueryCmd
}
// GetCmdGetAuctions queries the auctions in the store
func GetCmdGetAuctions(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "getauctions",
Short: "get a list of active auctions",
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/getauctions", queryRoute), nil)
if err != nil {
fmt.Printf("error when getting auctions - %s", err)
return nil
}
var out types.QueryResAuctions
cdc.MustUnmarshalJSON(res, &out)
if len(out) == 0 {
out = append(out, "There are currently no auctions")
}
return cliCtx.PrintOutput(out)
},
}
}

View File

@ -0,0 +1,67 @@
package cli
import (
"fmt"
"github.com/kava-labs/kava/x/auction/types"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
)
// GetTxCmd returns the transaction commands for this module
// TODO: Tests, see: https://github.com/cosmos/cosmos-sdk/blob/18de630d0ae1887113e266982b51c2bf1f662edb/x/staking/client/cli/tx_test.go
func GetTxCmd(cdc *codec.Codec) *cobra.Command {
auctionTxCmd := &cobra.Command{
Use: "auction",
Short: "auction transactions subcommands",
}
auctionTxCmd.AddCommand(client.PostCommands(
GetCmdPlaceBid(cdc),
)...)
return auctionTxCmd
}
// GetCmdPlaceBid cli command for creating and modifying cdps.
func GetCmdPlaceBid(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "placebid [AuctionID] [Bidder] [Bid] [Lot]",
Short: "place a bid on an auction",
Args: cobra.ExactArgs(4),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
id, err := types.NewIDFromString(args[0])
if err != nil {
fmt.Printf("invalid auction id - %s \n", string(args[0]))
return err
}
bid, err := sdk.ParseCoin(args[2])
if err != nil {
fmt.Printf("invalid bid amount - %s \n", string(args[2]))
return err
}
lot, err := sdk.ParseCoin(args[3])
if err != nil {
fmt.Printf("invalid lot - %s \n", string(args[3]))
return err
}
msg := types.NewMsgPlaceBid(id, cliCtx.GetFromAddress(), bid, lot)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}

View File

@ -0,0 +1,30 @@
package rest
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/types/rest"
)
// r.HandleFunc(fmt.Sprintf("/auction/bid/{%s}/{%s}/{%s}/{%s}", restAuctionID, restBidder, restBid, restLot), bidHandlerFn(cdc, cliCtx)).Methods("PUT")
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(fmt.Sprintf("/auction/getauctions"), queryGetAuctionsHandlerFn(cliCtx)).Methods("GET")
}
func queryGetAuctionsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
res, height, err := cliCtx.QueryWithData("/custom/auction/getauctions", nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}

View File

@ -0,0 +1,13 @@
package rest
import (
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
)
// RegisterRoutes - Central function to define routes that get registered by the main application
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
registerQueryRoutes(cliCtx, r)
registerTxRoutes(cliCtx, r)
}

View File

@ -0,0 +1,86 @@
package rest
import (
"bytes"
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/auction/types"
)
type placeBidReq struct {
BaseReq rest.BaseReq `json:"base_req"`
AuctionID string `json:"auction_id"`
Bidder string `json:"bidder"`
Bid string `json:"bid"`
Lot string `json:"lot"`
}
const (
restAuctionID = "auction_id"
restBidder = "bidder"
restBid = "bid"
restLot = "lot"
)
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(
fmt.Sprintf("/auction/bid/{%s}/{%s}/{%s}/{%s}", restAuctionID, restBidder, restBid, restLot), bidHandlerFn(cliCtx)).Methods("PUT")
}
func bidHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req placeBidReq
vars := mux.Vars(r)
strAuctionID := vars[restAuctionID]
bechBidder := vars[restBidder]
strBid := vars[restBid]
strLot := vars[restLot]
auctionID, err := types.NewIDFromString(strAuctionID)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
bidder, err := sdk.AccAddressFromBech32(bechBidder)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
bid, err := sdk.ParseCoin(strBid)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
lot, err := sdk.ParseCoin(strLot)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
msg := types.NewMsgPlaceBid(auctionID, bidder, bid, lot)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
fromAddr, err := sdk.AccAddressFromBech32(req.BaseReq.From)
if !bytes.Equal(fromAddr, bidder) {
rest.WriteErrorResponse(w, http.StatusUnauthorized, "must bid from own address")
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
}
}

14
x/auction/doc.go Normal file
View File

@ -0,0 +1,14 @@
/*
Package auction is a module for creating generic auctions and allowing users to place bids until a timeout is reached.
TODO
- investigate when exactly auctions close and verify queue/endblocker logic is ok
- add more test cases, add stronger validation to user inputs
- add minimum bid increment
- decided whether to put auction params like default timeouts into the auctions themselves
- add docs
- Add constants for the module and route names
- user facing things like cli, rest, querier, tags
- custom error types, codespace
*/
package auction

30
x/auction/genesis.go Normal file
View File

@ -0,0 +1,30 @@
package auction
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// InitGenesis - initializes the store state from genesis data
func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) {
keeper.SetParams(ctx, data.AuctionParams)
for _, a := range data.Auctions {
keeper.SetAuction(ctx, a)
}
}
// ExportGenesis returns a GenesisState for a given context and keeper.
func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState {
params := keeper.GetParams(ctx)
var genAuctions GenesisAuctions
iterator := keeper.GetAuctionIterator(ctx)
for ; iterator.Valid(); iterator.Next() {
auction := keeper.DecodeAuction(ctx, iterator.Value())
genAuctions = append(genAuctions, auction)
}
return NewGenesisState(params, genAuctions)
}

30
x/auction/handler.go Normal file
View File

@ -0,0 +1,30 @@
package auction
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// NewHandler returns a function to handle all "auction" type messages.
func NewHandler(keeper Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) {
case MsgPlaceBid:
return handleMsgPlaceBid(ctx, keeper, msg)
default:
errMsg := fmt.Sprintf("Unrecognized auction msg type: %T", msg)
return sdk.ErrUnknownRequest(errMsg).Result()
}
}
}
func handleMsgPlaceBid(ctx sdk.Context, keeper Keeper, msg MsgPlaceBid) sdk.Result {
err := keeper.PlaceBid(ctx, msg.AuctionID, msg.Bidder, msg.Bid, msg.Lot)
if err != nil {
return err.Result()
}
return sdk.Result{}
}

314
x/auction/keeper/keeper.go Normal file
View File

@ -0,0 +1,314 @@
package keeper
import (
"bytes"
"fmt"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
"github.com/kava-labs/kava/x/auction/types"
)
type Keeper struct {
bankKeeper types.BankKeeper
storeKey sdk.StoreKey
cdc *codec.Codec
paramSubspace subspace.Subspace
// TODO codespace
}
// NewKeeper returns a new auction keeper.
func NewKeeper(cdc *codec.Codec, bankKeeper types.BankKeeper, storeKey sdk.StoreKey, paramstore subspace.Subspace) Keeper {
return Keeper{
bankKeeper: bankKeeper,
storeKey: storeKey,
cdc: cdc,
paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()),
}
}
// TODO these 3 start functions be combined or abstracted away?
// StartForwardAuction starts a normal auction. Known as flap in maker.
func (k Keeper) StartForwardAuction(ctx sdk.Context, seller sdk.AccAddress, lot sdk.Coin, initialBid sdk.Coin) (types.ID, sdk.Error) {
// create auction
auction, initiatorOutput := types.NewForwardAuction(seller, lot, initialBid, types.EndTime(ctx.BlockHeight())+types.DefaultMaxAuctionDuration)
// start the auction
auctionID, err := k.startAuction(ctx, &auction, initiatorOutput)
if err != nil {
return 0, err
}
return auctionID, nil
}
// StartReverseAuction starts an auction where sellers compete by offering decreasing prices. Known as flop in maker.
func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer sdk.AccAddress, bid sdk.Coin, initialLot sdk.Coin) (types.ID, sdk.Error) {
// create auction
auction, initiatorOutput := types.NewReverseAuction(buyer, bid, initialLot, types.EndTime(ctx.BlockHeight())+types.DefaultMaxAuctionDuration)
// start the auction
auctionID, err := k.startAuction(ctx, &auction, initiatorOutput)
if err != nil {
return 0, err
}
return auctionID, nil
}
// StartForwardReverseAuction starts an auction where bidders bid up to a maxBid, then switch to bidding down on price. Known as flip in maker.
func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller sdk.AccAddress, lot sdk.Coin, maxBid sdk.Coin, otherPerson sdk.AccAddress) (types.ID, sdk.Error) {
// create auction
initialBid := sdk.NewInt64Coin(maxBid.Denom, 0) // set the bidding coin denomination from the specified max bid
auction, initiatorOutput := types.NewForwardReverseAuction(seller, lot, initialBid, types.EndTime(ctx.BlockHeight())+types.DefaultMaxAuctionDuration, maxBid, otherPerson)
// start the auction
auctionID, err := k.startAuction(ctx, &auction, initiatorOutput)
if err != nil {
return 0, err
}
return auctionID, nil
}
func (k Keeper) startAuction(ctx sdk.Context, auction types.Auction, initiatorOutput types.BankOutput) (types.ID, sdk.Error) {
// get ID
newAuctionID, err := k.getNextAuctionID(ctx)
if err != nil {
return 0, err
}
// set ID
auction.SetID(newAuctionID)
// subtract coins from initiator
_, err = k.bankKeeper.SubtractCoins(ctx, initiatorOutput.Address, sdk.NewCoins(initiatorOutput.Coin))
if err != nil {
return 0, err
}
// store auction
k.SetAuction(ctx, auction)
k.incrementNextAuctionID(ctx)
return newAuctionID, nil
}
// PlaceBid places a bid on any auction.
func (k Keeper) PlaceBid(ctx sdk.Context, auctionID types.ID, bidder sdk.AccAddress, bid sdk.Coin, lot sdk.Coin) sdk.Error {
// get auction from store
auction, found := k.GetAuction(ctx, auctionID)
if !found {
return sdk.ErrInternal("auction doesn't exist")
}
// place bid
coinOutputs, coinInputs, err := auction.PlaceBid(types.EndTime(ctx.BlockHeight()), bidder, lot, bid) // update auction according to what type of auction it is // TODO should this return updated Auction to be more immutable?
if err != nil {
return err
}
// TODO this will fail if someone tries to update their bid without the full bid amount sitting in their account
// sub outputs
for _, output := range coinOutputs {
_, err = k.bankKeeper.SubtractCoins(ctx, output.Address, sdk.NewCoins(output.Coin)) // TODO handle errors properly here. All coin transfers should be atomic. InputOutputCoins may work
if err != nil {
panic(err)
}
}
// add inputs
for _, input := range coinInputs {
_, err = k.bankKeeper.AddCoins(ctx, input.Address, sdk.NewCoins(input.Coin)) // TODO errors
if err != nil {
panic(err)
}
}
// store updated auction
k.SetAuction(ctx, auction)
return nil
}
// CloseAuction closes an auction and distributes funds to the seller and highest bidder.
// TODO because this is called by the end blocker, it has to be valid for the duration of the EndTime block. Should maybe move this to a begin blocker?
func (k Keeper) CloseAuction(ctx sdk.Context, auctionID types.ID) sdk.Error {
// get the auction from the store
auction, found := k.GetAuction(ctx, auctionID)
if !found {
return sdk.ErrInternal("auction doesn't exist")
}
// error if auction has not reached the end time
if ctx.BlockHeight() < int64(auction.GetEndTime()) { // auctions close at the end of the block with blockheight == EndTime
return sdk.ErrInternal(fmt.Sprintf("auction can't be closed as curent block height (%v) is under auction end time (%v)", ctx.BlockHeight(), auction.GetEndTime()))
}
// payout to the last bidder
coinInput := auction.GetPayout()
_, err := k.bankKeeper.AddCoins(ctx, coinInput.Address, sdk.NewCoins(coinInput.Coin))
if err != nil {
return err
}
// delete auction from store (and queue)
k.deleteAuction(ctx, auctionID)
return nil
}
// ---------- Store methods ----------
// Use these to add and remove auction from the store.
// getNextAuctionID gets the next available global AuctionID
func (k Keeper) getNextAuctionID(ctx sdk.Context) (types.ID, sdk.Error) { // TODO don't need error return here
// get next ID from store
store := ctx.KVStore(k.storeKey)
bz := store.Get(k.getNextAuctionIDKey())
if bz == nil {
// if not found, set the id at 0
bz = k.cdc.MustMarshalBinaryLengthPrefixed(types.ID(0))
store.Set(k.getNextAuctionIDKey(), bz)
// TODO Why does the gov module set the id in genesis? :
//return 0, ErrInvalidGenesis(keeper.codespace, "InitialProposalID never set")
}
var auctionID types.ID
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &auctionID)
return auctionID, nil
}
// incrementNextAuctionID increments the global ID in the store by 1
func (k Keeper) incrementNextAuctionID(ctx sdk.Context) sdk.Error {
// get next ID from store
store := ctx.KVStore(k.storeKey)
bz := store.Get(k.getNextAuctionIDKey())
if bz == nil {
panic("initial auctionID never set in genesis")
//return 0, ErrInvalidGenesis(keeper.codespace, "InitialProposalID never set") // TODO is this needed? Why not just set it zero here?
}
var auctionID types.ID
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &auctionID)
// increment the stored next ID
bz = k.cdc.MustMarshalBinaryLengthPrefixed(auctionID + 1)
store.Set(k.getNextAuctionIDKey(), bz)
return nil
}
// SetAuction puts the auction into the database and adds it to the queue
// it overwrites any pre-existing auction with same ID
func (k Keeper) SetAuction(ctx sdk.Context, auction types.Auction) {
// remove the auction from the queue if it is already in there
existingAuction, found := k.GetAuction(ctx, auction.GetID())
if found {
k.removeFromQueue(ctx, existingAuction.GetEndTime(), existingAuction.GetID())
}
// store auction
store := ctx.KVStore(k.storeKey)
bz := k.cdc.MustMarshalBinaryLengthPrefixed(auction)
store.Set(k.getAuctionKey(auction.GetID()), bz)
// add to the queue
k.insertIntoQueue(ctx, auction.GetEndTime(), auction.GetID())
}
// getAuction gets an auction from the store by auctionID
func (k Keeper) GetAuction(ctx sdk.Context, auctionID types.ID) (types.Auction, bool) {
var auction types.Auction
store := ctx.KVStore(k.storeKey)
bz := store.Get(k.getAuctionKey(auctionID))
if bz == nil {
return auction, false // TODO what is the correct behavior when an auction is not found? gov module follows this pattern of returning a bool
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &auction)
return auction, true
}
// deleteAuction removes an auction from the store without any validation
func (k Keeper) deleteAuction(ctx sdk.Context, auctionID types.ID) {
// remove from queue
auction, found := k.GetAuction(ctx, auctionID)
if found {
k.removeFromQueue(ctx, auction.GetEndTime(), auctionID)
}
// delete auction
store := ctx.KVStore(k.storeKey)
store.Delete(k.getAuctionKey(auctionID))
}
// ---------- Queue and key methods ----------
// These are lower level function used by the store methods above.
func (k Keeper) getNextAuctionIDKey() []byte {
return []byte("nextAuctionID")
}
func (k Keeper) getAuctionKey(auctionID types.ID) []byte {
return []byte(fmt.Sprintf("auctions:%d", auctionID))
}
// Inserts a AuctionID into the queue at endTime
func (k Keeper) insertIntoQueue(ctx sdk.Context, endTime types.EndTime, auctionID types.ID) {
// get the store
store := ctx.KVStore(k.storeKey)
// marshal thing to be inserted
bz := k.cdc.MustMarshalBinaryLengthPrefixed(auctionID)
// store it
store.Set(
getQueueElementKey(endTime, auctionID),
bz,
)
}
// removes an auctionID from the queue
func (k Keeper) removeFromQueue(ctx sdk.Context, endTime types.EndTime, auctionID types.ID) {
store := ctx.KVStore(k.storeKey)
store.Delete(getQueueElementKey(endTime, auctionID))
}
// Returns an iterator for all the auctions in the queue that expire by endTime
func (k Keeper) GetQueueIterator(ctx sdk.Context, endTime types.EndTime) sdk.Iterator { // TODO rename to "getAuctionsByExpiry" ?
// get store
store := ctx.KVStore(k.storeKey)
// get an interator
return store.Iterator(
queueKeyPrefix, // start key
sdk.PrefixEndBytes(getQueueElementKeyPrefix(endTime)), // end key (apparently exclusive but tests suggested otherwise)
)
}
// GetAuctionIterator returns an iterator over all auctions in the store
func (k Keeper) GetAuctionIterator(ctx sdk.Context) sdk.Iterator {
store := ctx.KVStore(k.storeKey)
return sdk.KVStorePrefixIterator(store, nil)
}
var queueKeyPrefix = []byte("queue")
var keyDelimiter = []byte(":")
// Returns half a key for an auctionID in the queue, it missed the id off the end
func getQueueElementKeyPrefix(endTime types.EndTime) []byte {
return bytes.Join([][]byte{
queueKeyPrefix,
sdk.Uint64ToBigEndian(uint64(endTime)), // TODO check this gives correct ordering
}, keyDelimiter)
}
// Returns the key for an auctionID in the queue
func getQueueElementKey(endTime types.EndTime, auctionID types.ID) []byte {
return bytes.Join([][]byte{
queueKeyPrefix,
sdk.Uint64ToBigEndian(uint64(endTime)), // TODO check this gives correct ordering
sdk.Uint64ToBigEndian(uint64(auctionID)),
}, keyDelimiter)
}
// GetAuctionID returns the id from an input Auction
func (k Keeper) DecodeAuctionID(ctx sdk.Context, idBytes []byte) types.ID {
var auctionID types.ID
k.cdc.MustUnmarshalBinaryLengthPrefixed(idBytes, &auctionID)
return auctionID
}
func (k Keeper) DecodeAuction(ctx sdk.Context, auctionBytes []byte) types.Auction {
var auction types.Auction
k.cdc.MustUnmarshalBinaryBare(auctionBytes, &auction)
return auction
}

View File

@ -0,0 +1,88 @@
package keeper
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/auction/types"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
)
func TestKeeper_SetGetDeleteAuction(t *testing.T) {
// setup keeper, create auction
mapp, keeper, addresses, _ := setUpMockApp()
header := abci.Header{Height: mapp.LastBlockHeight() + 1}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header}) // Without this it panics about "invalid memory address or nil pointer dereference"
ctx := mapp.BaseApp.NewContext(false, header)
auction, _ := types.NewForwardAuction(addresses[0], sdk.NewInt64Coin("usdx", 100), sdk.NewInt64Coin("kava", 0), types.EndTime(1000))
id := types.ID(5)
auction.SetID(id)
// write and read from store
keeper.SetAuction(ctx, &auction)
readAuction, found := keeper.GetAuction(ctx, id)
// check before and after match
require.True(t, found)
require.Equal(t, &auction, readAuction)
t.Log(auction)
t.Log(readAuction.GetID())
// check auction is in queue
iter := keeper.GetQueueIterator(ctx, 100000)
require.Equal(t, 1, len(convertIteratorToSlice(keeper, iter)))
iter.Close()
// delete auction
keeper.deleteAuction(ctx, id)
// check auction does not exist
_, found = keeper.GetAuction(ctx, id)
require.False(t, found)
// check auction not in queue
iter = keeper.GetQueueIterator(ctx, 100000)
require.Equal(t, 0, len(convertIteratorToSlice(keeper, iter)))
iter.Close()
}
// TODO convert to table driven test with more test cases
func TestKeeper_ExpiredAuctionQueue(t *testing.T) {
// setup keeper
mapp, keeper, _, _ := setUpMockApp()
header := abci.Header{Height: mapp.LastBlockHeight() + 1}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header)
// create an example queue
type queue []struct {
endTime types.EndTime
auctionID types.ID
}
q := queue{{1000, 0}, {1300, 2}, {5200, 1}}
// write and read queue
for _, v := range q {
keeper.insertIntoQueue(ctx, v.endTime, v.auctionID)
}
iter := keeper.GetQueueIterator(ctx, 1000)
// check before and after match
i := 0
for ; iter.Valid(); iter.Next() {
var auctionID types.ID
keeper.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &auctionID)
require.Equal(t, q[i].auctionID, auctionID)
i++
}
}
func convertIteratorToSlice(keeper Keeper, iterator sdk.Iterator) []types.ID {
var queue []types.ID
for ; iterator.Valid(); iterator.Next() {
var auctionID types.ID
keeper.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &auctionID)
queue = append(queue, auctionID)
}
return queue
}

View File

@ -0,0 +1,17 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/auction/types"
)
// SetParams sets the auth module's parameters.
func (k Keeper) SetParams(ctx sdk.Context, params types.AuctionParams) {
k.paramSubspace.SetParamSet(ctx, &params)
}
// GetParams gets the auth module's parameters.
func (k Keeper) GetParams(ctx sdk.Context) (params types.AuctionParams) {
k.paramSubspace.GetParamSet(ctx, &params)
return
}

View File

@ -0,0 +1,40 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/auction/types"
abci "github.com/tendermint/tendermint/abci/types"
)
// NewQuerier is the module level router for state queries
func NewQuerier(keeper Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
switch path[0] {
case types.QueryGetAuction:
return queryAuctions(ctx, req, keeper)
default:
return nil, sdk.ErrUnknownRequest("unknown auction query endpoint")
}
}
}
func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) {
var AuctionsList types.QueryResAuctions
iterator := keeper.GetAuctionIterator(ctx)
for ; iterator.Valid(); iterator.Next() {
var auction types.Auction
keeper.cdc.MustUnmarshalBinaryBare(iterator.Value(), &auction)
AuctionsList = append(AuctionsList, auction.String())
}
bz, err2 := codec.MarshalJSONIndent(keeper.cdc, AuctionsList)
if err2 != nil {
panic("could not marshal result to JSON")
}
return bz, nil
}

View File

@ -0,0 +1,35 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/mock"
"github.com/kava-labs/kava/x/auction/types"
"github.com/tendermint/tendermint/crypto"
)
func setUpMockApp() (*mock.App, Keeper, []sdk.AccAddress, []crypto.PrivKey) {
// Create uninitialized mock app
mapp := mock.NewApp()
// Register codecs
types.RegisterCodec(mapp.Cdc)
// Create keepers
keyAuction := sdk.NewKVStoreKey("auction")
blacklistedAddrs := make(map[string]bool)
bankKeeper := bank.NewBaseKeeper(mapp.AccountKeeper, mapp.ParamsKeeper.Subspace(bank.DefaultParamspace), bank.DefaultCodespace, blacklistedAddrs)
auctionKeeper := NewKeeper(mapp.Cdc, bankKeeper, keyAuction, mapp.ParamsKeeper.Subspace(types.DefaultParamspace))
// Mount and load the stores
err := mapp.CompleteSetup(keyAuction)
if err != nil {
panic("mock app setup failed")
}
// Create a bunch (ie 10) of pre-funded accounts to use for tests
genAccs, addrs, _, privKeys := mock.CreateGenAccounts(10, sdk.NewCoins(sdk.NewInt64Coin("token1", 100), sdk.NewInt64Coin("token2", 100)))
mock.SetGenesis(mapp, genAccs)
return mapp, auctionKeeper, addrs, privKeys
}

131
x/auction/module.go Normal file
View File

@ -0,0 +1,131 @@
package auction
import (
"encoding/json"
"github.com/gorilla/mux"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/auction/client/cli"
"github.com/kava-labs/kava/x/auction/client/rest"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
)
// AppModuleBasic app module basics object
type AppModuleBasic struct{}
// Name get module name
func (AppModuleBasic) Name() string {
return ModuleName
}
// RegisterCodec register module codec
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
RegisterCodec(cdc)
}
// DefaultGenesis default genesis state
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
}
// ValidateGenesis performs genesis state validation for the auction module.
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var data GenesisState
err := ModuleCdc.UnmarshalJSON(bz, &data)
if err != nil {
return err
}
return ValidateGenesis(data)
}
// RegisterRESTRoutes registers the REST routes for the auction module.
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
rest.RegisterRoutes(ctx, rtr)
}
// GetTxCmd returns the root tx command for the auction module.
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetTxCmd(cdc)
}
// GetQueryCmd returns the root query command for the auction module.
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(StoreKey, cdc)
}
// AppModule app module type
type AppModule struct {
AppModuleBasic
keeper Keeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
}
}
// Name module name
func (AppModule) Name() string {
return ModuleName
}
// RegisterInvariants performs a no-op.
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// Route module message route name
func (AppModule) Route() string {
return ModuleName
}
// NewHandler module handler
func (am AppModule) NewHandler() sdk.Handler {
return NewHandler(am.keeper)
}
// QuerierRoute module querier route name
func (AppModule) QuerierRoute() string {
return ModuleName
}
// NewQuerierHandler module querier
func (am AppModule) NewQuerierHandler() sdk.Querier {
return NewQuerier(am.keeper)
}
// InitGenesis performs genesis initialization for the auction module. It returns
// no validator updates.
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState GenesisState
ModuleCdc.MustUnmarshalJSON(data, &genesisState)
InitGenesis(ctx, am.keeper, genesisState)
return []abci.ValidatorUpdate{}
}
// ExportGenesis module export genesis
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return ModuleCdc.MustMarshalJSON(gs)
}
// BeginBlock module begin-block
func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {}
// EndBlock module end-block
func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
EndBlocker(ctx, am.keeper)
return []abci.ValidatorUpdate{}
}

307
x/auction/types/auctions.go Normal file
View File

@ -0,0 +1,307 @@
package types
import (
"fmt"
"strconv"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Auction is an interface to several types of auction.
type Auction interface {
GetID() ID
SetID(ID)
PlaceBid(currentBlockHeight EndTime, bidder sdk.AccAddress, lot sdk.Coin, bid sdk.Coin) ([]BankOutput, []BankInput, sdk.Error)
GetEndTime() EndTime // auctions close at the end of the block with blockheight EndTime (ie bids placed in that block are valid)
GetPayout() BankInput
String() string
}
// BaseAuction type shared by all Auctions
type BaseAuction struct {
ID ID
Initiator sdk.AccAddress // Person who starts the auction. Giving away Lot (aka seller in a forward auction)
Lot sdk.Coin // Amount of coins up being given by initiator (FA - amount for sale by seller, RA - cost of good by buyer (bid))
Bidder sdk.AccAddress // Person who bids in the auction. Receiver of Lot. (aka buyer in forward auction, seller in RA)
Bid sdk.Coin // Amount of coins being given by the bidder (FA - bid, RA - amount being sold)
EndTime EndTime // Block height at which the auction closes. It closes at the end of this block
MaxEndTime EndTime // Maximum closing time. Auctions can close before this but never after.
}
// ID type for auction IDs
type ID uint64
// NewIDFromString generate new auction ID from a string
func NewIDFromString(s string) (ID, error) {
n, err := strconv.ParseUint(s, 10, 64) // copied from how the gov module rest handler's parse proposal IDs
if err != nil {
return 0, err
}
return ID(n), nil
}
// EndTime type for end time of auctions
type EndTime int64 // TODO rename to Blockheight or don't define custom type
// BankInput the input and output types from the bank module where used here. But they use sdk.Coins instad of sdk.Coin. So it caused a lot of type conversion as auction mainly uses sdk.Coin.
type BankInput struct {
Address sdk.AccAddress
Coin sdk.Coin
}
// BankOutput output type for auction bids
type BankOutput struct {
Address sdk.AccAddress
Coin sdk.Coin
}
// GetID getter for auction ID
func (a BaseAuction) GetID() ID { return a.ID }
// SetID setter for auction ID
func (a *BaseAuction) SetID(id ID) { a.ID = id }
// GetEndTime getter for auction end time
func (a BaseAuction) GetEndTime() EndTime { return a.EndTime }
// GetPayout implements Auction
func (a BaseAuction) GetPayout() BankInput {
return BankInput{a.Bidder, a.Lot}
}
// PlaceBid implements Auction
func (a *BaseAuction) PlaceBid(currentBlockHeight EndTime, bidder sdk.AccAddress, lot sdk.Coin, bid sdk.Coin) ([]BankOutput, []BankInput, sdk.Error) {
// TODO check lot size matches lot?
// check auction has not closed
if currentBlockHeight > a.EndTime {
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("auction has closed")
}
// check bid is greater than last bid
if !a.Bid.IsLT(bid) { // TODO add minimum bid size
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("bid not greater than last bid")
}
// calculate coin movements
outputs := []BankOutput{{bidder, bid}} // new bidder pays bid now
inputs := []BankInput{{a.Bidder, a.Bid}, {a.Initiator, bid.Sub(a.Bid)}} // old bidder is paid back, extra goes to seller
// update auction
a.Bidder = bidder
a.Bid = bid
// increment timeout // TODO into keeper?
a.EndTime = EndTime(min(int64(currentBlockHeight+DefaultMaxBidDuration), int64(a.MaxEndTime))) // TODO is there a better way to structure these types?
return outputs, inputs, nil
}
func (e EndTime) String() string {
return string(e)
}
func (a BaseAuction) String() string {
return fmt.Sprintf(`Auction %d:
Initiator: %s
Lot: %s
Bidder: %s
Bid: %s
End Time: %s
Max End Time: %s`,
a.GetID(), a.Initiator, a.Lot,
a.Bidder, a.Bid, a.GetEndTime().String(),
a.MaxEndTime.String(),
)
}
// NewBaseAuction creates a new base auction
func NewBaseAuction(seller sdk.AccAddress, lot sdk.Coin, initialBid sdk.Coin, EndTime EndTime) BaseAuction {
auction := BaseAuction{
// no ID
Initiator: seller,
Lot: lot,
Bidder: seller, // send the proceeds from the first bid back to the seller
Bid: initialBid, // set this to zero most of the time
EndTime: EndTime,
MaxEndTime: EndTime,
}
return auction
}
// ForwardAuction type for forward auctions
type ForwardAuction struct {
BaseAuction
}
// NewForwardAuction creates a new forward auction
func NewForwardAuction(seller sdk.AccAddress, lot sdk.Coin, initialBid sdk.Coin, EndTime EndTime) (ForwardAuction, BankOutput) {
auction := ForwardAuction{BaseAuction{
// no ID
Initiator: seller,
Lot: lot,
Bidder: seller, // send the proceeds from the first bid back to the seller
Bid: initialBid, // set this to zero most of the time
EndTime: EndTime,
MaxEndTime: EndTime,
}}
output := BankOutput{seller, lot}
return auction, output
}
// PlaceBid implements Auction
func (a *ForwardAuction) PlaceBid(currentBlockHeight EndTime, bidder sdk.AccAddress, lot sdk.Coin, bid sdk.Coin) ([]BankOutput, []BankInput, sdk.Error) {
// TODO check lot size matches lot?
// check auction has not closed
if currentBlockHeight > a.EndTime {
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("auction has closed")
}
// check bid is greater than last bid
if !a.Bid.IsLT(bid) { // TODO add minimum bid size
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("bid not greater than last bid")
}
// calculate coin movements
outputs := []BankOutput{{bidder, bid}} // new bidder pays bid now
inputs := []BankInput{{a.Bidder, a.Bid}, {a.Initiator, bid.Sub(a.Bid)}} // old bidder is paid back, extra goes to seller
// update auction
a.Bidder = bidder
a.Bid = bid
// increment timeout // TODO into keeper?
a.EndTime = EndTime(min(int64(currentBlockHeight+DefaultMaxBidDuration), int64(a.MaxEndTime))) // TODO is there a better way to structure these types?
return outputs, inputs, nil
}
// ReverseAuction type for reverse auctions
// TODO when exporting state and initializing a new genesis, we'll need a way to differentiate forward from reverse auctions
type ReverseAuction struct {
BaseAuction
}
// NewReverseAuction creates a new reverse auction
func NewReverseAuction(buyer sdk.AccAddress, bid sdk.Coin, initialLot sdk.Coin, EndTime EndTime) (ReverseAuction, BankOutput) {
auction := ReverseAuction{BaseAuction{
// no ID
Initiator: buyer,
Lot: initialLot,
Bidder: buyer, // send proceeds from the first bid to the buyer
Bid: bid, // amount that the buyer it buying - doesn't change over course of auction
EndTime: EndTime,
MaxEndTime: EndTime,
}}
output := BankOutput{buyer, initialLot}
return auction, output
}
// PlaceBid implements Auction
func (a *ReverseAuction) PlaceBid(currentBlockHeight EndTime, bidder sdk.AccAddress, lot sdk.Coin, bid sdk.Coin) ([]BankOutput, []BankInput, sdk.Error) {
// check bid size matches bid?
// check auction has not closed
if currentBlockHeight > a.EndTime {
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("auction has closed")
}
// check bid is less than last bid
if !lot.IsLT(a.Lot) { // TODO add min bid decrements
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("lot not smaller than last lot")
}
// calculate coin movements
outputs := []BankOutput{{bidder, a.Bid}} // new bidder pays bid now
inputs := []BankInput{{a.Bidder, a.Bid}, {a.Initiator, a.Lot.Sub(lot)}} // old bidder is paid back, decrease in price for goes to buyer
// update auction
a.Bidder = bidder
a.Lot = lot
// increment timeout // TODO into keeper?
a.EndTime = EndTime(min(int64(currentBlockHeight+DefaultMaxBidDuration), int64(a.MaxEndTime))) // TODO is there a better way to structure these types?
return outputs, inputs, nil
}
// ForwardReverseAuction type for forward reverse auction
type ForwardReverseAuction struct {
BaseAuction
MaxBid sdk.Coin
OtherPerson sdk.AccAddress // TODO rename, this is normally the original CDP owner
}
func (a ForwardReverseAuction) String() string {
return fmt.Sprintf(`Auction %d:
Initiator: %s
Lot: %s
Bidder: %s
Bid: %s
End Time: %s
Max End Time: %s
Max Bid %s
Other Person %s`,
a.GetID(), a.Initiator, a.Lot,
a.Bidder, a.Bid, a.GetEndTime().String(),
a.MaxEndTime.String(), a.MaxBid, a.OtherPerson,
)
}
// NewForwardReverseAuction creates a new forward reverse auction
func NewForwardReverseAuction(seller sdk.AccAddress, lot sdk.Coin, initialBid sdk.Coin, EndTime EndTime, maxBid sdk.Coin, otherPerson sdk.AccAddress) (ForwardReverseAuction, BankOutput) {
auction := ForwardReverseAuction{
BaseAuction: BaseAuction{
// no ID
Initiator: seller,
Lot: lot,
Bidder: seller, // send the proceeds from the first bid back to the seller
Bid: initialBid, // 0 most of the time
EndTime: EndTime,
MaxEndTime: EndTime},
MaxBid: maxBid,
OtherPerson: otherPerson,
}
output := BankOutput{seller, lot}
return auction, output
}
// PlaceBid implements auction
func (a *ForwardReverseAuction) PlaceBid(currentBlockHeight EndTime, bidder sdk.AccAddress, lot sdk.Coin, bid sdk.Coin) (outputs []BankOutput, inputs []BankInput, err sdk.Error) {
// check auction has not closed
if currentBlockHeight > a.EndTime {
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("auction has closed")
}
// determine phase of auction
switch {
case a.Bid.IsLT(a.MaxBid) && bid.IsLT(a.MaxBid):
// Forward auction phase
if !a.Bid.IsLT(bid) { // TODO add min bid increments
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("bid not greater than last bid")
}
outputs = []BankOutput{{bidder, bid}} // new bidder pays bid now
inputs = []BankInput{{a.Bidder, a.Bid}, {a.Initiator, bid.Sub(a.Bid)}} // old bidder is paid back, extra goes to seller
case a.Bid.IsLT(a.MaxBid):
// Switch over phase
if !bid.IsEqual(a.MaxBid) { // require bid == a.MaxBid
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("bid greater than the max bid")
}
outputs = []BankOutput{{bidder, bid}} // new bidder pays bid now
inputs = []BankInput{
{a.Bidder, a.Bid}, // old bidder is paid back
{a.Initiator, bid.Sub(a.Bid)}, // extra goes to seller
{a.OtherPerson, a.Lot.Sub(lot)}, //decrease in price for goes to original CDP owner
}
case a.Bid.IsEqual(a.MaxBid):
// Reverse auction phase
if !lot.IsLT(a.Lot) { // TODO add min bid decrements
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("lot not smaller than last lot")
}
outputs = []BankOutput{{bidder, a.Bid}} // new bidder pays bid now
inputs = []BankInput{{a.Bidder, a.Bid}, {a.OtherPerson, a.Lot.Sub(lot)}} // old bidder is paid back, decrease in price for goes to original CDP owner
default:
panic("should never be reached") // TODO
}
// update auction
a.Bidder = bidder
a.Lot = lot
a.Bid = bid
// increment timeout
// TODO use bid duration param
a.EndTime = EndTime(min(int64(currentBlockHeight+DefaultMaxBidDuration), int64(a.MaxEndTime))) // TODO is there a better way to structure these types?
return outputs, inputs, nil
}

View File

@ -0,0 +1,403 @@
package types
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
// TODO can this be less verbose? Should PlaceBid() be split into smaller functions?
// It would be possible to combine all auction tests into one test runner.
func TestForwardAuction_PlaceBid(t *testing.T) {
seller := sdk.AccAddress([]byte("a_seller"))
buyer1 := sdk.AccAddress([]byte("buyer1"))
buyer2 := sdk.AccAddress([]byte("buyer2"))
end := EndTime(10000)
now := EndTime(10)
type args struct {
currentBlockHeight EndTime
bidder sdk.AccAddress
lot sdk.Coin
bid sdk.Coin
}
tests := []struct {
name string
auction ForwardAuction
args args
expectedOutputs []BankOutput
expectedInputs []BankInput
expectedEndTime EndTime
expectedBidder sdk.AccAddress
expectedBid sdk.Coin
expectpass bool
}{
{
"normal",
ForwardAuction{BaseAuction{
Initiator: seller,
Lot: c("usdx", 100),
Bidder: buyer1,
Bid: c("kava", 6),
EndTime: end,
MaxEndTime: end,
}},
args{now, buyer2, c("usdx", 100), c("kava", 10)},
[]BankOutput{{buyer2, c("kava", 10)}},
[]BankInput{{buyer1, c("kava", 6)}, {seller, c("kava", 4)}},
now + DefaultMaxBidDuration,
buyer2,
c("kava", 10),
true,
},
{
"lowBid",
ForwardAuction{BaseAuction{
Initiator: seller,
Lot: c("usdx", 100),
Bidder: buyer1,
Bid: c("kava", 6),
EndTime: end,
MaxEndTime: end,
}},
args{now, buyer2, c("usdx", 100), c("kava", 5)},
[]BankOutput{},
[]BankInput{},
end,
buyer1,
c("kava", 6),
false,
},
{
"equalBid",
ForwardAuction{BaseAuction{
Initiator: seller,
Lot: c("usdx", 100),
Bidder: buyer1,
Bid: c("kava", 6),
EndTime: end,
MaxEndTime: end,
}},
args{now, buyer2, c("usdx", 100), c("kava", 6)},
[]BankOutput{},
[]BankInput{},
end,
buyer1,
c("kava", 6),
false,
},
{
"timeout",
ForwardAuction{BaseAuction{
Initiator: seller,
Lot: c("usdx", 100),
Bidder: buyer1,
Bid: c("kava", 6),
EndTime: end,
MaxEndTime: end,
}},
args{end + 1, buyer2, c("usdx", 100), c("kava", 10)},
[]BankOutput{},
[]BankInput{},
end,
buyer1,
c("kava", 6),
false,
},
{
"hitMaxEndTime",
ForwardAuction{BaseAuction{
Initiator: seller,
Lot: c("usdx", 100),
Bidder: buyer1,
Bid: c("kava", 6),
EndTime: end,
MaxEndTime: end,
}},
args{end - 1, buyer2, c("usdx", 100), c("kava", 10)},
[]BankOutput{{buyer2, c("kava", 10)}},
[]BankInput{{buyer1, c("kava", 6)}, {seller, c("kava", 4)}},
end, // end time should be capped at MaxEndTime
buyer2,
c("kava", 10),
true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// update auction and return in/outputs
outputs, inputs, err := tc.auction.PlaceBid(tc.args.currentBlockHeight, tc.args.bidder, tc.args.lot, tc.args.bid)
// check for err
if tc.expectpass {
require.Nil(t, err)
} else {
require.NotNil(t, err)
}
// check for correct in/outputs
require.Equal(t, tc.expectedOutputs, outputs)
require.Equal(t, tc.expectedInputs, inputs)
// check for correct EndTime, bidder, bid
require.Equal(t, tc.expectedEndTime, tc.auction.EndTime)
require.Equal(t, tc.expectedBidder, tc.auction.Bidder)
require.Equal(t, tc.expectedBid, tc.auction.Bid)
})
}
}
func TestReverseAuction_PlaceBid(t *testing.T) {
buyer := sdk.AccAddress([]byte("a_buyer"))
seller1 := sdk.AccAddress([]byte("seller1"))
seller2 := sdk.AccAddress([]byte("seller2"))
end := EndTime(10000)
now := EndTime(10)
type args struct {
currentBlockHeight EndTime
bidder sdk.AccAddress
lot sdk.Coin
bid sdk.Coin
}
tests := []struct {
name string
auction ReverseAuction
args args
expectedOutputs []BankOutput
expectedInputs []BankInput
expectedEndTime EndTime
expectedBidder sdk.AccAddress
expectedLot sdk.Coin
expectpass bool
}{
{
"normal",
ReverseAuction{BaseAuction{
Initiator: buyer,
Lot: c("kava", 10),
Bidder: seller1,
Bid: c("usdx", 100),
EndTime: end,
MaxEndTime: end,
}},
args{now, seller2, c("kava", 9), c("usdx", 100)},
[]BankOutput{{seller2, c("usdx", 100)}},
[]BankInput{{seller1, c("usdx", 100)}, {buyer, c("kava", 1)}},
now + DefaultMaxBidDuration,
seller2,
c("kava", 9),
true,
},
{
"highBid",
ReverseAuction{BaseAuction{
Initiator: buyer,
Lot: c("kava", 10),
Bidder: seller1,
Bid: c("usdx", 100),
EndTime: end,
MaxEndTime: end,
}},
args{now, seller2, c("kava", 11), c("usdx", 100)},
[]BankOutput{},
[]BankInput{},
end,
seller1,
c("kava", 10),
false,
},
{
"equalBid",
ReverseAuction{BaseAuction{
Initiator: buyer,
Lot: c("kava", 10),
Bidder: seller1,
Bid: c("usdx", 100),
EndTime: end,
MaxEndTime: end,
}},
args{now, seller2, c("kava", 10), c("usdx", 100)},
[]BankOutput{},
[]BankInput{},
end,
seller1,
c("kava", 10),
false,
},
{
"timeout",
ReverseAuction{BaseAuction{
Initiator: buyer,
Lot: c("kava", 10),
Bidder: seller1,
Bid: c("usdx", 100),
EndTime: end,
MaxEndTime: end,
}},
args{end + 1, seller2, c("kava", 9), c("usdx", 100)},
[]BankOutput{},
[]BankInput{},
end,
seller1,
c("kava", 10),
false,
},
{
"hitMaxEndTime",
ReverseAuction{BaseAuction{
Initiator: buyer,
Lot: c("kava", 10),
Bidder: seller1,
Bid: c("usdx", 100),
EndTime: end,
MaxEndTime: end,
}},
args{end - 1, seller2, c("kava", 9), c("usdx", 100)},
[]BankOutput{{seller2, c("usdx", 100)}},
[]BankInput{{seller1, c("usdx", 100)}, {buyer, c("kava", 1)}},
end, // end time should be capped at MaxEndTime
seller2,
c("kava", 9),
true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// update auction and return in/outputs
outputs, inputs, err := tc.auction.PlaceBid(tc.args.currentBlockHeight, tc.args.bidder, tc.args.lot, tc.args.bid)
// check for err
if tc.expectpass {
require.Nil(t, err)
} else {
require.NotNil(t, err)
}
// check for correct in/outputs
require.Equal(t, tc.expectedOutputs, outputs)
require.Equal(t, tc.expectedInputs, inputs)
// check for correct EndTime, bidder, bid
require.Equal(t, tc.expectedEndTime, tc.auction.EndTime)
require.Equal(t, tc.expectedBidder, tc.auction.Bidder)
require.Equal(t, tc.expectedLot, tc.auction.Lot)
})
}
}
func TestForwardReverseAuction_PlaceBid(t *testing.T) {
cdpOwner := sdk.AccAddress([]byte("a_cdp_owner"))
seller := sdk.AccAddress([]byte("a_seller"))
buyer1 := sdk.AccAddress([]byte("buyer1"))
buyer2 := sdk.AccAddress([]byte("buyer2"))
end := EndTime(10000)
now := EndTime(10)
type args struct {
currentBlockHeight EndTime
bidder sdk.AccAddress
lot sdk.Coin
bid sdk.Coin
}
tests := []struct {
name string
auction ForwardReverseAuction
args args
expectedOutputs []BankOutput
expectedInputs []BankInput
expectedEndTime EndTime
expectedBidder sdk.AccAddress
expectedLot sdk.Coin
expectedBid sdk.Coin
expectpass bool
}{
{
"normalForwardBid",
ForwardReverseAuction{BaseAuction: BaseAuction{
Initiator: seller,
Lot: c("xrp", 100),
Bidder: buyer1,
Bid: c("usdx", 5),
EndTime: end,
MaxEndTime: end},
MaxBid: c("usdx", 10),
OtherPerson: cdpOwner,
},
args{now, buyer2, c("xrp", 100), c("usdx", 6)},
[]BankOutput{{buyer2, c("usdx", 6)}},
[]BankInput{{buyer1, c("usdx", 5)}, {seller, c("usdx", 1)}},
now + DefaultMaxBidDuration,
buyer2,
c("xrp", 100),
c("usdx", 6),
true,
},
{
"normalSwitchOverBid",
ForwardReverseAuction{BaseAuction: BaseAuction{
Initiator: seller,
Lot: c("xrp", 100),
Bidder: buyer1,
Bid: c("usdx", 5),
EndTime: end,
MaxEndTime: end},
MaxBid: c("usdx", 10),
OtherPerson: cdpOwner,
},
args{now, buyer2, c("xrp", 99), c("usdx", 10)},
[]BankOutput{{buyer2, c("usdx", 10)}},
[]BankInput{{buyer1, c("usdx", 5)}, {seller, c("usdx", 5)}, {cdpOwner, c("xrp", 1)}},
now + DefaultMaxBidDuration,
buyer2,
c("xrp", 99),
c("usdx", 10),
true,
},
{
"normalReverseBid",
ForwardReverseAuction{BaseAuction: BaseAuction{
Initiator: seller,
Lot: c("xrp", 99),
Bidder: buyer1,
Bid: c("usdx", 10),
EndTime: end,
MaxEndTime: end},
MaxBid: c("usdx", 10),
OtherPerson: cdpOwner,
},
args{now, buyer2, c("xrp", 90), c("usdx", 10)},
[]BankOutput{{buyer2, c("usdx", 10)}},
[]BankInput{{buyer1, c("usdx", 10)}, {cdpOwner, c("xrp", 9)}},
now + DefaultMaxBidDuration,
buyer2,
c("xrp", 90),
c("usdx", 10),
true,
},
// TODO more test cases
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// update auction and return in/outputs
outputs, inputs, err := tc.auction.PlaceBid(tc.args.currentBlockHeight, tc.args.bidder, tc.args.lot, tc.args.bid)
// check for err
if tc.expectpass {
require.Nil(t, err)
} else {
require.NotNil(t, err)
}
// check for correct in/outputs
require.Equal(t, tc.expectedOutputs, outputs)
require.Equal(t, tc.expectedInputs, inputs)
// check for correct EndTime, bidder, bid
require.Equal(t, tc.expectedEndTime, tc.auction.EndTime)
require.Equal(t, tc.expectedBidder, tc.auction.Bidder)
require.Equal(t, tc.expectedLot, tc.auction.Lot)
require.Equal(t, tc.expectedBid, tc.auction.Bid)
})
}
}
// defined to avoid cluttering test cases with long function name
func c(denom string, amount int64) sdk.Coin {
return sdk.NewInt64Coin(denom, amount)
}

23
x/auction/types/codec.go Normal file
View File

@ -0,0 +1,23 @@
package types
import (
"github.com/cosmos/cosmos-sdk/codec"
)
// ModuleCdc module level codec
var ModuleCdc = codec.New()
func init() {
RegisterCodec(ModuleCdc)
}
// RegisterCodec registers concrete types on the codec.
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgPlaceBid{}, "auction/MsgPlaceBid", nil)
// Register the Auction interface and concrete types
cdc.RegisterInterface((*Auction)(nil), nil)
cdc.RegisterConcrete(&ForwardAuction{}, "auction/ForwardAuction", nil)
cdc.RegisterConcrete(&ReverseAuction{}, "auction/ReverseAuction", nil)
cdc.RegisterConcrete(&ForwardReverseAuction{}, "auction/ForwardReverseAuction", nil)
}

View File

@ -0,0 +1,10 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
type BankKeeper interface {
SubtractCoins(sdk.Context, sdk.AccAddress, sdk.Coins) (sdk.Coins, sdk.Error)
AddCoins(sdk.Context, sdk.AccAddress, sdk.Coins) (sdk.Coins, sdk.Error)
}

View File

@ -0,0 +1,47 @@
package types
import (
"bytes"
)
// GenesisAuctions type for an array of auctions
type GenesisAuctions []Auction
// GenesisState - auction state that must be provided at genesis
type GenesisState struct {
AuctionParams AuctionParams `json:"auction_params" yaml:"auction_params"`
Auctions GenesisAuctions `json:"genesis_auctions" yaml:"genesis_auctions"`
}
// NewGenesisState returns a new genesis state object for auctions module
func NewGenesisState(ap AuctionParams, ga GenesisAuctions) GenesisState {
return GenesisState{
AuctionParams: ap,
Auctions: ga,
}
}
// DefaultGenesisState defines default genesis state for auction module
func DefaultGenesisState() GenesisState {
return NewGenesisState(DefaultAuctionParams(), GenesisAuctions{})
}
// Equal checks whether two GenesisState structs are equivalent
func (data GenesisState) Equal(data2 GenesisState) bool {
b1 := ModuleCdc.MustMarshalBinaryBare(data)
b2 := ModuleCdc.MustMarshalBinaryBare(data2)
return bytes.Equal(b1, b2)
}
// IsEmpty returns true if a GenesisState is empty
func (data GenesisState) IsEmpty() bool {
return data.Equal(GenesisState{})
}
// ValidateGenesis validates genesis inputs. Returns error if validation of any input fails.
func ValidateGenesis(data GenesisState) error {
if err := data.AuctionParams.Validate(); err != nil {
return err
}
return nil
}

15
x/auction/types/keys.go Normal file
View File

@ -0,0 +1,15 @@
package types
const (
// ModuleName The name that will be used throughout the module
ModuleName = "auction"
// StoreKey Top level store key where all module items will be stored
StoreKey = ModuleName
// RouterKey Top level router key
RouterKey = ModuleName
// DefaultParamspace default name for parameter store
DefaultParamspace = ModuleName
)

93
x/auction/types/msg.go Normal file
View File

@ -0,0 +1,93 @@
package types
import sdk "github.com/cosmos/cosmos-sdk/types"
// MsgPlaceBid is the message type used to place a bid on any type of auction.
type MsgPlaceBid struct {
AuctionID ID
Bidder sdk.AccAddress // This can be a buyer (who increments bid), or a seller (who decrements lot) TODO rename to be clearer?
Bid sdk.Coin
Lot sdk.Coin
}
// NewMsgPlaceBid returns a new MsgPlaceBid.
func NewMsgPlaceBid(auctionID ID, bidder sdk.AccAddress, bid sdk.Coin, lot sdk.Coin) MsgPlaceBid {
return MsgPlaceBid{
AuctionID: auctionID,
Bidder: bidder,
Bid: bid,
Lot: lot,
}
}
// Route return the message type used for routing the message.
func (msg MsgPlaceBid) Route() string { return "auction" }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgPlaceBid) Type() string { return "place_bid" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgPlaceBid) ValidateBasic() sdk.Error {
if msg.Bidder.Empty() {
return sdk.ErrInternal("invalid (empty) bidder address")
}
if msg.Bid.Amount.LT(sdk.ZeroInt()) {
return sdk.ErrInternal("invalid (negative) bid amount")
}
if msg.Lot.Amount.LT(sdk.ZeroInt()) {
return sdk.ErrInternal("invalid (negative) lot amount")
}
// TODO check coin denoms
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgPlaceBid) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgPlaceBid) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Bidder}
}
// The CDP system doesn't need Msgs for starting auctions. But they could be added to allow people to create random auctions of their own, and to make this module more general purpose.
// type MsgStartForwardAuction struct {
// Seller sdk.AccAddress
// Amount sdk.Coins
// // TODO add starting bid amount?
// // TODO specify asset denom to be received
// }
// // NewMsgStartAuction returns a new MsgStartAuction.
// func NewMsgStartAuction(seller sdk.AccAddress, amount sdk.Coins, maxBid sdk.Coins) MsgStartAuction {
// return MsgStartAuction{
// Seller: seller,
// Amount: amount,
// MaxBid: maxBid,
// }
// }
// // Route return the message type used for routing the message.
// func (msg MsgStartAuction) Route() string { return "auction" }
// // Type returns a human-readable string for the message, intended for utilization within tags.
// func (msg MsgStartAuction) Type() string { return "start_auction" }
// // ValidateBasic does a simple validation check that doesn't require access to any other information.
// func (msg MsgStartAuction) ValidateBasic() sdk.Error {
// return nil
// }
// // GetSignBytes gets the canonical byte representation of the Msg.
// func (msg MsgStartAuction) GetSignBytes() []byte {
// bz := msgCdc.MustMarshalJSON(msg)
// return sdk.MustSortJSON(bz)
// }
// // GetSigners returns the addresses of signers that must sign.
// func (msg MsgStartAuction) GetSigners() []sdk.AccAddress {
// return []sdk.AccAddress{msg.Seller}
// }

View File

@ -0,0 +1,32 @@
package types
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
func TestMsgPlaceBid_ValidateBasic(t *testing.T) {
addr := sdk.AccAddress([]byte("someName"))
tests := []struct {
name string
msg MsgPlaceBid
expectPass bool
}{
{"normal", MsgPlaceBid{0, addr, sdk.NewInt64Coin("usdx", 10), sdk.NewInt64Coin("kava", 20)}, true},
{"emptyAddr", MsgPlaceBid{0, sdk.AccAddress{}, sdk.NewInt64Coin("usdx", 10), sdk.NewInt64Coin("kava", 20)}, false},
{"negativeBid", MsgPlaceBid{0, addr, sdk.Coin{"usdx", sdk.NewInt(-10)}, sdk.NewInt64Coin("kava", 20)}, false},
{"negativeLot", MsgPlaceBid{0, addr, sdk.NewInt64Coin("usdx", 10), sdk.Coin{"kava", sdk.NewInt(-20)}}, false},
{"zerocoins", MsgPlaceBid{0, addr, sdk.NewInt64Coin("usdx", 0), sdk.NewInt64Coin("kava", 0)}, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.expectPass {
require.Nil(t, tc.msg.ValidateBasic())
} else {
require.NotNil(t, tc.msg.ValidateBasic())
}
})
}
}

98
x/auction/types/params.go Normal file
View File

@ -0,0 +1,98 @@
package types
import (
"bytes"
"fmt"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
)
// Defaults for auction params
const (
// DefaultMaxAuctionDuration max length of auction, roughly 2 days in blocks
DefaultMaxAuctionDuration EndTime = 2 * 24 * 3600 / 5
// DefaultBidDuration how long an auction gets extended when someone bids, roughly 3 hours in blocks
DefaultMaxBidDuration EndTime = 3 * 3600 / 5
// DefaultStartingAuctionID what the id of the first auction will be
DefaultStartingAuctionID ID = ID(0)
)
// Parameter keys
var (
// ParamStoreKeyAuctionParams Param store key for auction params
KeyAuctionBidDuration = []byte("MaxBidDuration")
KeyAuctionDuration = []byte("MaxAuctionDuration")
KeyAuctionStartingID = []byte("StartingAuctionID")
)
var _ subspace.ParamSet = &AuctionParams{}
// AuctionParams governance parameters for auction module
type AuctionParams struct {
MaxAuctionDuration EndTime `json:"max_auction_duration" yaml:"max_auction_duration"` // max length of auction, in blocks
MaxBidDuration EndTime `json:"max_bid_duration" yaml:"max_bid_duration"`
StartingAuctionID ID `json:"starting_auction_id" yaml:"starting_auction_id"`
}
// NewAuctionParams creates a new AuctionParams object
func NewAuctionParams(maxAuctionDuration EndTime, bidDuration EndTime, startingID ID) AuctionParams {
return AuctionParams{
MaxAuctionDuration: maxAuctionDuration,
MaxBidDuration: bidDuration,
StartingAuctionID: startingID,
}
}
// DefaultAuctionParams default parameters for auctions
func DefaultAuctionParams() AuctionParams {
return NewAuctionParams(
DefaultMaxAuctionDuration,
DefaultMaxBidDuration,
DefaultStartingAuctionID,
)
}
// ParamKeyTable Key declaration for parameters
func ParamKeyTable() subspace.KeyTable {
return subspace.NewKeyTable().RegisterParamSet(&AuctionParams{})
}
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
// pairs of auth module's parameters.
// nolint
func (ap *AuctionParams) ParamSetPairs() subspace.ParamSetPairs {
return subspace.ParamSetPairs{
{KeyAuctionBidDuration, &ap.MaxBidDuration},
{KeyAuctionDuration, &ap.MaxAuctionDuration},
{KeyAuctionStartingID, &ap.StartingAuctionID},
}
}
// Equal returns a boolean determining if two AuctionParams types are identical.
func (ap AuctionParams) Equal(ap2 AuctionParams) bool {
bz1 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&ap)
bz2 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&ap2)
return bytes.Equal(bz1, bz2)
}
// String implements stringer interface
func (ap AuctionParams) String() string {
return fmt.Sprintf(`Auction Params:
Max Auction Duration: %s
Max Bid Duration: %s
Starting Auction ID: %v`, ap.MaxAuctionDuration, ap.MaxBidDuration, ap.StartingAuctionID)
}
// Validate checks that the parameters have valid values.
func (ap AuctionParams) Validate() error {
if ap.MaxAuctionDuration <= EndTime(0) {
return fmt.Errorf("max auction duration should be positive, is %s", ap.MaxAuctionDuration)
}
if ap.MaxBidDuration <= EndTime(0) {
return fmt.Errorf("bid duration should be positive, is %s", ap.MaxBidDuration)
}
if ap.StartingAuctionID <= ID(0) {
return fmt.Errorf("starting auction ID should be positive, is %v", ap.StartingAuctionID)
}
return nil
}

View File

@ -0,0 +1,18 @@
package types
import (
"strings"
)
const (
// QueryGetAuction command for getting the information about a particular auction
QueryGetAuction = "getauctions"
)
// QueryResAuctions Result Payload for an auctions query
type QueryResAuctions []string
// implement fmt.Stringer
func (n QueryResAuctions) String() string {
return strings.Join(n[:], "\n")
}

9
x/auction/types/utils.go Normal file
View File

@ -0,0 +1,9 @@
package types
// Go doesn't have a built in min function for integers :(
func min(a, b int64) int64 {
if a < b {
return a
}
return b
}

61
x/cdp/alias.go Normal file
View File

@ -0,0 +1,61 @@
// nolint
// autogenerated code using github.com/rigelrozanski/multitool
// aliases generated for the following subdirectories:
// ALIASGEN: github.com/kava-labs/kava/x/cdp/types/
// ALIASGEN: github.com/kava-labs/kava/x/cdp/keeper/
package cdp
import (
"github.com/kava-labs/kava/x/cdp/keeper"
"github.com/kava-labs/kava/x/cdp/types"
)
const (
ModuleName = types.ModuleName
StoreKey = types.StoreKey
RouterKey = types.RouterKey
DefaultParamspace = types.DefaultParamspace
QueryGetCdps = types.QueryGetCdps
QueryGetParams = types.QueryGetParams
RestOwner = types.RestOwner
RestCollateralDenom = types.RestCollateralDenom
RestUnderCollateralizedAt = types.RestUnderCollateralizedAt
GovDenom = types.GovDenom
)
var (
// functions aliases
RegisterCodec = types.RegisterCodec
DefaultGenesisState = types.DefaultGenesisState
ValidateGenesis = types.ValidateGenesis
NewMsgCreateOrModifyCDP = types.NewMsgCreateOrModifyCDP
ParamKeyTable = types.ParamKeyTable
DefaultParams = types.DefaultParams
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
// variable aliases
ModuleCdc = types.ModuleCdc
KeyGlobalDebtLimit = types.KeyGlobalDebtLimit
KeyCollateralParams = types.KeyCollateralParams
KeyStableDenoms = types.KeyStableDenoms
LiquidatorAccountAddress = keeper.LiquidatorAccountAddress
)
type (
BankKeeper = types.BankKeeper
PricefeedKeeper = types.PricefeedKeeper
GenesisState = types.GenesisState
MsgCreateOrModifyCDP = types.MsgCreateOrModifyCDP
MsgTransferCDP = types.MsgTransferCDP
CdpParams = types.CdpParams
CollateralParams = types.CollateralParams
QueryCdpsParams = types.QueryCdpsParams
ModifyCdpRequestBody = types.ModifyCdpRequestBody
CDP = types.CDP
CDPs = types.CDPs
ByCollateralRatio = types.ByCollateralRatio
CollateralState = types.CollateralState
Keeper = keeper.Keeper
LiquidatorModuleAccount = keeper.LiquidatorModuleAccount
)

67
x/cdp/app_test.go Normal file
View File

@ -0,0 +1,67 @@
package cdp
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/mock"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/pricefeed"
)
func TestApp_CreateModifyDeleteCDP(t *testing.T) {
// Setup
mapp, keeper, pfKeeper := setUpMockAppWithoutGenesis()
genAccs, addrs, _, privKeys := mock.CreateGenAccounts(1, cs(c("xrp", 100)))
testAddr := addrs[0]
testPrivKey := privKeys[0]
mock.SetGenesis(mapp, genAccs)
mock.CheckBalance(t, mapp, testAddr, cs(c("xrp", 100)))
// setup pricefeed, TODO can this be shortened a bit?
header := abci.Header{Height: mapp.LastBlockHeight() + 1}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header)
params := CdpParams{
GlobalDebtLimit: sdk.NewInt(100000),
CollateralParams: []CollateralParams{
{
Denom: "xrp",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt(10000),
},
},
StableDenoms: []string{"usdx"},
}
keeper.SetParams(ctx, params)
keeper.SetGlobalDebt(ctx, sdk.NewInt(0))
ap := pricefeed.AssetParams{
Assets: []pricefeed.Asset{pricefeed.Asset{AssetCode: "xrp", Description: ""}},
}
pfKeeper.SetAssetParams(ctx, ap)
pfKeeper.SetPrice(
ctx, sdk.AccAddress{}, "xrp",
sdk.MustNewDecFromStr("1.00"),
sdk.NewInt(10))
pfKeeper.SetCurrentPrices(ctx)
mapp.EndBlock(abci.RequestEndBlock{})
mapp.Commit()
// Create CDP
msgs := []sdk.Msg{NewMsgCreateOrModifyCDP(testAddr, "xrp", i(10), i(5))}
mock.SignCheckDeliver(t, mapp.Cdc, mapp.BaseApp, abci.Header{Height: mapp.LastBlockHeight() + 1}, msgs, []uint64{0}, []uint64{0}, true, true, testPrivKey)
mock.CheckBalance(t, mapp, testAddr, cs(c("usdx", 5), c("xrp", 90)))
// Modify CDP
msgs = []sdk.Msg{NewMsgCreateOrModifyCDP(testAddr, "xrp", i(40), i(5))}
mock.SignCheckDeliver(t, mapp.Cdc, mapp.BaseApp, abci.Header{Height: mapp.LastBlockHeight() + 1}, msgs, []uint64{0}, []uint64{1}, true, true, testPrivKey)
mock.CheckBalance(t, mapp, testAddr, cs(c("usdx", 10), c("xrp", 50)))
// Delete CDP
msgs = []sdk.Msg{NewMsgCreateOrModifyCDP(testAddr, "xrp", i(-50), i(-10))}
mock.SignCheckDeliver(t, mapp.Cdc, mapp.BaseApp, abci.Header{Height: mapp.LastBlockHeight() + 1}, msgs, []uint64{0}, []uint64{2}, true, true, testPrivKey)
mock.CheckBalance(t, mapp, testAddr, cs(c("xrp", 100)))
}

167
x/cdp/client/cli/query.go Normal file
View File

@ -0,0 +1,167 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types"
)
// GetQueryCmd returns the cli query commands for this module
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
// Group nameservice queries under a subcommand
cdpQueryCmd := &cobra.Command{
Use: "cdp",
Short: "Querying commands for the cdp module",
}
cdpQueryCmd.AddCommand(client.GetCommands(
GetCmdGetCdp(queryRoute, cdc),
GetCmdGetCdps(queryRoute, cdc),
GetCmdGetUnderCollateralizedCdps(queryRoute, cdc),
GetCmdGetParams(queryRoute, cdc),
)...)
return cdpQueryCmd
}
// GetCmdGetCdp queries the latest info about a particular cdp
func GetCmdGetCdp(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "cdp [ownerAddress] [collateralType]",
Short: "get info about a cdp",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
ownerAddress, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
collateralType := args[1] // TODO validation?
bz, err := cdc.MarshalJSON(types.QueryCdpsParams{
Owner: ownerAddress,
CollateralDenom: collateralType,
})
if err != nil {
return err
}
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdps)
res, _, err := cliCtx.QueryWithData(route, bz)
if err != nil {
fmt.Printf("error when getting cdp info - %s", err)
fmt.Printf("could not get current cdp info - %s %s \n", string(ownerAddress), string(collateralType))
return err
}
// Decode and print results
var cdps types.CDPs
cdc.MustUnmarshalJSON(res, &cdps)
if len(cdps) != 1 {
panic("Unexpected number of CDPs returned from querier. This shouldn't happen.")
}
return cliCtx.PrintOutput(cdps[0])
},
}
}
// GetCmdGetCdps queries the store for all cdps for a collateral type
func GetCmdGetCdps(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "cdps [collateralType]",
Short: "get info about many cdps",
Long: "Get all CDPs. Specify a collateral type to get only CDPs with that collateral type.",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
bz, err := cdc.MarshalJSON(types.QueryCdpsParams{CollateralDenom: args[0]}) // denom="" returns all CDPs // TODO will this fail if there are no args?
if err != nil {
return err
}
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdps)
res, _, err := cliCtx.QueryWithData(route, bz)
if err != nil {
return err
}
// Decode and print results
var out types.CDPs
cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out)
},
}
}
func GetCmdGetUnderCollateralizedCdps(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "bad-cdps [collateralType] [price]",
Short: "get under collateralized CDPs",
Long: "Get all CDPS of a particular collateral type that will be under collateralized at the specified price. Pass in the current price to get currently under collateralized CDPs.",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
price, errSdk := sdk.NewDecFromStr(args[1])
if errSdk != nil {
return fmt.Errorf(errSdk.Error()) // TODO check this returns useful output
}
bz, err := cdc.MarshalJSON(types.QueryCdpsParams{
CollateralDenom: args[0],
UnderCollateralizedAt: price,
})
if err != nil {
return err
}
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdps)
res, _, err := cliCtx.QueryWithData(route, bz)
if err != nil {
return err
}
// Decode and print results
var out types.CDPs
cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out)
},
}
}
func GetCmdGetParams(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "params",
Short: "get the cdp module parameters",
Long: "Get the current global cdp module parameters.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetParams)
res, _, err := cliCtx.QueryWithData(route, nil) // TODO use cliCtx.QueryStore?
if err != nil {
return err
}
// Decode and print results
var out types.CdpParams
cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out)
},
}
}

61
x/cdp/client/cli/tx.go Normal file
View File

@ -0,0 +1,61 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/cdp/types"
)
// GetTxCmd returns the transaction commands for this module
// TODO: Tests, see: https://github.com/cosmos/cosmos-sdk/blob/18de630d0ae1887113e266982b51c2bf1f662edb/x/staking/client/cli/tx_test.go
func GetTxCmd(cdc *codec.Codec) *cobra.Command {
cdpTxCmd := &cobra.Command{
Use: "cdp",
Short: "cdp transactions subcommands",
}
cdpTxCmd.AddCommand(client.PostCommands(
GetCmdModifyCdp(cdc),
)...)
return cdpTxCmd
}
// GetCmdModifyCdp cli command for creating and modifying cdps.
func GetCmdModifyCdp(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "modifycdp [ownerAddress] [collateralType] [collateralChange] [debtChange]",
Short: "create or modify a cdp",
Args: cobra.ExactArgs(4),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
collateralChange, ok := sdk.NewIntFromString(args[2])
if !ok {
fmt.Printf("invalid collateral amount - %s \n", string(args[2]))
return nil
}
debtChange, ok := sdk.NewIntFromString(args[3])
if !ok {
fmt.Printf("invalid debt amount - %s \n", string(args[3]))
return nil
}
msg := types.NewMsgCreateOrModifyCDP(cliCtx.GetFromAddress(), args[1], collateralChange, debtChange)
err := msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}

View File

@ -0,0 +1,51 @@
package client
import (
"github.com/cosmos/cosmos-sdk/client"
cdpcmd "github.com/kava-labs/kava/x/cdp/client/cli"
"github.com/spf13/cobra"
amino "github.com/tendermint/go-amino"
)
// ModuleClient exports all client functionality from this module
type ModuleClient struct {
storeKey string
cdc *amino.Codec
}
// NewModuleClient creates client for the module
func NewModuleClient(storeKey string, cdc *amino.Codec) ModuleClient {
return ModuleClient{storeKey, cdc}
}
// GetQueryCmd returns the cli query commands for this module
func (mc ModuleClient) GetQueryCmd() *cobra.Command {
// Group nameservice queries under a subcommand
cdpQueryCmd := &cobra.Command{
Use: "cdp",
Short: "Querying commands for the cdp module",
}
cdpQueryCmd.AddCommand(client.GetCommands(
cdpcmd.GetCmdGetCdp(mc.storeKey, mc.cdc),
cdpcmd.GetCmdGetCdps(mc.storeKey, mc.cdc),
cdpcmd.GetCmdGetUnderCollateralizedCdps(mc.storeKey, mc.cdc),
cdpcmd.GetCmdGetParams(mc.storeKey, mc.cdc),
)...)
return cdpQueryCmd
}
// GetTxCmd returns the transaction commands for this module
func (mc ModuleClient) GetTxCmd() *cobra.Command {
cdpTxCmd := &cobra.Command{
Use: "cdp",
Short: "cdp transactions subcommands",
}
cdpTxCmd.AddCommand(client.PostCommands(
cdpcmd.GetCmdModifyCdp(mc.cdc),
)...)
return cdpTxCmd
}

100
x/cdp/client/rest/query.go Normal file
View File

@ -0,0 +1,100 @@
package rest
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/kava-labs/kava/x/cdp/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
/*
API Design:
Currently CDPs do not have IDs so standard REST uri conventions (ie GET /cdps/{cdp-id}) don't work too well.
Get one or more cdps
GET /cdps?collateralDenom={denom}&owner={address}&underCollateralizedAt={price}
Modify a CDP (idempotent). Create is not separated out because conceptually all CDPs already exist (just with zero collateral and debt). // TODO is making this idempotent actually useful?
PUT /cdps
Get the module params, including authorized collateral denoms.
GET /params
*/
// RegisterRoutes - Central function to define routes that get registered by the main application
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc("/cdps", getCdpsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc("/cdps/params", getParamsHandlerFn(cliCtx)).Methods("GET")
}
func getCdpsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// get parameters from the URL
ownerBech32 := r.URL.Query().Get(types.RestOwner)
collateralDenom := r.URL.Query().Get(types.RestCollateralDenom)
priceString := r.URL.Query().Get(types.RestUnderCollateralizedAt)
// Construct querier params
querierParams := types.QueryCdpsParams{}
if len(ownerBech32) != 0 {
owner, err := sdk.AccAddressFromBech32(ownerBech32)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
querierParams.Owner = owner
}
if len(collateralDenom) != 0 {
// TODO validate denom
querierParams.CollateralDenom = collateralDenom
}
if len(priceString) != 0 {
price, err := sdk.NewDecFromStr(priceString)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
querierParams.UnderCollateralizedAt = price
}
querierParamsBz, err := cliCtx.Codec.MarshalJSON(querierParams)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Get the CDPs
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetCdps), querierParamsBz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func getParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Get the params
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetParams), nil)
cliCtx = cliCtx.WithHeight(height)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// Return the params
rest.PostProcessResponse(w, cliCtx, res)
}
}

13
x/cdp/client/rest/rest.go Normal file
View File

@ -0,0 +1,13 @@
package rest
import (
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
)
// RegisterRoutes - Central function to define routes that get registered by the main application
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
registerQueryRoutes(cliCtx, r)
registerTxRoutes(cliCtx, r)
}

69
x/cdp/client/rest/tx.go Normal file
View File

@ -0,0 +1,69 @@
package rest
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/cdp/types"
)
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc("/cdps", modifyCdpHandlerFn(cliCtx)).Methods("PUT")
}
func modifyCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Decode PUT request body
var requestBody types.ModifyCdpRequestBody
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
return
}
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
if !requestBody.BaseReq.ValidateBasic(w) {
return
}
// Get the stored CDP
querierParams := types.QueryCdpsParams{
Owner: requestBody.Cdp.Owner,
CollateralDenom: requestBody.Cdp.CollateralDenom,
}
querierParamsBz, err := cliCtx.Codec.MarshalJSON(querierParams)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetCdps), querierParamsBz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
var cdps types.CDPs
err = cliCtx.Codec.UnmarshalJSON(res, &cdps)
if len(cdps) != 1 || err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// Calculate CDP updates
collateralDelta := requestBody.Cdp.CollateralAmount.Sub(cdps[0].CollateralAmount)
debtDelta := requestBody.Cdp.Debt.Sub(cdps[0].Debt)
// Create and return msg
msg := types.NewMsgCreateOrModifyCDP(
requestBody.Cdp.Owner,
requestBody.Cdp.CollateralDenom,
collateralDelta,
debtDelta,
)
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
}
}

27
x/cdp/doc.go Normal file
View File

@ -0,0 +1,27 @@
/*
Package CDP manages the storage of Collateralized Debt Positions. It handles their creation, modification, and stores the global state of all CDPs.
Notes
- sdk.Int is used for all the number types to maintain compatibility with internal type of sdk.Coin - saves type conversion when doing maths.
Also it allows for changes to a CDP to be expressed as a +ve or -ve number.
- Only allowing one CDP per account-collateralDenom pair for now to keep things simple.
- Genesis forces the global debt to start at zero, ie no stable coins in existence. This could be changed.
- The cdp module fulfills the bank keeper interface and keeps track of the liquidator module's coins. This won't be needed with module accounts.
- GetCDPs does not return an iterator, but instead reads out (potentially) all CDPs from the store. This isn't a huge performance concern as it is never used during a block, only for querying.
An iterator could be created, following the queue style construct in gov and auction, where CDP IDs are stored under ordered keys.
These keys could be a collateral-denom:collateral-ratio so that it is efficient to obtain the undercollateralized CDP for a given price and liquidation ratio.
However creating a byte sortable representation of a collateral ratio wasn't very easy so the simpler approach was chosen.
TODO
- A shorter name for an under-collateralized CDP would shorten a lot of function names
- remove fake bank keeper and setup a proper liquidator module account
- what happens if a collateral type is removed from the list of allowed ones?
- Should the values used to generate a key for a stored struct be in the struct?
- Add constants for the module and route names
- Many more TODOs in the code
- add more aggressive test cases
- tags
- custom error types, codespace
*/
package cdp

47
x/cdp/genesis.go Normal file
View File

@ -0,0 +1,47 @@
package cdp
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, data GenesisState) {
// validate denoms - check that any collaterals in the CdpParams are in the pricefeed, pricefeed needs to initgenesis before cdp
collateralMap := make(map[string]int)
ap := pk.GetAssetParams(ctx)
for _, a := range ap.Assets {
collateralMap[a.AssetCode] = 1
}
for _, col := range data.Params.CollateralParams {
_, found := collateralMap[col.Denom]
if !found {
panic(fmt.Sprintf("%s collateral not found in pricefeed", col.Denom))
}
}
k.SetParams(ctx, data.Params)
for _, cdp := range data.CDPs {
k.SetCDP(ctx, cdp)
}
k.SetGlobalDebt(ctx, data.GlobalDebt)
}
func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState {
params := k.GetParams(ctx)
cdps, err := k.GetCDPs(ctx, "", sdk.Dec{})
if err != nil {
panic(err)
}
debt := k.GetGlobalDebt(ctx)
return GenesisState{
Params: params,
GlobalDebt: debt,
CDPs: cdps,
}
}

30
x/cdp/handler.go Normal file
View File

@ -0,0 +1,30 @@
package cdp
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Handle all cdp messages.
func NewHandler(keeper Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) {
case MsgCreateOrModifyCDP:
return handleMsgCreateOrModifyCDP(ctx, keeper, msg)
default:
errMsg := fmt.Sprintf("Unrecognized cdp msg type: %T", msg)
return sdk.ErrUnknownRequest(errMsg).Result()
}
}
}
func handleMsgCreateOrModifyCDP(ctx sdk.Context, keeper Keeper, msg MsgCreateOrModifyCDP) sdk.Result {
err := keeper.ModifyCDP(ctx, msg.Sender, msg.CollateralDenom, msg.CollateralChange, msg.DebtChange)
if err != nil {
return err.Result()
}
return sdk.Result{}
}

67
x/cdp/keeper/bank_test.go Normal file
View File

@ -0,0 +1,67 @@
package keeper
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/mock"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
)
// Test the bank functionality of the CDP keeper
func TestKeeper_AddSubtractGetCoins(t *testing.T) {
_, addrs := mock.GeneratePrivKeyAddressPairs(1)
normalAddr := addrs[0]
tests := []struct {
name string
address sdk.AccAddress
shouldAdd bool
amount sdk.Coins
expectedCoins sdk.Coins
}{
{"addNormalAddress", normalAddr, true, cs(c("usdx", 53)), cs(c("usdx", 153), c("kava", 100))},
{"subNormalAddress", normalAddr, false, cs(c("usdx", 53)), cs(c("usdx", 47), c("kava", 100))},
{"addLiquidatorStable", LiquidatorAccountAddress, true, cs(c("usdx", 53)), cs(c("usdx", 153))},
{"subLiquidatorStable", LiquidatorAccountAddress, false, cs(c("usdx", 53)), cs(c("usdx", 47))},
{"addLiquidatorGov", LiquidatorAccountAddress, true, cs(c("kava", 53)), cs(c("usdx", 100))}, // no change to balance
{"subLiquidatorGov", LiquidatorAccountAddress, false, cs(c("kava", 53)), cs(c("usdx", 100))}, // no change to balance
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// setup keeper
mapp, keeper, _, _ := setUpMockAppWithoutGenesis()
// initialize an account with coins
genAcc := auth.BaseAccount{
Address: normalAddr,
Coins: cs(c("usdx", 100), c("kava", 100)),
}
mock.SetGenesis(mapp, []authexported.Account{&genAcc})
// create a new context and setup the liquidator account
header := abci.Header{Height: mapp.LastBlockHeight() + 1}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header)
keeper.setLiquidatorModuleAccount(ctx, LiquidatorModuleAccount{cs(c("usdx", 100))}) // set gov coin "balance" to zero
// perform the test action
var err sdk.Error
if tc.shouldAdd {
_, err = keeper.AddCoins(ctx, tc.address, tc.amount)
} else {
_, err = keeper.SubtractCoins(ctx, tc.address, tc.amount)
}
mapp.EndBlock(abci.RequestEndBlock{})
mapp.Commit()
// check balances are as expected
require.NoError(t, err)
require.Equal(t, tc.expectedCoins, keeper.GetCoins(ctx, tc.address))
})
}
}

499
x/cdp/keeper/keeper.go Normal file
View File

@ -0,0 +1,499 @@
package keeper
import (
"bytes"
"fmt"
"sort"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
"github.com/kava-labs/kava/x/cdp/types"
)
// Keeper cdp Keeper
type Keeper struct {
key sdk.StoreKey
cdc *codec.Codec
paramSubspace subspace.Subspace
pricefeedKeeper types.PricefeedKeeper
bankKeeper types.BankKeeper
}
// NewKeeper creates a new keeper
func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, bk types.BankKeeper) Keeper {
return Keeper{
key: key,
cdc: cdc,
paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()),
pricefeedKeeper: pfk,
bankKeeper: bk,
}
}
// ModifyCDP creates, changes, or deletes a CDP
// TODO can/should this function be split up?
func (k Keeper) ModifyCDP(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string, changeInCollateral sdk.Int, changeInDebt sdk.Int) sdk.Error {
// Phase 1: Get state, make changes in memory and check if they're ok.
// Check collateral type ok
p := k.GetParams(ctx)
if !p.IsCollateralPresent(collateralDenom) { // maybe abstract this logic into GetCDP
return sdk.ErrInternal("collateral type not enabled to create CDPs")
}
// Check the owner has enough collateral and stable coins
if changeInCollateral.IsPositive() { // adding collateral to CDP
ok := k.bankKeeper.HasCoins(ctx, owner, sdk.NewCoins(sdk.NewCoin(collateralDenom, changeInCollateral)))
if !ok {
return sdk.ErrInsufficientCoins("not enough collateral in sender's account")
}
}
if changeInDebt.IsNegative() { // reducing debt, by adding stable coin to CDP
ok := k.bankKeeper.HasCoins(ctx, owner, sdk.NewCoins(sdk.NewCoin("usdx", changeInDebt.Neg())))
if !ok {
return sdk.ErrInsufficientCoins("not enough stable coin in sender's account")
}
}
// Change collateral and debt recorded in CDP
// Get CDP (or create if not exists)
cdp, found := k.GetCDP(ctx, owner, collateralDenom)
if !found {
cdp = types.CDP{Owner: owner, CollateralDenom: collateralDenom, CollateralAmount: sdk.ZeroInt(), Debt: sdk.ZeroInt()}
}
// Add/Subtract collateral and debt
cdp.CollateralAmount = cdp.CollateralAmount.Add(changeInCollateral)
if cdp.CollateralAmount.IsNegative() {
return sdk.ErrInternal(" can't withdraw more collateral than exists in CDP")
}
cdp.Debt = cdp.Debt.Add(changeInDebt)
if cdp.Debt.IsNegative() {
return sdk.ErrInternal("can't pay back more debt than exists in CDP")
}
isUnderCollateralized := cdp.IsUnderCollateralized(
k.pricefeedKeeper.GetCurrentPrice(ctx, cdp.CollateralDenom).Price,
p.GetCollateralParams(cdp.CollateralDenom).LiquidationRatio,
)
if isUnderCollateralized {
return sdk.ErrInternal("Change to CDP would put it below liquidation ratio")
}
// TODO check for dust
// Add/Subtract from global debt limit
gDebt := k.GetGlobalDebt(ctx)
gDebt = gDebt.Add(changeInDebt)
if gDebt.IsNegative() {
return sdk.ErrInternal("global debt can't be negative") // This should never happen if debt per CDP can't be negative
}
if gDebt.GT(p.GlobalDebtLimit) {
return sdk.ErrInternal("change to CDP would put the system over the global debt limit")
}
// Add/Subtract from collateral debt limit
collateralState, found := k.GetCollateralState(ctx, cdp.CollateralDenom)
if !found {
collateralState = types.CollateralState{Denom: cdp.CollateralDenom, TotalDebt: sdk.ZeroInt()} // Already checked that this denom is authorized, so ok to create new CollateralState
}
collateralState.TotalDebt = collateralState.TotalDebt.Add(changeInDebt)
if collateralState.TotalDebt.IsNegative() {
return sdk.ErrInternal("total debt for this collateral type can't be negative") // This should never happen if debt per CDP can't be negative
}
if collateralState.TotalDebt.GT(p.GetCollateralParams(cdp.CollateralDenom).DebtLimit) {
return sdk.ErrInternal("change to CDP would put the system over the debt limit for this collateral type")
}
// Phase 2: Update all the state
// change owner's coins (increase or decrease)
var err sdk.Error
if changeInCollateral.IsNegative() {
_, err = k.bankKeeper.AddCoins(ctx, owner, sdk.NewCoins(sdk.NewCoin(collateralDenom, changeInCollateral.Neg())))
} else {
_, err = k.bankKeeper.SubtractCoins(ctx, owner, sdk.NewCoins(sdk.NewCoin(collateralDenom, changeInCollateral)))
}
if err != nil {
panic(err) // this shouldn't happen because coin balance was checked earlier
}
if changeInDebt.IsNegative() {
_, err = k.bankKeeper.SubtractCoins(ctx, owner, sdk.NewCoins(sdk.NewCoin("usdx", changeInDebt.Neg())))
} else {
_, err = k.bankKeeper.AddCoins(ctx, owner, sdk.NewCoins(sdk.NewCoin("usdx", changeInDebt)))
}
if err != nil {
panic(err) // this shouldn't happen because coin balance was checked earlier
}
// Set CDP
if cdp.CollateralAmount.IsZero() && cdp.Debt.IsZero() { // TODO maybe abstract this logic into SetCDP
k.deleteCDP(ctx, cdp)
} else {
k.SetCDP(ctx, cdp)
}
// set total debts
k.SetGlobalDebt(ctx, gDebt)
k.setCollateralState(ctx, collateralState)
return nil
}
// TODO
// // TransferCDP allows people to transfer ownership of their CDPs to others
// func (k Keeper) TransferCDP(ctx sdk.Context, from sdk.AccAddress, to sdk.AccAddress, collateralDenom string) sdk.Error {
// return nil
// }
// PartialSeizeCDP removes collateral and debt from a CDP and decrements global debt counters. It does not move collateral to another account so is unsafe.
// TODO should this be made safer by moving collateral to liquidatorModuleAccount ? If so how should debt be moved?
func (k Keeper) PartialSeizeCDP(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string, collateralToSeize sdk.Int, debtToSeize sdk.Int) sdk.Error {
// get CDP
cdp, found := k.GetCDP(ctx, owner, collateralDenom)
if !found {
return sdk.ErrInternal("could not find CDP")
}
// Check if CDP is undercollateralized
p := k.GetParams(ctx)
isUnderCollateralized := cdp.IsUnderCollateralized(
k.pricefeedKeeper.GetCurrentPrice(ctx, cdp.CollateralDenom).Price,
p.GetCollateralParams(cdp.CollateralDenom).LiquidationRatio,
)
if !isUnderCollateralized {
return sdk.ErrInternal("CDP is not currently under the liquidation ratio")
}
// Remove Collateral
if collateralToSeize.IsNegative() {
return sdk.ErrInternal("cannot seize negative collateral")
}
cdp.CollateralAmount = cdp.CollateralAmount.Sub(collateralToSeize)
if cdp.CollateralAmount.IsNegative() {
return sdk.ErrInternal("can't seize more collateral than exists in CDP")
}
// Remove Debt
if debtToSeize.IsNegative() {
return sdk.ErrInternal("cannot seize negative debt")
}
cdp.Debt = cdp.Debt.Sub(debtToSeize)
if cdp.Debt.IsNegative() {
return sdk.ErrInternal("can't seize more debt than exists in CDP")
}
// Update debt per collateral type
collateralState, found := k.GetCollateralState(ctx, cdp.CollateralDenom)
if !found {
return sdk.ErrInternal("could not find collateral state")
}
collateralState.TotalDebt = collateralState.TotalDebt.Sub(debtToSeize)
if collateralState.TotalDebt.IsNegative() {
return sdk.ErrInternal("Total debt per collateral type is negative.") // This should not happen given the checks on the CDP.
}
// Note: Global debt is not decremented here. It's only decremented when debt and stable coin are annihilated (aka heal)
// TODO update global seized debt? this is what maker does (named vice in Vat.grab) but it's not used anywhere
// Store updated state
if cdp.CollateralAmount.IsZero() && cdp.Debt.IsZero() { // TODO maybe abstract this logic into SetCDP
k.deleteCDP(ctx, cdp)
} else {
k.SetCDP(ctx, cdp)
}
k.setCollateralState(ctx, collateralState)
return nil
}
// ReduceGlobalDebt decreases the stored global debt counter. It is used by the liquidator when it annihilates debt and stable coin.
// TODO Can the interface between cdp and liquidator modules be improved so that this function doesn't exist?
func (k Keeper) ReduceGlobalDebt(ctx sdk.Context, amount sdk.Int) sdk.Error {
if amount.IsNegative() {
return sdk.ErrInternal("reduction in global debt must be a positive amount")
}
newGDebt := k.GetGlobalDebt(ctx).Sub(amount)
if newGDebt.IsNegative() {
return sdk.ErrInternal("cannot reduce global debt by amount specified")
}
k.SetGlobalDebt(ctx, newGDebt)
return nil
}
func (k Keeper) GetStableDenom() string {
return "usdx"
}
func (k Keeper) GetGovDenom() string {
return "kava"
}
// ---------- Store Wrappers ----------
func (k Keeper) getCDPKeyPrefix(collateralDenom string) []byte {
return bytes.Join(
[][]byte{
[]byte("cdp"),
[]byte(collateralDenom),
},
nil, // no separator
)
}
func (k Keeper) getCDPKey(owner sdk.AccAddress, collateralDenom string) []byte {
return bytes.Join(
[][]byte{
k.getCDPKeyPrefix(collateralDenom),
[]byte(owner.String()),
},
nil, // no separator
)
}
func (k Keeper) GetCDP(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string) (types.CDP, bool) {
// get store
store := ctx.KVStore(k.key)
// get CDP
bz := store.Get(k.getCDPKey(owner, collateralDenom))
// unmarshal
if bz == nil {
return types.CDP{}, false
}
var cdp types.CDP
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &cdp)
return cdp, true
}
func (k Keeper) SetCDP(ctx sdk.Context, cdp types.CDP) {
// get store
store := ctx.KVStore(k.key)
// marshal and set
bz := k.cdc.MustMarshalBinaryLengthPrefixed(cdp)
store.Set(k.getCDPKey(cdp.Owner, cdp.CollateralDenom), bz)
}
func (k Keeper) deleteCDP(ctx sdk.Context, cdp types.CDP) { // TODO should this id the cdp by passing in owner,collateralDenom pair?
// get store
store := ctx.KVStore(k.key)
// delete key
store.Delete(k.getCDPKey(cdp.Owner, cdp.CollateralDenom))
}
// GetCDPs returns all CDPs, optionally filtered by collateral type and liquidation price.
// `price` filters for CDPs that will be below the liquidation ratio when the collateral is at that specified price.
func (k Keeper) GetCDPs(ctx sdk.Context, collateralDenom string, price sdk.Dec) (types.CDPs, sdk.Error) {
// Validate inputs
params := k.GetParams(ctx)
if len(collateralDenom) != 0 && !params.IsCollateralPresent(collateralDenom) {
return nil, sdk.ErrInternal("collateral denom not authorized")
}
if len(collateralDenom) == 0 && !(price.IsNil() || price.IsNegative()) {
return nil, sdk.ErrInternal("cannot specify price without collateral denom")
}
// Get an iterator over CDPs
store := ctx.KVStore(k.key)
iter := sdk.KVStorePrefixIterator(store, k.getCDPKeyPrefix(collateralDenom)) // could be all CDPs is collateralDenom is ""
// Decode CDPs into slice
var cdps types.CDPs
for ; iter.Valid(); iter.Next() {
var cdp types.CDP
k.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &cdp)
cdps = append(cdps, cdp)
}
// Sort by collateral ratio (collateral/debt)
sort.Sort(types.ByCollateralRatio(cdps)) // TODO this doesn't make much sense across different collateral types
// Filter for CDPs that would be under-collateralized at the specified price
// If price is nil or -ve, skip the filtering as it would return all CDPs anyway
if !price.IsNil() && !price.IsNegative() {
var filteredCDPs types.CDPs
for _, cdp := range cdps {
if cdp.IsUnderCollateralized(price, params.GetCollateralParams(collateralDenom).LiquidationRatio) {
filteredCDPs = append(filteredCDPs, cdp)
} else {
break // break early because list is sorted
}
}
cdps = filteredCDPs
}
return cdps, nil
}
var globalDebtKey = []byte("globalDebt")
func (k Keeper) GetGlobalDebt(ctx sdk.Context) sdk.Int {
// get store
store := ctx.KVStore(k.key)
// get bytes
bz := store.Get(globalDebtKey)
// unmarshal
if bz == nil {
panic("global debt not found")
}
var globalDebt sdk.Int
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &globalDebt)
return globalDebt
}
func (k Keeper) SetGlobalDebt(ctx sdk.Context, globalDebt sdk.Int) {
// get store
store := ctx.KVStore(k.key)
// marshal and set
bz := k.cdc.MustMarshalBinaryLengthPrefixed(globalDebt)
store.Set(globalDebtKey, bz)
}
func (k Keeper) getCollateralStateKey(collateralDenom string) []byte {
return []byte(collateralDenom)
}
func (k Keeper) GetCollateralState(ctx sdk.Context, collateralDenom string) (types.CollateralState, bool) {
// get store
store := ctx.KVStore(k.key)
// get bytes
bz := store.Get(k.getCollateralStateKey(collateralDenom))
// unmarshal
if bz == nil {
return types.CollateralState{}, false
}
var collateralState types.CollateralState
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &collateralState)
return collateralState, true
}
func (k Keeper) setCollateralState(ctx sdk.Context, collateralstate types.CollateralState) {
// get store
store := ctx.KVStore(k.key)
// marshal and set
bz := k.cdc.MustMarshalBinaryLengthPrefixed(collateralstate)
store.Set(k.getCollateralStateKey(collateralstate.Denom), bz)
}
// ---------- Weird Bank Stuff ----------
// This only exists because module accounts aren't really a thing yet.
// Also because we need module accounts that allow for burning/minting.
// These functions make the CDP module act as a bank keeper, ie it fulfills the bank.Keeper interface.
// It intercepts calls to send coins to/from the liquidator module account, otherwise passing the calls onto the normal bank keeper.
// Not sure if module accounts are good, but they make the auction module more general:
// - startAuction would just "mints" coins, relying on calling function to decrement them somewhere
// - closeAuction would have to call something specific for the receiver module to accept coins (like liquidationKeeper.AddStableCoins)
// The auction and liquidator modules can probably just use SendCoins to keep things safe (instead of AddCoins and SubtractCoins).
// So they should define their own interfaces which this module should fulfill, rather than this fulfilling the entire bank.Keeper interface.
// bank.Keeper interfaces:
// type SendKeeper interface {
// type ViewKeeper interface {
// GetCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
// HasCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) bool
// Codespace() sdk.CodespaceType
// }
// SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error)
// GetSendEnabled(ctx sdk.Context) bool
// SetSendEnabled(ctx sdk.Context, enabled bool)
// }
// type Keeper interface {
// SendKeeper
// SetCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) sdk.Error
// SubtractCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error)
// AddCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error)
// InputOutputCoins(ctx sdk.Context, inputs []Input, outputs []Output) (sdk.Tags, sdk.Error)
// DelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error)
// UndelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error)
var LiquidatorAccountAddress = sdk.AccAddress([]byte("whatever"))
var liquidatorAccountKey = []byte("liquidatorAccount")
func (k Keeper) GetLiquidatorAccountAddress() sdk.AccAddress {
return LiquidatorAccountAddress
}
type LiquidatorModuleAccount struct {
Coins sdk.Coins // keeps track of seized collateral, surplus usdx, and mints/burns gov coins
}
func (k Keeper) AddCoins(ctx sdk.Context, address sdk.AccAddress, amount sdk.Coins) (sdk.Coins, sdk.Error) {
// intercept module account
if address.Equals(LiquidatorAccountAddress) {
if !amount.IsValid() {
return nil, sdk.ErrInvalidCoins(amount.String())
}
// remove gov token from list
filteredCoins := stripGovCoin(amount)
// add coins to module account
lma := k.getLiquidatorModuleAccount(ctx)
updatedCoins := lma.Coins.Add(filteredCoins)
if updatedCoins.IsAnyNegative() {
return amount, sdk.ErrInsufficientCoins(fmt.Sprintf("insufficient account funds; %s < %s", lma.Coins, amount))
}
lma.Coins = updatedCoins
k.setLiquidatorModuleAccount(ctx, lma)
return updatedCoins, nil
} else {
return k.bankKeeper.AddCoins(ctx, address, amount)
}
}
// TODO abstract stuff better
func (k Keeper) SubtractCoins(ctx sdk.Context, address sdk.AccAddress, amount sdk.Coins) (sdk.Coins, sdk.Error) {
// intercept module account
if address.Equals(LiquidatorAccountAddress) {
if !amount.IsValid() {
return nil, sdk.ErrInvalidCoins(amount.String())
}
// remove gov token from list
filteredCoins := stripGovCoin(amount)
// subtract coins from module account
lma := k.getLiquidatorModuleAccount(ctx)
updatedCoins, isNegative := lma.Coins.SafeSub(filteredCoins)
if isNegative {
return amount, sdk.ErrInsufficientCoins(fmt.Sprintf("insufficient account funds; %s < %s", lma.Coins, amount))
}
lma.Coins = updatedCoins
k.setLiquidatorModuleAccount(ctx, lma)
return updatedCoins, nil
} else {
return k.bankKeeper.SubtractCoins(ctx, address, amount)
}
}
// TODO Should this return anything for the gov coin balance? Currently returns nothing.
func (k Keeper) GetCoins(ctx sdk.Context, address sdk.AccAddress) sdk.Coins {
if address.Equals(LiquidatorAccountAddress) {
return k.getLiquidatorModuleAccount(ctx).Coins
} else {
return k.bankKeeper.GetCoins(ctx, address)
}
}
// TODO test this with unsorted coins
func (k Keeper) HasCoins(ctx sdk.Context, address sdk.AccAddress, amount sdk.Coins) bool {
if address.Equals(LiquidatorAccountAddress) {
return true
} else {
return k.getLiquidatorModuleAccount(ctx).Coins.IsAllGTE(stripGovCoin(amount))
}
}
func (k Keeper) getLiquidatorModuleAccount(ctx sdk.Context) LiquidatorModuleAccount {
// get store
store := ctx.KVStore(k.key)
// get bytes
bz := store.Get(liquidatorAccountKey)
if bz == nil {
return LiquidatorModuleAccount{} // TODO is it safe to do this, or better to initialize the account explicitly
}
// unmarshal
var lma LiquidatorModuleAccount
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &lma)
return lma
}
func (k Keeper) setLiquidatorModuleAccount(ctx sdk.Context, lma LiquidatorModuleAccount) {
store := ctx.KVStore(k.key)
bz := k.cdc.MustMarshalBinaryLengthPrefixed(lma)
store.Set(liquidatorAccountKey, bz)
}
func stripGovCoin(coins sdk.Coins) sdk.Coins {
filteredCoins := sdk.NewCoins()
for _, c := range coins {
if c.Denom != "kava" {
filteredCoins = append(filteredCoins, c)
}
}
return filteredCoins
}

356
x/cdp/keeper/keeper_test.go Normal file
View File

@ -0,0 +1,356 @@
package keeper
import (
"fmt"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/mock"
"github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/pricefeed"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
)
// How could one reduce the number of params in the test cases. Create a table driven test for each of the 4 add/withdraw collateral/debt?
// These are more like app level tests - I think this is a symptom of having 'ModifyCDP' do a lot. Could be easier for testing purposes to break it down.
func TestKeeper_ModifyCDP(t *testing.T) {
_, addrs := mock.GeneratePrivKeyAddressPairs(1)
ownerAddr := addrs[0]
type state struct { // TODO this allows invalid state to be set up, should it?
CDP types.CDP
OwnerCoins sdk.Coins
GlobalDebt sdk.Int
CollateralState types.CollateralState
}
type args struct {
owner sdk.AccAddress
collateralDenom string
changeInCollateral sdk.Int
changeInDebt sdk.Int
}
tests := []struct {
name string
priorState state
price string
// also missing CDPModuleParams
args args
expectPass bool
expectedState state
}{
{
"addCollateralAndDecreaseDebt",
state{types.CDP{ownerAddr, "xrp", i(100), i(2)}, cs(c("xrp", 10), c("usdx", 2)), i(2), types.CollateralState{"xrp", i(2)}},
"10.345",
args{ownerAddr, "xrp", i(10), i(-1)},
true,
state{types.CDP{ownerAddr, "xrp", i(110), i(1)}, cs( /* 0xrp */ c("usdx", 1)), i(1), types.CollateralState{"xrp", i(1)}},
},
{
"removeTooMuchCollateral",
state{types.CDP{ownerAddr, "xrp", i(1000), i(200)}, cs(c("xrp", 10), c("usdx", 10)), i(200), types.CollateralState{"xrp", i(200)}},
"1.00",
args{ownerAddr, "xrp", i(-601), i(0)},
false,
state{types.CDP{ownerAddr, "xrp", i(1000), i(200)}, cs(c("xrp", 10), c("usdx", 10)), i(200), types.CollateralState{"xrp", i(200)}},
},
{
"withdrawTooMuchStableCoin",
state{types.CDP{ownerAddr, "xrp", i(1000), i(200)}, cs(c("xrp", 10), c("usdx", 10)), i(200), types.CollateralState{"xrp", i(200)}},
"1.00",
args{ownerAddr, "xrp", i(0), i(301)},
false,
state{types.CDP{ownerAddr, "xrp", i(1000), i(200)}, cs(c("xrp", 10), c("usdx", 10)), i(200), types.CollateralState{"xrp", i(200)}},
},
{
"createCDPAndWithdrawStable",
state{types.CDP{}, cs(c("xrp", 10), c("usdx", 10)), i(0), types.CollateralState{"xrp", i(0)}},
"1.00",
args{ownerAddr, "xrp", i(5), i(2)},
true,
state{types.CDP{ownerAddr, "xrp", i(5), i(2)}, cs(c("xrp", 5), c("usdx", 12)), i(2), types.CollateralState{"xrp", i(2)}},
},
{
"emptyCDP",
state{types.CDP{ownerAddr, "xrp", i(1000), i(200)}, cs(c("xrp", 10), c("usdx", 201)), i(200), types.CollateralState{"xrp", i(200)}},
"1.00",
args{ownerAddr, "xrp", i(-1000), i(-200)},
true,
state{types.CDP{}, cs(c("xrp", 1010), c("usdx", 1)), i(0), types.CollateralState{"xrp", i(0)}},
},
{
"invalidCollateralType",
state{types.CDP{}, cs(c("shitcoin", 5000000)), i(0), types.CollateralState{}},
"0.000001",
args{ownerAddr, "shitcoin", i(5000000), i(1)}, // ratio of 5:1
false,
state{types.CDP{}, cs(c("shitcoin", 5000000)), i(0), types.CollateralState{}},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// setup keeper
mapp, keeper, _, _ := setUpMockAppWithoutGenesis()
// initialize cdp owner account with coins
genAcc := auth.BaseAccount{
Address: ownerAddr,
Coins: tc.priorState.OwnerCoins,
}
mock.SetGenesis(mapp, []authexported.Account{&genAcc})
// create a new context
header := abci.Header{Height: mapp.LastBlockHeight() + 1}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header)
keeper.SetParams(ctx, defaultParamsSingle())
// setup store state
ap := pricefeed.AssetParams{
Assets: []pricefeed.Asset{
pricefeed.Asset{AssetCode: "xrp", Description: ""},
},
}
keeper.pricefeedKeeper.SetAssetParams(ctx, ap)
_, err := keeper.pricefeedKeeper.SetPrice(
ctx, ownerAddr, "xrp",
sdk.MustNewDecFromStr(tc.price),
sdk.NewInt(ctx.BlockHeight()+10))
if err != nil {
t.Log("test context height", ctx.BlockHeight())
t.Log(err)
t.Log(tc.name)
}
err = keeper.pricefeedKeeper.SetCurrentPrices(ctx)
if err != nil {
t.Log("test context height", ctx.BlockHeight())
t.Log(err)
t.Log(tc.name)
}
if tc.priorState.CDP.CollateralDenom != "" { // check if the prior CDP should be created or not (see if an empty one was specified)
keeper.SetCDP(ctx, tc.priorState.CDP)
}
keeper.SetGlobalDebt(ctx, tc.priorState.GlobalDebt)
if tc.priorState.CollateralState.Denom != "" {
keeper.setCollateralState(ctx, tc.priorState.CollateralState)
}
// call func under test
err = keeper.ModifyCDP(ctx, tc.args.owner, tc.args.collateralDenom, tc.args.changeInCollateral, tc.args.changeInDebt)
mapp.EndBlock(abci.RequestEndBlock{})
mapp.Commit()
// get new state for verification
actualCDP, found := keeper.GetCDP(ctx, tc.args.owner, tc.args.collateralDenom)
if tc.name == "removeTooMuchCollateral" {
t.Log(actualCDP.String())
}
// check for err
if tc.expectPass {
require.NoError(t, err, fmt.Sprint(err))
} else {
require.Error(t, err)
}
actualGDebt := keeper.GetGlobalDebt(ctx)
actualCstate, _ := keeper.GetCollateralState(ctx, tc.args.collateralDenom)
// check state
require.Equal(t, tc.expectedState.CDP, actualCDP)
if tc.expectedState.CDP.CollateralDenom == "" { // if the expected CDP is blank, then expect the CDP to have been deleted (hence not found)
require.False(t, found)
} else {
require.True(t, found)
}
require.Equal(t, tc.expectedState.GlobalDebt, actualGDebt)
require.Equal(t, tc.expectedState.CollateralState, actualCstate)
// check owner balance
mock.CheckBalance(t, mapp, ownerAddr, tc.expectedState.OwnerCoins)
})
}
}
// TODO change to table driven test to test more test cases
func TestKeeper_PartialSeizeCDP(t *testing.T) {
// Setup
const collateral = "xrp"
mapp, keeper, _, _ := setUpMockAppWithoutGenesis()
genAccs, addrs, _, _ := mock.CreateGenAccounts(1, cs(c(collateral, 100)))
testAddr := addrs[0]
mock.SetGenesis(mapp, genAccs)
// setup pricefeed
header := abci.Header{Height: mapp.LastBlockHeight() + 1}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header)
keeper.SetParams(ctx, defaultParamsSingle())
ap := pricefeed.AssetParams{
Assets: []pricefeed.Asset{
pricefeed.Asset{AssetCode: "xrp", Description: ""},
},
}
keeper.pricefeedKeeper.SetAssetParams(ctx, ap)
keeper.pricefeedKeeper.SetPrice(
ctx, sdk.AccAddress{}, collateral,
sdk.MustNewDecFromStr("1.00"),
i(10))
keeper.pricefeedKeeper.SetCurrentPrices(ctx)
// Create CDP
keeper.SetGlobalDebt(ctx, i(0))
err := keeper.ModifyCDP(ctx, testAddr, collateral, i(10), i(5))
require.NoError(t, err)
// Reduce price
keeper.pricefeedKeeper.SetPrice(
ctx, sdk.AccAddress{}, collateral,
sdk.MustNewDecFromStr("0.90"),
i(10))
keeper.pricefeedKeeper.SetCurrentPrices(ctx)
// Seize entire CDP
err = keeper.PartialSeizeCDP(ctx, testAddr, collateral, i(10), i(5))
// Check
require.NoError(t, err)
_, found := keeper.GetCDP(ctx, testAddr, collateral)
require.False(t, found)
collateralState, found := keeper.GetCollateralState(ctx, collateral)
require.True(t, found)
require.Equal(t, sdk.ZeroInt(), collateralState.TotalDebt)
}
func TestKeeper_GetCDPs(t *testing.T) {
// setup keeper
mapp, keeper, _, _ := setUpMockAppWithoutGenesis()
mock.SetGenesis(mapp, []authexported.Account(nil))
header := abci.Header{Height: mapp.LastBlockHeight() + 1}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header)
keeper.SetParams(ctx, defaultParamsMulti())
// setup CDPs
_, addrs := mock.GeneratePrivKeyAddressPairs(2)
cdps := types.CDPs{
{addrs[0], "xrp", i(4000), i(5)},
{addrs[1], "xrp", i(4000), i(2000)},
{addrs[0], "btc", i(10), i(20)},
}
for _, cdp := range cdps {
keeper.SetCDP(ctx, cdp)
}
// Check nil params returns all CDPs
returnedCdps, err := keeper.GetCDPs(ctx, "", sdk.Dec{})
require.NoError(t, err)
require.Equal(t,
types.CDPs{
{addrs[0], "btc", i(10), i(20)},
{addrs[1], "xrp", i(4000), i(2000)},
{addrs[0], "xrp", i(4000), i(5)}},
returnedCdps,
)
// Check correct CDPs filtered by collateral and sorted
returnedCdps, err = keeper.GetCDPs(ctx, "xrp", d("0.00000001"))
require.NoError(t, err)
require.Equal(t,
types.CDPs{
{addrs[1], "xrp", i(4000), i(2000)},
{addrs[0], "xrp", i(4000), i(5)}},
returnedCdps,
)
returnedCdps, err = keeper.GetCDPs(ctx, "xrp", sdk.Dec{})
require.NoError(t, err)
require.Equal(t,
types.CDPs{
{addrs[1], "xrp", i(4000), i(2000)},
{addrs[0], "xrp", i(4000), i(5)}},
returnedCdps,
)
returnedCdps, err = keeper.GetCDPs(ctx, "xrp", d("0.9"))
require.NoError(t, err)
require.Equal(t,
types.CDPs{
{addrs[1], "xrp", i(4000), i(2000)}},
returnedCdps,
)
// Check high price returns no CDPs
returnedCdps, err = keeper.GetCDPs(ctx, "xrp", d("999999999.99"))
require.NoError(t, err)
require.Equal(t,
types.CDPs(nil),
returnedCdps,
)
// Check unauthorized collateral denom returns error
_, err = keeper.GetCDPs(ctx, "a non existent coin", d("0.34023"))
require.Error(t, err)
// Check price without collateral returns error
_, err = keeper.GetCDPs(ctx, "", d("0.34023"))
require.Error(t, err)
// Check deleting a CDP removes it
keeper.deleteCDP(ctx, cdps[0])
returnedCdps, err = keeper.GetCDPs(ctx, "", sdk.Dec{})
require.NoError(t, err)
require.Equal(t,
types.CDPs{
{addrs[0], "btc", i(10), i(20)},
{addrs[1], "xrp", i(4000), i(2000)}},
returnedCdps,
)
}
func TestKeeper_GetSetDeleteCDP(t *testing.T) {
// setup keeper, create CDP
mapp, keeper, _, _ := setUpMockAppWithoutGenesis()
header := abci.Header{Height: mapp.LastBlockHeight() + 1}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header)
keeper.SetParams(ctx, defaultParamsSingle())
_, addrs := mock.GeneratePrivKeyAddressPairs(1)
cdp := types.CDP{addrs[0], "xrp", i(412), i(56)}
// write and read from store
keeper.SetCDP(ctx, cdp)
readCDP, found := keeper.GetCDP(ctx, cdp.Owner, cdp.CollateralDenom)
// check before and after match
require.True(t, found)
require.Equal(t, cdp, readCDP)
// delete auction
keeper.deleteCDP(ctx, cdp)
// check auction does not exist
_, found = keeper.GetCDP(ctx, cdp.Owner, cdp.CollateralDenom)
require.False(t, found)
}
func TestKeeper_GetSetGDebt(t *testing.T) {
// setup keeper, create GDebt
mapp, keeper, _, _ := setUpMockAppWithoutGenesis()
header := abci.Header{Height: mapp.LastBlockHeight() + 1}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header)
keeper.SetParams(ctx, defaultParamsSingle())
gDebt := i(4120000)
// write and read from store
keeper.SetGlobalDebt(ctx, gDebt)
readGDebt := keeper.GetGlobalDebt(ctx)
// check before and after match
require.Equal(t, gDebt, readGDebt)
}
func TestKeeper_GetSetCollateralState(t *testing.T) {
// setup keeper, create CState
mapp, keeper, _, _ := setUpMockAppWithoutGenesis()
header := abci.Header{Height: mapp.LastBlockHeight() + 1}
mapp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := mapp.BaseApp.NewContext(false, header)
keeper.SetParams(ctx, defaultParamsSingle())
collateralState := types.CollateralState{"xrp", i(15400)}
// write and read from store
keeper.setCollateralState(ctx, collateralState)
readCState, found := keeper.GetCollateralState(ctx, collateralState.Denom)
// check before and after match
require.Equal(t, collateralState, readCState)
require.True(t, found)
}

19
x/cdp/keeper/params.go Normal file
View File

@ -0,0 +1,19 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types"
)
// ---------- Module Parameters ----------
// GetParams returns the params from the store
func (k Keeper) GetParams(ctx sdk.Context) types.CdpParams {
var p types.CdpParams
k.paramSubspace.GetParamSet(ctx, &p)
return p
}
// SetParams sets params on the store
func (k Keeper) SetParams(ctx sdk.Context, cdpParams types.CdpParams) {
k.paramSubspace.SetParamSet(ctx, &cdpParams)
}

83
x/cdp/keeper/querier.go Normal file
View File

@ -0,0 +1,83 @@
package keeper
import (
"fmt"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types"
)
func NewQuerier(keeper Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
switch path[0] {
case types.QueryGetCdps:
return queryGetCdps(ctx, req, keeper)
case types.QueryGetParams:
return queryGetParams(ctx, req, keeper)
default:
return nil, sdk.ErrUnknownRequest("unknown cdp query endpoint")
}
}
}
// queryGetCdps fetches CDPs, optionally filtering by any of the query params (in QueryCdpsParams).
// While CDPs do not have an ID, this method can be used to get one CDP by specifying the collateral and owner.
func queryGetCdps(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
// Decode request
var requestParams types.QueryCdpsParams
err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil {
return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err))
}
// Get CDPs
var cdps types.CDPs
if len(requestParams.Owner) != 0 {
if len(requestParams.CollateralDenom) != 0 {
// owner and collateral specified - get a single CDP
cdp, found := keeper.GetCDP(ctx, requestParams.Owner, requestParams.CollateralDenom)
if !found {
cdp = types.CDP{Owner: requestParams.Owner, CollateralDenom: requestParams.CollateralDenom, CollateralAmount: sdk.ZeroInt(), Debt: sdk.ZeroInt()}
}
cdps = types.CDPs{cdp}
} else {
// owner, but no collateral specified - get all CDPs for one address
return nil, sdk.ErrInternal("getting all CDPs belonging to one owner not implemented")
}
} else {
// owner not specified -- get all CDPs or all CDPs of one collateral type, optionally filtered by price
var errSdk sdk.Error // := doesn't work here
cdps, errSdk = keeper.GetCDPs(ctx, requestParams.CollateralDenom, requestParams.UnderCollateralizedAt)
if errSdk != nil {
return nil, errSdk
}
}
// Encode results
bz, err := codec.MarshalJSONIndent(keeper.cdc, cdps)
if err != nil {
return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
}
return bz, nil
}
// queryGetParams fetches the cdp module parameters
// TODO does this need to exist? Can you use cliCtx.QueryStore instead?
func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
// Get params
params := keeper.GetParams(ctx)
// Encode results
bz, err := codec.MarshalJSONIndent(keeper.cdc, params)
if err != nil {
return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
}
return bz, nil
}

View File

@ -0,0 +1,88 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/mock"
"github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/pricefeed"
"github.com/tendermint/tendermint/crypto"
)
// Mock app is an ABCI app with an in memory database.
// This function creates an app, setting up the keepers, routes, begin and end blockers.
// But leaves it to the tests to call InitChain (done by calling mock.SetGenesis)
// The app works by submitting ABCI messages.
// - InitChain sets up the app db from genesis.
// - BeginBlock starts the delivery of a new block
// - DeliverTx delivers a tx
// - EndBlock signals the end of a block
// - Commit ?
func setUpMockAppWithoutGenesis() (*mock.App, Keeper, []sdk.AccAddress, []crypto.PrivKey) {
// Create uninitialized mock app
mapp := mock.NewApp()
// Register codecs
types.RegisterCodec(mapp.Cdc)
// Create keepers
keyCDP := sdk.NewKVStoreKey("cdp")
keyPriceFeed := sdk.NewKVStoreKey(pricefeed.StoreKey)
pk := mapp.ParamsKeeper
priceFeedKeeper := pricefeed.NewKeeper(keyPriceFeed, mapp.Cdc, pk.Subspace(pricefeed.DefaultParamspace).WithKeyTable(pricefeed.ParamKeyTable()), pricefeed.DefaultCodespace)
blacklistedAddrs := make(map[string]bool)
bankKeeper := bank.NewBaseKeeper(mapp.AccountKeeper, pk.Subspace(bank.DefaultParamspace), bank.DefaultCodespace, blacklistedAddrs)
cdpKeeper := NewKeeper(mapp.Cdc, keyCDP, pk.Subspace(types.DefaultParamspace), priceFeedKeeper, bankKeeper)
// Mount and load the stores
err := mapp.CompleteSetup(keyPriceFeed, keyCDP)
if err != nil {
panic("mock app setup failed")
}
// Create a bunch (ie 10) of pre-funded accounts to use for tests
genAccs, addrs, _, privKeys := mock.CreateGenAccounts(10, sdk.NewCoins(sdk.NewInt64Coin("token1", 100), sdk.NewInt64Coin("token2", 100)))
mock.SetGenesis(mapp, genAccs)
return mapp, cdpKeeper, addrs, privKeys
}
// Avoid cluttering test cases with long function name
func i(in int64) sdk.Int { return sdk.NewInt(in) }
func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
func defaultParamsMulti() types.CdpParams {
return types.CdpParams{
GlobalDebtLimit: sdk.NewInt(1000000),
CollateralParams: []types.CollateralParams{
{
Denom: "btc",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt(500000),
},
{
Denom: "xrp",
LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
DebtLimit: sdk.NewInt(500000),
},
},
StableDenoms: []string{"usdx"},
}
}
func defaultParamsSingle() types.CdpParams {
return types.CdpParams{
GlobalDebtLimit: sdk.NewInt(1000000),
CollateralParams: []types.CollateralParams{
{
Denom: "xrp",
LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
DebtLimit: sdk.NewInt(500000),
},
},
StableDenoms: []string{"usdx"},
}
}

132
x/cdp/module.go Normal file
View File

@ -0,0 +1,132 @@
package cdp
import (
"encoding/json"
"github.com/gorilla/mux"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/cdp/client/cli"
"github.com/kava-labs/kava/x/cdp/client/rest"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
)
// AppModuleBasic app module basics object
type AppModuleBasic struct{}
// Name get module name
func (AppModuleBasic) Name() string {
return ModuleName
}
// RegisterCodec register module codec
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
RegisterCodec(cdc)
}
// DefaultGenesis default genesis state
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
}
// ValidateGenesis module validate genesis
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var data GenesisState
err := ModuleCdc.UnmarshalJSON(bz, &data)
if err != nil {
return err
}
return ValidateGenesis(data)
}
// RegisterRESTRoutes registers the REST routes for the cdp module.
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
rest.RegisterRoutes(ctx, rtr)
}
// GetTxCmd returns the root tx command for the cdp module.
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetTxCmd(cdc)
}
// GetQueryCmd returns the root query command for the auction module.
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(StoreKey, cdc)
}
// AppModule app module type
type AppModule struct {
AppModuleBasic
keeper Keeper
pricefeedKeeper PricefeedKeeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper, pricefeedKeeper PricefeedKeeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
pricefeedKeeper: pricefeedKeeper,
}
}
// Name module name
func (AppModule) Name() string {
return ModuleName
}
// RegisterInvariants register module invariants
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// Route module message route name
func (AppModule) Route() string {
return ModuleName
}
// NewHandler module handler
func (am AppModule) NewHandler() sdk.Handler {
return NewHandler(am.keeper)
}
// QuerierRoute module querier route name
func (AppModule) QuerierRoute() string {
return ModuleName
}
// NewQuerierHandler module querier
func (am AppModule) NewQuerierHandler() sdk.Querier {
return NewQuerier(am.keeper)
}
// InitGenesis module init-genesis
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState GenesisState
ModuleCdc.MustUnmarshalJSON(data, &genesisState)
InitGenesis(ctx, am.keeper, am.pricefeedKeeper, genesisState)
return []abci.ValidatorUpdate{}
}
// ExportGenesis module export genesis
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return ModuleCdc.MustMarshalJSON(gs)
}
// BeginBlock module begin-block
func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {}
// EndBlock module end-block
func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}

52
x/cdp/test_common.go Normal file
View File

@ -0,0 +1,52 @@
package cdp
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/mock"
"github.com/kava-labs/kava/x/pricefeed"
)
// Mock app is an ABCI app with an in memory database.
// This function creates an app, setting up the keepers, routes, begin and end blockers.
// But leaves it to the tests to call InitChain (done by calling mock.SetGenesis)
// The app works by submitting ABCI messages.
// - InitChain sets up the app db from genesis.
// - BeginBlock starts the delivery of a new block
// - DeliverTx delivers a tx
// - EndBlock signals the end of a block
// - Commit ?
func setUpMockAppWithoutGenesis() (*mock.App, Keeper, PricefeedKeeper) {
// Create uninitialized mock app
mapp := mock.NewApp()
// Register codecs
RegisterCodec(mapp.Cdc)
// Create keepers
keyCDP := sdk.NewKVStoreKey("cdp")
keyPriceFeed := sdk.NewKVStoreKey(pricefeed.StoreKey)
pk := mapp.ParamsKeeper
priceFeedKeeper := pricefeed.NewKeeper(keyPriceFeed, mapp.Cdc, pk.Subspace(pricefeed.DefaultParamspace).WithKeyTable(pricefeed.ParamKeyTable()), pricefeed.DefaultCodespace)
blacklistedAddrs := make(map[string]bool)
bankKeeper := bank.NewBaseKeeper(mapp.AccountKeeper, pk.Subspace(bank.DefaultParamspace), bank.DefaultCodespace, blacklistedAddrs)
cdpKeeper := NewKeeper(mapp.Cdc, keyCDP, pk.Subspace(DefaultParamspace), priceFeedKeeper, bankKeeper)
// Register routes
mapp.Router().AddRoute("cdp", NewHandler(cdpKeeper))
// Mount and load the stores
err := mapp.CompleteSetup(keyPriceFeed, keyCDP)
if err != nil {
panic("mock app setup failed")
}
return mapp, cdpKeeper, priceFeedKeeper
}
// Avoid cluttering test cases with long function name
func i(in int64) sdk.Int { return sdk.NewInt(in) }
func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }

19
x/cdp/types/codec.go Normal file
View File

@ -0,0 +1,19 @@
package types
import "github.com/cosmos/cosmos-sdk/codec"
// ModuleCdc generic sealed codec to be used throughout module
var ModuleCdc *codec.Codec
func init() {
cdc := codec.New()
RegisterCodec(cdc)
codec.RegisterCrypto(cdc)
ModuleCdc = cdc.Seal()
}
// RegisterCodec registers concrete types on the codec.
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgCreateOrModifyCDP{}, "cdp/MsgCreateOrModifyCDP", nil)
cdc.RegisterConcrete(MsgTransferCDP{}, "cdp/MsgTransferCDP", nil)
}

View File

@ -0,0 +1,22 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
pftypes "github.com/kava-labs/kava/x/pricefeed/types"
)
type BankKeeper interface {
GetCoins(sdk.Context, sdk.AccAddress) sdk.Coins
HasCoins(sdk.Context, sdk.AccAddress, sdk.Coins) bool
AddCoins(sdk.Context, sdk.AccAddress, sdk.Coins) (sdk.Coins, sdk.Error)
SubtractCoins(sdk.Context, sdk.AccAddress, sdk.Coins) (sdk.Coins, sdk.Error)
}
type PricefeedKeeper interface {
GetCurrentPrice(sdk.Context, string) pftypes.CurrentPrice
GetAssetParams(sdk.Context) pftypes.AssetParams
// These are used for testing TODO replace mockApp with keeper in tests to remove these
SetAssetParams(sdk.Context, pftypes.AssetParams)
SetPrice(sdk.Context, sdk.AccAddress, string, sdk.Dec, sdk.Int) (pftypes.PostedPrice, sdk.Error)
SetCurrentPrices(sdk.Context) sdk.Error
}

37
x/cdp/types/genesis.go Normal file
View File

@ -0,0 +1,37 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// GenesisState is the state that must be provided at genesis.
// TODO What is globaldebt and is is separate from the global debt limit in CdpParams
type GenesisState struct {
Params CdpParams `json:"params" yaml:"params"`
GlobalDebt sdk.Int `json:"global_debt" yaml:"global_debt"`
CDPs CDPs `json:"cdps" yaml:"cdps"`
// don't need to setup CollateralStates as they are created as needed
}
// DefaultGenesisState returns a default genesis state
// TODO make this empty, load test values independent
func DefaultGenesisState() GenesisState {
return GenesisState{
Params: DefaultParams(),
GlobalDebt: sdk.ZeroInt(),
CDPs: CDPs{},
}
}
// ValidateGenesis performs basic validation of genesis data returning an
// error for any failed validation criteria.
func ValidateGenesis(data GenesisState) error {
if err := data.Params.Validate(); err != nil {
return err
}
// check global debt is zero - force the chain to always start with zero stable coin, otherwise collateralStatus's will need to be set up as well. - what? This seems indefensible.
return nil
}

15
x/cdp/types/keys.go Normal file
View File

@ -0,0 +1,15 @@
package types
const (
// ModuleName The name that will be used throughout the module
ModuleName = "cdp"
// StoreKey Top level store key where all module items will be stored
StoreKey = ModuleName
// RouterKey Top level router key
RouterKey = ModuleName
// DefaultParamspace default name for parameter store
DefaultParamspace = ModuleName
)

55
x/cdp/types/msg.go Normal file
View File

@ -0,0 +1,55 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// MsgCreateOrModifyCDP creates, adds/removes collateral/stable coin from a cdp
// TODO Make this more user friendly - maybe split into four functions.
type MsgCreateOrModifyCDP struct {
Sender sdk.AccAddress
CollateralDenom string
CollateralChange sdk.Int
DebtChange sdk.Int
}
// NewMsgPlaceBid returns a new MsgPlaceBid.
func NewMsgCreateOrModifyCDP(sender sdk.AccAddress, collateralDenom string, collateralChange sdk.Int, debtChange sdk.Int) MsgCreateOrModifyCDP {
return MsgCreateOrModifyCDP{
Sender: sender,
CollateralDenom: collateralDenom,
CollateralChange: collateralChange,
DebtChange: debtChange,
}
}
// Route return the message type used for routing the message.
func (msg MsgCreateOrModifyCDP) Route() string { return "cdp" }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgCreateOrModifyCDP) Type() string { return "create_modify_cdp" } // TODO snake case?
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgCreateOrModifyCDP) ValidateBasic() sdk.Error {
if msg.Sender.Empty() {
return sdk.ErrInternal("invalid (empty) sender address")
}
// TODO check coin denoms
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgCreateOrModifyCDP) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgCreateOrModifyCDP) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}
// MsgTransferCDP changes the ownership of a cdp
type MsgTransferCDP struct {
// TODO
}

138
x/cdp/types/params.go Normal file
View File

@ -0,0 +1,138 @@
package types
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
)
/*
How this uses the sdk params module:
- Put all the params for this module in one struct `CDPModuleParams`
- Store this in the keeper's paramSubspace under one key
- Provide a function to load the param struct all at once `keeper.GetParams(ctx)`
It's possible to set individual key value pairs within a paramSubspace, but reading and setting them is awkward (an empty variable needs to be created, then Get writes the value into it)
This approach will be awkward if we ever need to write individual parameters (because they're stored all together). If this happens do as the sdk modules do - store parameters separately with custom get/set func for each.
*/
// CdpParams governance parameters for cdp module
type CdpParams struct {
GlobalDebtLimit sdk.Int
CollateralParams []CollateralParams
StableDenoms []string
}
// CollateralParams governance parameters for each collateral type within the cdp module
type CollateralParams struct {
Denom string // Coin name of collateral type
LiquidationRatio sdk.Dec // The ratio (Collateral (priced in stable coin) / Debt) under which a CDP will be liquidated
DebtLimit sdk.Int // Maximum amount of debt allowed to be drawn from this collateral type
//DebtFloor sdk.Int // used to prevent dust
}
// Parameter keys
var (
// ParamStoreKeyAuctionParams Param store key for auction params
KeyGlobalDebtLimit = []byte("GlobalDebtLimit")
KeyCollateralParams = []byte("CollateralParams")
KeyStableDenoms = []byte("StableDenoms")
)
// ParamKeyTable Key declaration for parameters
func ParamKeyTable() subspace.KeyTable {
return subspace.NewKeyTable().RegisterParamSet(&CdpParams{})
}
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
// pairs of auth module's parameters.
// nolint
func (p *CdpParams) ParamSetPairs() subspace.ParamSetPairs {
return subspace.ParamSetPairs{
{KeyGlobalDebtLimit, &p.GlobalDebtLimit},
{KeyCollateralParams, &p.CollateralParams},
{KeyStableDenoms, &p.StableDenoms},
}
}
// String implements fmt.Stringer
func (p CdpParams) String() string {
out := fmt.Sprintf(`Params:
Global Debt Limit: %s
Collateral Params:`,
p.GlobalDebtLimit,
)
for _, cp := range p.CollateralParams {
out += fmt.Sprintf(`
%s
Liquidation Ratio: %s
Debt Limit: %s`,
cp.Denom,
cp.LiquidationRatio,
cp.DebtLimit,
)
}
return out
}
// GetCollateralParams returns params for a specific collateral denom
func (p CdpParams) GetCollateralParams(collateralDenom string) CollateralParams {
// search for matching denom, return
for _, cp := range p.CollateralParams {
if cp.Denom == collateralDenom {
return cp
}
}
// panic if not found, to be safe
panic("collateral params not found in module params")
}
// IsCollateralPresent returns true if the denom is among the collaterals in cdp module
func (p CdpParams) IsCollateralPresent(collateralDenom string) bool {
// search for matching denom, return
for _, cp := range p.CollateralParams {
if cp.Denom == collateralDenom {
return true
}
}
return false
}
// Validate checks that the parameters have valid values.
func (p CdpParams) Validate() error {
collateralDupMap := make(map[string]int)
denomDupMap := make(map[string]int)
for _, collateral := range p.CollateralParams {
_, found := collateralDupMap[collateral.Denom]
if found {
return fmt.Errorf("duplicate denom: %s", collateral.Denom)
}
collateralDupMap[collateral.Denom] = 1
if collateral.DebtLimit.IsNegative() {
return fmt.Errorf("debt limit should be positive, is %s for %s", collateral.DebtLimit, collateral.Denom)
}
// TODO do we want to enforce overcollateralization at this level? -- probably not, as it's technically a governance thing (kevin)
}
if p.GlobalDebtLimit.IsNegative() {
return fmt.Errorf("global debt limit should be positive, is %s", p.GlobalDebtLimit)
}
for _, denom := range p.StableDenoms {
_, found := denomDupMap[denom]
if found {
return fmt.Errorf("duplicate stable denom: %s", denom)
}
denomDupMap[denom] = 1
}
return nil
}
func DefaultParams() CdpParams {
return CdpParams{
GlobalDebtLimit: sdk.NewInt(0),
CollateralParams: []CollateralParams{},
StableDenoms: []string{"usdx"},
}
}

25
x/cdp/types/querier.go Normal file
View File

@ -0,0 +1,25 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
)
const (
QueryGetCdps = "cdps"
QueryGetParams = "params"
RestOwner = "owner"
RestCollateralDenom = "collateralDenom"
RestUnderCollateralizedAt = "underCollateralizedAt"
)
type QueryCdpsParams struct {
CollateralDenom string // get CDPs with this collateral denom
Owner sdk.AccAddress // get CDPs belonging to this owner
UnderCollateralizedAt sdk.Dec // get CDPs that will be below the liquidation ratio when the collateral is at this price.
}
type ModifyCdpRequestBody struct {
BaseReq rest.BaseReq `json:"base_req"`
Cdp CDP `json:"cdp"`
}

77
x/cdp/types/types.go Normal file
View File

@ -0,0 +1,77 @@
package types
import (
"fmt"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// GovDenom asset code of the governance coin
const GovDenom = "kava"
// CDP is the state of a single Collateralized Debt Position.
type CDP struct {
//ID []byte // removing IDs for now to make things simpler
Owner sdk.AccAddress `json:"owner" yaml:"owner"` // Account that authorizes changes to the CDP
CollateralDenom string `json:"collateral_denom" yaml:"collateral_denom"` // Type of collateral stored in this CDP
CollateralAmount sdk.Int `json:"collateral_amount" yaml:"collateral_amount"` // Amount of collateral stored in this CDP
Debt sdk.Int `json:"debt" yaml:"debt"` // Amount of stable coin drawn from this CDP
}
func (cdp CDP) IsUnderCollateralized(price sdk.Dec, liquidationRatio sdk.Dec) bool {
collateralValue := sdk.NewDecFromInt(cdp.CollateralAmount).Mul(price)
minCollateralValue := liquidationRatio.Mul(sdk.NewDecFromInt(cdp.Debt))
return collateralValue.LT(minCollateralValue) // TODO LT or LTE?
}
func (cdp CDP) String() string {
return strings.TrimSpace(fmt.Sprintf(`CDP:
Owner: %s
Collateral: %s
Debt: %s`,
cdp.Owner,
sdk.NewCoin(cdp.CollateralDenom, cdp.CollateralAmount),
sdk.NewCoin("usdx", cdp.Debt),
))
}
type CDPs []CDP
// String implements stringer
func (cdps CDPs) String() string {
out := ""
for _, cdp := range cdps {
out += cdp.String() + "\n"
}
return out
}
// ByCollateralRatio is used to sort CDPs
type ByCollateralRatio CDPs
func (cdps ByCollateralRatio) Len() int { return len(cdps) }
func (cdps ByCollateralRatio) Swap(i, j int) { cdps[i], cdps[j] = cdps[j], cdps[i] }
func (cdps ByCollateralRatio) Less(i, j int) bool {
// Sort by "collateral ratio" ie collateralAmount/Debt
// The comparison is: collat_i/debt_i < collat_j/debt_j
// But to avoid division this can be rearranged to: collat_i*debt_j < collat_j*debt_i
// Provided the values are positive, so check for positive values.
if cdps[i].CollateralAmount.IsNegative() ||
cdps[i].Debt.IsNegative() ||
cdps[j].CollateralAmount.IsNegative() ||
cdps[j].Debt.IsNegative() {
panic("negative collateral and debt not supported in CDPs")
}
// TODO overflows could cause panics
left := cdps[i].CollateralAmount.Mul(cdps[j].Debt)
right := cdps[j].CollateralAmount.Mul(cdps[i].Debt)
return left.LT(right)
}
// CollateralState stores global information tied to a particular collateral type.
type CollateralState struct {
Denom string `json:"denom" yaml:"denom"` // Type of collateral
TotalDebt sdk.Int `json:"total_debt" yaml:"total_debt"` // total debt collateralized by a this coin type
//AccumulatedFees sdk.Int // Ignoring fees for now
}

52
x/liquidator/alias.go Normal file
View File

@ -0,0 +1,52 @@
// nolint
// autogenerated code using github.com/rigelrozanski/multitool
// aliases generated for the following subdirectories:
// ALIASGEN: github.com/kava-labs/kava/x/liquidator/types/
// ALIASGEN: github.com/kava-labs/kava/x/liquidator/keeper/
package liquidator
import (
"github.com/kava-labs/kava/x/liquidator/keeper"
"github.com/kava-labs/kava/x/liquidator/types"
)
const (
ModuleName = types.ModuleName
StoreKey = types.StoreKey
RouterKey = types.RouterKey
QuerierRoute = types.QuerierRoute
DefaultParamspace = types.DefaultParamspace
QueryGetOutstandingDebt = types.QueryGetOutstandingDebt
)
var (
// functions aliases
RegisterCodec = types.RegisterCodec
DefaultGenesisState = types.DefaultGenesisState
ValidateGenesis = types.ValidateGenesis
NewLiquidatorParams = types.NewLiquidatorParams
ParamKeyTable = types.ParamKeyTable
DefaultParams = types.DefaultParams
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
// variable aliases
ModuleCdc = types.ModuleCdc
KeyDebtAuctionSize = types.KeyDebtAuctionSize
KeyCollateralParams = types.KeyCollateralParams
)
type (
CdpKeeper = types.CdpKeeper
BankKeeper = types.BankKeeper
AuctionKeeper = types.AuctionKeeper
GenesisState = types.GenesisState
MsgSeizeAndStartCollateralAuction = types.MsgSeizeAndStartCollateralAuction
MsgStartDebtAuction = types.MsgStartDebtAuction
LiquidatorParams = types.LiquidatorParams
CollateralParams = types.CollateralParams
SeizeAndStartCollateralAuctionRequest = types.SeizeAndStartCollateralAuctionRequest
StartDebtAuctionRequest = types.StartDebtAuctionRequest
SeizedDebt = types.SeizedDebt
Keeper = keeper.Keeper
)

View File

@ -0,0 +1,49 @@
package cli
import (
"fmt"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/spf13/cobra"
"github.com/kava-labs/kava/x/liquidator/types"
)
// GetQueryCmd returns the cli query commands for this module
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
// Group nameservice queries under a subcommand
queryCmd := &cobra.Command{
Use: "liquidator",
Short: "Querying commands for the cdp liquidator",
}
queryCmd.AddCommand(client.GetCommands(
GetCmdGetOutstandingDebt(queryRoute, cdc),
)...)
return queryCmd
}
// GetCmdGetOutstandingDebt queries for the remaining available debt in the liquidator module after settlement with the module's stablecoin balance.
func GetCmdGetOutstandingDebt(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "debt",
Short: "get the outstanding seized debt",
Long: "Get the remaining available debt after settlement with the liquidator's stable coin balance.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetOutstandingDebt), nil)
if err != nil {
return err
}
var outstandingDebt sdk.Int
cdc.MustUnmarshalJSON(res, &outstandingDebt)
return cliCtx.PrintOutput(outstandingDebt)
},
}
}

View File

@ -0,0 +1,98 @@
package cli
import (
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/liquidator/types"
)
// GetTxCmd returns the transaction commands for this module
func GetTxCmd(cdc *codec.Codec) *cobra.Command {
txCmd := &cobra.Command{
Use: "liquidator",
Short: "liquidator transactions subcommands",
}
txCmd.AddCommand(client.PostCommands(
GetCmdSeizeAndStartCollateralAuction(cdc),
GetCmdStartDebtAuction(cdc),
)...)
return txCmd
}
// GetCmdSeizeAndStartCollateralAuction seize funds from a CDP and send to auction
func GetCmdSeizeAndStartCollateralAuction(cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "seize [cdp-owner] [collateral-denom]",
Short: "",
Long: `Seize a fixed amount of collateral and debt from a CDP then start an auction with the collateral.
The amount of collateral seized is given by the 'AuctionSize' module parameter or, if there isn't enough collateral in the CDP, all the CDP's collateral is seized.
Debt is seized in proportion to the collateral seized so that the CDP stays at the same collateral to debt ratio.
A 'forward-reverse' auction is started selling the seized collateral for some stable coin, with a maximum bid of stable coin set to equal the debt seized.
As this is a forward-reverse auction type, if the max stable coin is bid then bidding continues by bidding down the amount of collateral taken by the bidder. At the end, extra collateral is returned to the original CDP owner.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
// Validate inputs
sender := cliCtx.GetFromAddress()
cdpOwner, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
denom := args[1]
// TODO validate denom?
// Prepare and send message
msg := types.MsgSeizeAndStartCollateralAuction{
Sender: sender,
CdpOwner: cdpOwner,
CollateralDenom: denom,
}
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
return cmd
}
func GetCmdStartDebtAuction(cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "mint",
Short: "start a debt auction, minting gov coin to cover debt",
Long: "Start a reverse auction, selling off minted gov coin to raise a fixed amount of stable coin.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
sender := cliCtx.GetFromAddress()
// Prepare and send message
msg := types.MsgStartDebtAuction{
Sender: sender,
}
err := msg.ValidateBasic()
if err != nil {
return err
}
// TODO print out results like auction ID?
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
return cmd
}

View File

@ -0,0 +1,30 @@
package rest
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/kava-labs/kava/x/liquidator/types"
)
// RegisterRoutes - Central function to define routes that get registered by the main application
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc("/liquidator/outstandingdebt", queryDebtHandlerFn(cliCtx)).Methods("GET")
// r.HandleFunc("liquidator/burn", surplusAuctionHandlerFn(cdc, cliCtx).Methods("POST"))
}
func queryDebtHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/liquidator/%s", types.QueryGetOutstandingDebt), nil)
cliCtx = cliCtx.WithHeight(height)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res) // write JSON to response writer
}
}

View File

@ -0,0 +1,13 @@
package rest
import (
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
)
// RegisterRoutes - Central function to define routes that get registered by the main application
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
registerQueryRoutes(cliCtx, r)
registerTxRoutes(cliCtx, r)
}

View File

@ -0,0 +1,73 @@
package rest
import (
"net/http"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/liquidator/types"
)
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc("/liquidator/seize", seizeCdpHandlerFn(cliCtx)).Methods("POST")
r.HandleFunc("/liquidator/mint", debtAuctionHandlerFn(cliCtx)).Methods("POST")
}
func seizeCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Get args from post body
var req types.SeizeAndStartCollateralAuctionRequest
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { // This function writes a response on error
return
}
req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) { // This function writes a response on error
return
}
// Create msg
msg := types.MsgSeizeAndStartCollateralAuction{
req.Sender,
req.CdpOwner,
req.CollateralDenom,
}
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Generate tx and write response
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
}
}
func debtAuctionHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Get args from post body
var req types.StartDebtAuctionRequest
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
return
}
req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) {
return
}
// Create msg
msg := types.MsgStartDebtAuction{
req.Sender,
}
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Generate tx and write response
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
}
}

21
x/liquidator/doc.go Normal file
View File

@ -0,0 +1,21 @@
/*
Package Liquidator settles bad debt from undercollateralized CDPs by seizing them and raising funds through auctions.
Notes
- Missing the debt queue thing from Vow
- seized collateral and usdx are stored in the module account, but debt (aka Sin) is stored in keeper
- The boundary between the liquidator and the cdp modules is messy.
- The CDP type is used in liquidator
- cdp knows about seizing
- seizing of a CDP is split across each module
- recording of debt is split across modules
- liquidator needs get access to stable and gov denoms from the cdp module
TODO
- Is returning unsold collateral to the CDP owner rather than the CDP a problem? It could prevent the CDP from becoming safe again.
- Add some kind of more complete test
- Add constants for the module and route names
- tags
- custom error types, codespace
*/
package liquidator

19
x/liquidator/genesis.go Normal file
View File

@ -0,0 +1,19 @@
package liquidator
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// InitGenesis sets the genesis state in the keeper.
func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) {
keeper.SetParams(ctx, data.Params)
}
// ExportGenesis returns a GenesisState for a given context and keeper.
func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState {
params := keeper.GetParams(ctx)
return GenesisState{
Params: params,
}
}

54
x/liquidator/handler.go Normal file
View File

@ -0,0 +1,54 @@
package liquidator
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Handle all liquidator messages.
func NewHandler(keeper Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) {
case MsgSeizeAndStartCollateralAuction:
return handleMsgSeizeAndStartCollateralAuction(ctx, keeper, msg)
case MsgStartDebtAuction:
return handleMsgStartDebtAuction(ctx, keeper)
// case MsgStartSurplusAuction:
// return handleMsgStartSurplusAuction(ctx, keeper)
default:
errMsg := fmt.Sprintf("Unrecognized liquidator msg type: %T", msg)
return sdk.ErrUnknownRequest(errMsg).Result()
}
}
}
func handleMsgSeizeAndStartCollateralAuction(ctx sdk.Context, keeper Keeper, msg MsgSeizeAndStartCollateralAuction) sdk.Result {
_, err := keeper.SeizeAndStartCollateralAuction(ctx, msg.CdpOwner, msg.CollateralDenom)
if err != nil {
return err.Result()
}
return sdk.Result{} // TODO tags, return auction ID
}
func handleMsgStartDebtAuction(ctx sdk.Context, keeper Keeper) sdk.Result {
// cancel out any debt and stable coins before trying to start auction
keeper.SettleDebt(ctx)
// start an auction
_, err := keeper.StartDebtAuction(ctx)
if err != nil {
return err.Result()
}
return sdk.Result{} // TODO tags, return auction ID
}
// With no stability and liquidation fees, surplus auctions can never be run.
// func handleMsgStartSurplusAuction(ctx sdk.Context, keeper Keeper) sdk.Result {
// // cancel out any debt and stable coins before trying to start auction
// keeper.settleDebt(ctx)
// _, err := keeper.StartSurplusAuction(ctx)
// if err != nil {
// return err.Result()
// }
// return sdk.Result{} // TODO tags
// }

View File

@ -0,0 +1,204 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/liquidator/types"
)
type Keeper struct {
cdc *codec.Codec
paramSubspace subspace.Subspace
key sdk.StoreKey
cdpKeeper types.CdpKeeper
auctionKeeper types.AuctionKeeper
bankKeeper types.BankKeeper
}
func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, paramstore subspace.Subspace, cdpKeeper types.CdpKeeper, auctionKeeper types.AuctionKeeper, bankKeeper types.BankKeeper) Keeper {
subspace := paramstore.WithKeyTable(types.ParamKeyTable())
return Keeper{
cdc: cdc,
paramSubspace: subspace,
key: storeKey,
cdpKeeper: cdpKeeper,
auctionKeeper: auctionKeeper,
bankKeeper: bankKeeper,
}
}
// SeizeAndStartCollateralAuction pulls collateral out of a CDP and sells it in an auction for stable coin. Excess collateral goes to the original CDP owner.
// Known as Cat.bite in maker
// result: stable coin is transferred to module account, collateral is transferred from module account to buyer, (and any excess collateral is transferred to original CDP owner)
func (k Keeper) SeizeAndStartCollateralAuction(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string) (auction.ID, sdk.Error) {
// Get CDP
cdp, found := k.cdpKeeper.GetCDP(ctx, owner, collateralDenom)
if !found {
return 0, sdk.ErrInternal("CDP not found")
}
// Calculate amount of collateral to sell in this auction
paramsMap := make(map[string]types.CollateralParams)
params := k.GetParams(ctx).CollateralParams
for _, cp := range params {
paramsMap[cp.Denom] = cp
}
collateralParams, found := paramsMap[collateralDenom]
if !found {
return 0, sdk.ErrInternal("collateral denom not found")
}
collateralToSell := sdk.MinInt(cdp.CollateralAmount, collateralParams.AuctionSize)
// Calculate the corresponding maximum amount of stable coin to raise TODO test maths
stableToRaise := sdk.NewDecFromInt(collateralToSell).Quo(sdk.NewDecFromInt(cdp.CollateralAmount)).Mul(sdk.NewDecFromInt(cdp.Debt)).RoundInt()
// Seize the collateral and debt from the CDP
err := k.partialSeizeCDP(ctx, owner, collateralDenom, collateralToSell, stableToRaise)
if err != nil {
return 0, err
}
// Start "forward reverse" auction type
lot := sdk.NewCoin(cdp.CollateralDenom, collateralToSell)
maxBid := sdk.NewCoin(k.cdpKeeper.GetStableDenom(), stableToRaise)
auctionID, err := k.auctionKeeper.StartForwardReverseAuction(ctx, k.cdpKeeper.GetLiquidatorAccountAddress(), lot, maxBid, owner)
if err != nil {
panic(err) // TODO how can errors here be handled to be safe with the state update in PartialSeizeCDP?
}
return auctionID, nil
}
// StartDebtAuction sells off minted gov coin to raise set amounts of stable coin.
// Known as Vow.flop in maker
// result: minted gov coin moved to highest bidder, stable coin moved to moduleAccount
func (k Keeper) StartDebtAuction(ctx sdk.Context) (auction.ID, sdk.Error) {
// Ensure amount of seized stable coin is 0 (ie Joy = 0)
stableCoins := k.bankKeeper.GetCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress()).AmountOf(k.cdpKeeper.GetStableDenom())
if !stableCoins.IsZero() {
return 0, sdk.ErrInternal("debt auction cannot be started as there is outstanding stable coins")
}
// check the seized debt is above a threshold
params := k.GetParams(ctx)
seizedDebt := k.GetSeizedDebt(ctx)
if seizedDebt.Available().LT(params.DebtAuctionSize) {
return 0, sdk.ErrInternal("not enough seized debt to start an auction")
}
// start reverse auction, selling minted gov coin for stable coin
auctionID, err := k.auctionKeeper.StartReverseAuction(
ctx,
k.cdpKeeper.GetLiquidatorAccountAddress(),
sdk.NewCoin(k.cdpKeeper.GetStableDenom(), params.DebtAuctionSize),
sdk.NewInt64Coin(k.cdpKeeper.GetGovDenom(), 2^255-1), // TODO is there a way to avoid potentially minting infinite gov coin?
)
if err != nil {
return 0, err
}
// Record amount of debt sent for auction. Debt can only be reduced in lock step with reducing stable coin
seizedDebt.SentToAuction = seizedDebt.SentToAuction.Add(params.DebtAuctionSize)
k.setSeizedDebt(ctx, seizedDebt)
return auctionID, nil
}
// With no stability and liquidation fees, surplus auctions can never be run.
// StartSurplusAuction sells off excess stable coin in exchange for gov coin, which is burned
// Known as Vow.flap in maker
// result: stable coin removed from module account (eventually to buyer), gov coin transferred to module account
// func (k Keeper) StartSurplusAuction(ctx sdk.Context) (auction.ID, sdk.Error) {
// // TODO ensure seized debt is 0
// // check there is enough surplus to be sold
// surplus := k.bankKeeper.GetCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress()).AmountOf(k.cdpKeeper.GetStableDenom())
// if surplus.LT(SurplusAuctionSize) {
// return 0, sdk.ErrInternal("not enough surplus stable coin to start an auction")
// }
// // start normal auction, selling stable coin
// auctionID, err := k.auctionKeeper.StartForwardAuction(
// ctx,
// k.cdpKeeper.GetLiquidatorAccountAddress(),
// sdk.NewCoin(k.cdpKeeper.GetStableDenom(), SurplusAuctionSize),
// sdk.NewInt64Coin(k.cdpKeeper.GetGovDenom(), 0),
// )
// if err != nil {
// return 0, err
// }
// // Starting the auction will remove coins from the account, so they don't need modified here.
// return auctionID, nil
// }
// PartialSeizeCDP seizes some collateral and debt from an under-collateralized CDP.
func (k Keeper) partialSeizeCDP(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string, collateralToSeize sdk.Int, debtToSeize sdk.Int) sdk.Error { // aka Cat.bite
// Seize debt and collateral in the cdp module. This also validates the inputs.
err := k.cdpKeeper.PartialSeizeCDP(ctx, owner, collateralDenom, collateralToSeize, debtToSeize)
if err != nil {
return err // cdp could be not found, or not under collateralized, or inputs invalid
}
// increment the total seized debt (Awe) by cdp.debt
seizedDebt := k.GetSeizedDebt(ctx)
seizedDebt.Total = seizedDebt.Total.Add(debtToSeize)
k.setSeizedDebt(ctx, seizedDebt)
// add cdp.collateral amount of coins to the moduleAccount (so they can be transferred to the auction later)
coins := sdk.NewCoins(sdk.NewCoin(collateralDenom, collateralToSeize))
_, err = k.bankKeeper.AddCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress(), coins)
if err != nil {
panic(err) // TODO this shouldn't happen?
}
return nil
}
// SettleDebt removes equal amounts of debt and stable coin from the liquidator's reserves (and also updates the global debt in the cdp module).
// This is called in the handler when a debt or surplus auction is started
// TODO Should this be called with an amount, rather than annihilating the maximum?
func (k Keeper) SettleDebt(ctx sdk.Context) sdk.Error {
// Calculate max amount of debt and stable coins that can be settled (ie annihilated)
debt := k.GetSeizedDebt(ctx)
stableCoins := k.bankKeeper.GetCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress()).AmountOf(k.cdpKeeper.GetStableDenom())
settleAmount := sdk.MinInt(debt.Total, stableCoins)
// Call cdp module to reduce GlobalDebt. This can fail if genesis not set
err := k.cdpKeeper.ReduceGlobalDebt(ctx, settleAmount)
if err != nil {
return err
}
// Decrement total seized debt (also decrement from SentToAuction debt)
updatedDebt, err := debt.Settle(settleAmount)
if err != nil {
return err // this should not error in this context
}
k.setSeizedDebt(ctx, updatedDebt)
// Subtract stable coin from moduleAccout
k.bankKeeper.SubtractCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress(), sdk.Coins{sdk.NewCoin(k.cdpKeeper.GetStableDenom(), settleAmount)})
return nil
}
// ---------- Store Wrappers ----------
func (k Keeper) getSeizedDebtKey() []byte {
return []byte("seizedDebt")
}
func (k Keeper) GetSeizedDebt(ctx sdk.Context) types.SeizedDebt {
store := ctx.KVStore(k.key)
bz := store.Get(k.getSeizedDebtKey())
if bz == nil {
// TODO make initial seized debt and CDPs configurable at genesis, then panic here if not found
bz = k.cdc.MustMarshalBinaryLengthPrefixed(types.SeizedDebt{
Total: sdk.ZeroInt(),
SentToAuction: sdk.ZeroInt()})
}
var seizedDebt types.SeizedDebt
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &seizedDebt)
return seizedDebt
}
func (k Keeper) setSeizedDebt(ctx sdk.Context, debt types.SeizedDebt) {
store := ctx.KVStore(k.key)
bz := k.cdc.MustMarshalBinaryLengthPrefixed(debt)
store.Set(k.getSeizedDebtKey(), bz)
}

View File

@ -0,0 +1,136 @@
package keeper
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/mock"
"github.com/stretchr/testify/require"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/liquidator/types"
"github.com/kava-labs/kava/x/pricefeed"
)
func TestKeeper_SeizeAndStartCollateralAuction(t *testing.T) {
// Setup
ctx, k := setupTestKeepers()
_, addrs := mock.GeneratePrivKeyAddressPairs(1)
pricefeed.InitGenesis(ctx, k.pricefeedKeeper, pricefeedGenesis())
k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("8000.00"), i(999999999))
k.pricefeedKeeper.SetCurrentPrices(ctx)
cdp.InitGenesis(ctx, k.cdpKeeper, k.pricefeedKeeper, cdpDefaultGenesis())
dp := defaultParams()
k.liquidatorKeeper.SetParams(ctx, dp)
k.bankKeeper.AddCoins(ctx, addrs[0], cs(c("btc", 100)))
k.cdpKeeper.ModifyCDP(ctx, addrs[0], "btc", i(3), i(16000))
k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("7999.99"), i(999999999))
k.pricefeedKeeper.SetCurrentPrices(ctx)
// Run test function
auctionID, err := k.liquidatorKeeper.SeizeAndStartCollateralAuction(ctx, addrs[0], "btc")
// Check CDP
require.NoError(t, err)
cdp, found := k.cdpKeeper.GetCDP(ctx, addrs[0], "btc")
require.True(t, found)
require.Equal(t, cdp.CollateralAmount, i(2)) // original amount - params.CollateralAuctionSize
require.Equal(t, cdp.Debt, i(10667)) // original debt scaled by amount of collateral removed
// Check auction exists
_, found = k.auctionKeeper.GetAuction(ctx, auctionID)
require.True(t, found)
// TODO check auction values are correct?
}
func TestKeeper_StartDebtAuction(t *testing.T) {
// Setup
ctx, k := setupTestKeepers()
k.liquidatorKeeper.SetParams(ctx, defaultParams())
initSDebt := types.SeizedDebt{i(2000), i(0)}
k.liquidatorKeeper.setSeizedDebt(ctx, initSDebt)
// Execute
auctionID, err := k.liquidatorKeeper.StartDebtAuction(ctx)
// Check
require.NoError(t, err)
require.Equal(t,
types.SeizedDebt{
initSDebt.Total,
initSDebt.SentToAuction.Add(k.liquidatorKeeper.GetParams(ctx).DebtAuctionSize),
},
k.liquidatorKeeper.GetSeizedDebt(ctx),
)
_, found := k.auctionKeeper.GetAuction(ctx, auctionID)
require.True(t, found)
// TODO check auction values are correct?
}
// func TestKeeper_StartSurplusAuction(t *testing.T) {
// // Setup
// ctx, k := setupTestKeepers()
// initSurplus := i(2000)
// k.liquidatorKeeper.bankKeeper.AddCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress(), cs(sdk.NewCoin(k.cdpKeeper.GetStableDenom(), initSurplus)))
// k.liquidatorKeeper.setSeizedDebt(ctx, i(0))
// // Execute
// auctionID, err := k.liquidatorKeeper.StartSurplusAuction(ctx)
// // Check
// require.NoError(t, err)
// require.Equal(t,
// initSurplus.Sub(SurplusAuctionSize),
// k.liquidatorKeeper.bankKeeper.GetCoins(ctx,
// k.cdpKeeper.GetLiquidatorAccountAddress(),
// ).AmountOf(k.cdpKeeper.GetStableDenom()),
// )
// _, found := k.auctionKeeper.GetAuction(ctx, auctionID)
// require.True(t, found)
// }
func TestKeeper_partialSeizeCDP(t *testing.T) {
// Setup
ctx, k := setupTestKeepers()
_, addrs := mock.GeneratePrivKeyAddressPairs(1)
pricefeed.InitGenesis(ctx, k.pricefeedKeeper, pricefeedGenesis())
k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("8000.00"), i(999999999))
k.pricefeedKeeper.SetCurrentPrices(ctx)
k.bankKeeper.AddCoins(ctx, addrs[0], cs(c("btc", 100)))
cdp.InitGenesis(ctx, k.cdpKeeper, k.pricefeedKeeper, cdpDefaultGenesis())
k.liquidatorKeeper.SetParams(ctx, defaultParams())
k.cdpKeeper.ModifyCDP(ctx, addrs[0], "btc", i(3), i(16000))
k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("7999.99"), i(999999999))
k.pricefeedKeeper.SetCurrentPrices(ctx)
// Run test function
err := k.liquidatorKeeper.partialSeizeCDP(ctx, addrs[0], "btc", i(2), i(10000))
// Check
require.NoError(t, err)
cdp, found := k.cdpKeeper.GetCDP(ctx, addrs[0], "btc")
require.True(t, found)
require.Equal(t, i(1), cdp.CollateralAmount)
require.Equal(t, i(6000), cdp.Debt)
}
func TestKeeper_GetSetSeizedDebt(t *testing.T) {
// Setup
ctx, k := setupTestKeepers()
debt := types.SeizedDebt{i(234247645), i(2343)}
// Run test function
k.liquidatorKeeper.setSeizedDebt(ctx, debt)
readDebt := k.liquidatorKeeper.GetSeizedDebt(ctx)
// Check
require.Equal(t, debt, readDebt)
}

View File

@ -0,0 +1,18 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/liquidator/types"
)
// GetParams returns the params for liquidator module
func (k Keeper) GetParams(ctx sdk.Context) types.LiquidatorParams {
var params types.LiquidatorParams
k.paramSubspace.GetParamSet(ctx, &params)
return params
}
// SetParams sets params for the liquidator module
func (k Keeper) SetParams(ctx sdk.Context, params types.LiquidatorParams) {
k.paramSubspace.SetParamSet(ctx, &params)
}

View File

@ -0,0 +1,45 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/liquidator/types"
abci "github.com/tendermint/tendermint/abci/types"
)
func NewQuerier(keeper Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
switch path[0] {
case types.QueryGetOutstandingDebt:
return queryGetOutstandingDebt(ctx, path[1:], req, keeper)
// case QueryGetSurplus:
// return queryGetSurplus()
default:
return nil, sdk.ErrUnknownRequest("unknown liquidator query endpoint")
}
}
}
func queryGetOutstandingDebt(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
// Calculate the remaining seized debt after settling with the liquidator's stable coins.
stableCoins := keeper.bankKeeper.GetCoins(
ctx,
keeper.cdpKeeper.GetLiquidatorAccountAddress(),
).AmountOf(keeper.cdpKeeper.GetStableDenom())
seizedDebt := keeper.GetSeizedDebt(ctx)
settleAmount := sdk.MinInt(seizedDebt.Total, stableCoins)
seizedDebt, err := seizedDebt.Settle(settleAmount)
if err != nil {
return nil, err // this shouldn't error in this context
}
// Get the available debt after settling
oustandingDebt := seizedDebt.Available()
// Encode and return
bz, err2 := codec.MarshalJSONIndent(keeper.cdc, oustandingDebt)
if err2 != nil {
return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error()))
}
return bz, nil
}

View File

@ -0,0 +1,170 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/params"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/log"
dbm "github.com/tendermint/tm-db"
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/liquidator/types"
"github.com/kava-labs/kava/x/pricefeed"
)
// Avoid cluttering test cases with long function name
func i(in int64) sdk.Int { return sdk.NewInt(in) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
type keepers struct {
paramsKeeper params.Keeper
accountKeeper auth.AccountKeeper
bankKeeper bank.Keeper
pricefeedKeeper pricefeed.Keeper
auctionKeeper auction.Keeper
cdpKeeper cdp.Keeper
liquidatorKeeper Keeper
}
func setupTestKeepers() (sdk.Context, keepers) {
// Setup in memory database
keyParams := sdk.NewKVStoreKey(params.StoreKey)
tkeyParams := sdk.NewTransientStoreKey(params.TStoreKey)
keyAcc := sdk.NewKVStoreKey(auth.StoreKey)
keyPriceFeed := sdk.NewKVStoreKey(pricefeed.StoreKey)
keyCDP := sdk.NewKVStoreKey("cdp")
keyAuction := sdk.NewKVStoreKey("auction")
keyLiquidator := sdk.NewKVStoreKey("liquidator")
db := dbm.NewMemDB()
ms := store.NewCommitMultiStore(db)
ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db)
ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db)
ms.MountStoreWithDB(keyAcc, sdk.StoreTypeIAVL, db)
ms.MountStoreWithDB(keyPriceFeed, sdk.StoreTypeIAVL, db)
ms.MountStoreWithDB(keyCDP, sdk.StoreTypeIAVL, db)
ms.MountStoreWithDB(keyAuction, sdk.StoreTypeIAVL, db)
ms.MountStoreWithDB(keyLiquidator, sdk.StoreTypeIAVL, db)
err := ms.LoadLatestVersion()
if err != nil {
panic(err)
}
// Create Codec
cdc := makeTestCodec()
// Create Keepers
paramsKeeper := params.NewKeeper(cdc, keyParams, tkeyParams, params.DefaultCodespace)
accountKeeper := auth.NewAccountKeeper(
cdc,
keyAcc,
paramsKeeper.Subspace(auth.DefaultParamspace),
auth.ProtoBaseAccount,
)
blacklistedAddrs := make(map[string]bool)
bankKeeper := bank.NewBaseKeeper(
accountKeeper,
paramsKeeper.Subspace(bank.DefaultParamspace),
bank.DefaultCodespace,
blacklistedAddrs,
)
pricefeedKeeper := pricefeed.NewKeeper(keyPriceFeed, cdc, paramsKeeper.Subspace(pricefeed.DefaultParamspace).WithKeyTable(pricefeed.ParamKeyTable()), pricefeed.DefaultCodespace)
cdpKeeper := cdp.NewKeeper(
cdc,
keyCDP,
paramsKeeper.Subspace(cdp.DefaultParamspace),
pricefeedKeeper,
bankKeeper,
)
auctionKeeper := auction.NewKeeper(cdc, cdpKeeper, keyAuction, paramsKeeper.Subspace(auction.DefaultParamspace)) // Note: cdp keeper stands in for bank keeper
liquidatorKeeper := NewKeeper(
cdc,
keyLiquidator,
paramsKeeper.Subspace(types.DefaultParamspace),
cdpKeeper,
auctionKeeper,
cdpKeeper,
) // Note: cdp keeper stands in for bank keeper
// Create context
ctx := sdk.NewContext(ms, abci.Header{ChainID: "testchain"}, false, log.NewNopLogger())
return ctx, keepers{
paramsKeeper,
accountKeeper,
bankKeeper,
pricefeedKeeper,
auctionKeeper,
cdpKeeper,
liquidatorKeeper,
}
}
func makeTestCodec() *codec.Codec {
var cdc = codec.New()
auth.RegisterCodec(cdc)
bank.RegisterCodec(cdc)
pricefeed.RegisterCodec(cdc)
auction.RegisterCodec(cdc)
cdp.RegisterCodec(cdc)
types.RegisterCodec(cdc)
sdk.RegisterCodec(cdc)
codec.RegisterCrypto(cdc)
return cdc
}
func defaultParams() types.LiquidatorParams {
return types.LiquidatorParams{
DebtAuctionSize: sdk.NewInt(1000),
CollateralParams: []types.CollateralParams{
{
Denom: "btc",
AuctionSize: sdk.NewInt(1),
},
},
}
}
func cdpDefaultGenesis() cdp.GenesisState {
return cdp.GenesisState{
cdp.CdpParams{
GlobalDebtLimit: sdk.NewInt(1000000),
CollateralParams: []cdp.CollateralParams{
{
Denom: "btc",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt(500000),
},
},
},
sdk.ZeroInt(),
cdp.CDPs{},
}
}
func pricefeedGenesis() pricefeed.GenesisState {
ap := pricefeed.AssetParams{
Assets: []pricefeed.Asset{
pricefeed.Asset{AssetCode: "btc", Description: "a description"},
},
}
return pricefeed.GenesisState{
AssetParams: ap,
OracleParams: pricefeed.DefaultOracleParams(),
PostedPrices: []pricefeed.PostedPrice{
pricefeed.PostedPrice{
AssetCode: "btc",
OracleAddress: "",
Price: sdk.MustNewDecFromStr("8000.00"),
Expiry: sdk.NewInt(999999999),
},
},
}
}

130
x/liquidator/module.go Normal file
View File

@ -0,0 +1,130 @@
package liquidator
import (
"encoding/json"
"github.com/gorilla/mux"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/liquidator/client/cli"
"github.com/kava-labs/kava/x/liquidator/client/rest"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
)
// AppModuleBasic app module basics object
type AppModuleBasic struct{}
// Name get module name
func (AppModuleBasic) Name() string {
return ModuleName
}
// RegisterCodec register module codec
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
RegisterCodec(cdc)
}
// DefaultGenesis default genesis state
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
}
// ValidateGenesis module validate genesis
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var data GenesisState
err := ModuleCdc.UnmarshalJSON(bz, &data)
if err != nil {
return err
}
return ValidateGenesis(data)
}
// RegisterRESTRoutes registers the REST routes for the liquidator module.
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
rest.RegisterRoutes(ctx, rtr)
}
// GetTxCmd returns the root tx command for the liquidator module.
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetTxCmd(cdc)
}
// GetQueryCmd returns the root query command for the auction module.
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(StoreKey, cdc)
}
// AppModule app module type
type AppModule struct {
AppModuleBasic
keeper Keeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
}
}
// Name module name
func (AppModule) Name() string {
return ModuleName
}
// RegisterInvariants register module invariants
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// Route module message route name
func (AppModule) Route() string {
return ModuleName
}
// NewHandler module handler
func (am AppModule) NewHandler() sdk.Handler {
return NewHandler(am.keeper)
}
// QuerierRoute module querier route name
func (AppModule) QuerierRoute() string {
return ModuleName
}
// NewQuerierHandler module querier
func (am AppModule) NewQuerierHandler() sdk.Querier {
return NewQuerier(am.keeper)
}
// InitGenesis module init-genesis
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState GenesisState
ModuleCdc.MustUnmarshalJSON(data, &genesisState)
InitGenesis(ctx, am.keeper, genesisState)
return []abci.ValidatorUpdate{}
}
// ExportGenesis module export genesis
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return ModuleCdc.MustMarshalJSON(gs)
}
// BeginBlock module begin-block
func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {}
// EndBlock module end-block
func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}

View File

@ -0,0 +1,20 @@
package types
import "github.com/cosmos/cosmos-sdk/codec"
// ModuleCdc module level codec
var ModuleCdc = codec.New()
func init() {
cdc := codec.New()
RegisterCodec(cdc)
codec.RegisterCrypto(cdc)
ModuleCdc = cdc.Seal()
}
// RegisterCodec registers concrete types on the codec.
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgSeizeAndStartCollateralAuction{}, "liquidator/MsgSeizeAndStartCollateralAuction", nil)
cdc.RegisterConcrete(MsgStartDebtAuction{}, "liquidator/MsgStartDebtAuction", nil)
// cdc.RegisterConcrete(MsgStartSurplusAuction{}, "liquidator/MsgStartSurplusAuction", nil)
}

View File

@ -0,0 +1,32 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/cdp"
)
// CdpKeeper expected interface for the cdp keeper
type CdpKeeper interface {
GetCDP(sdk.Context, sdk.AccAddress, string) (cdp.CDP, bool)
PartialSeizeCDP(sdk.Context, sdk.AccAddress, string, sdk.Int, sdk.Int) sdk.Error
ReduceGlobalDebt(sdk.Context, sdk.Int) sdk.Error
GetStableDenom() string // TODO can this be removed somehow?
GetGovDenom() string
GetLiquidatorAccountAddress() sdk.AccAddress // This won't need to exist once the module account is defined in this module (instead of in the cdp module)
}
// BankKeeper expected interface for the bank keeper
type BankKeeper interface {
GetCoins(sdk.Context, sdk.AccAddress) sdk.Coins
AddCoins(sdk.Context, sdk.AccAddress, sdk.Coins) (sdk.Coins, sdk.Error)
SubtractCoins(sdk.Context, sdk.AccAddress, sdk.Coins) (sdk.Coins, sdk.Error)
}
// AuctionKeeper expected interface for the auction keeper
type AuctionKeeper interface {
StartForwardAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin) (auction.ID, sdk.Error)
StartReverseAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin) (auction.ID, sdk.Error)
StartForwardReverseAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin, sdk.AccAddress) (auction.ID, sdk.Error)
}

View File

@ -0,0 +1,22 @@
package types
// GenesisState is the state that must be provided at genesis.
type GenesisState struct {
Params LiquidatorParams `json:"liquidator_params" yaml:"liquidator_params"`
}
// DefaultGenesisState returns a default genesis state
// TODO pick better values
func DefaultGenesisState() GenesisState {
return GenesisState{
DefaultParams(),
}
}
// ValidateGenesis performs basic validation of genesis data returning an error for any failed validation criteria.
func ValidateGenesis(data GenesisState) error {
if err := data.Params.Validate(); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,18 @@
package types
const (
// ModuleName is the name of the module
ModuleName = "liquidator"
// StoreKey is the store key string for liquidator
StoreKey = ModuleName
// RouterKey is the message route for liquidator
RouterKey = ModuleName
// QuerierRoute is the querier route for liquidator
QuerierRoute = ModuleName
// DefaultParamspace default name for parameter store
DefaultParamspace = ModuleName
)

97
x/liquidator/types/msg.go Normal file
View File

@ -0,0 +1,97 @@
package types
import sdk "github.com/cosmos/cosmos-sdk/types"
/*
Message types for starting various auctions.
Note: these message types are not final and will likely change.
Design options and problems:
- msgs that only start auctions
- senders have to pay fees
- these msgs cannot be bundled into a tx with a PlaceBid msg because PlaceBid requires an auction ID
- msgs that start auctions and place an initial bid
- place bid can fail, leaving auction without bids which is similar to first case
- no msgs, auctions started automatically
- running this as an endblocker adds complexity and potential vulnerabilities
*/
// MsgSeizeAndStartCollateralAuction siezes a cdp that is below liquidation ratio and starts an auction for the collateral
type MsgSeizeAndStartCollateralAuction struct {
Sender sdk.AccAddress // only needed to pay the tx fees
CdpOwner sdk.AccAddress
CollateralDenom string
}
// Route return the message type used for routing the message.
func (msg MsgSeizeAndStartCollateralAuction) Route() string { return "liquidator" }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgSeizeAndStartCollateralAuction) Type() string { return "seize_and_start_auction" } // TODO snake case?
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgSeizeAndStartCollateralAuction) ValidateBasic() sdk.Error {
if msg.Sender.Empty() {
return sdk.ErrInternal("invalid (empty) sender address")
}
if msg.CdpOwner.Empty() {
return sdk.ErrInternal("invalid (empty) CDP owner address")
}
// TODO check coin denoms
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgSeizeAndStartCollateralAuction) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgSeizeAndStartCollateralAuction) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}
// MsgStartDebtAuction starts an auction of gov tokens for stable tokens
type MsgStartDebtAuction struct {
Sender sdk.AccAddress // only needed to pay the tx fees
}
// Route returns the route for this message
func (msg MsgStartDebtAuction) Route() string { return "liquidator" }
// Type returns the type for this message
func (msg MsgStartDebtAuction) Type() string { return "start_debt_auction" }
// ValidateBasic simple validation check
func (msg MsgStartDebtAuction) ValidateBasic() sdk.Error {
if msg.Sender.Empty() {
return sdk.ErrInternal("invalid (empty) sender address")
}
return nil
}
// GetSignBytes returns canonical byte representation of the message
func (msg MsgStartDebtAuction) GetSignBytes() []byte {
return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg))
}
// GetSigners returns the addresses of the signers of the message
func (msg MsgStartDebtAuction) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Sender} }
// With no stability and liquidation fees, surplus auctions can never be run.
// type MsgStartSurplusAuction struct {
// Sender sdk.AccAddress // only needed to pay the tx fees
// }
// func (msg MsgStartSurplusAuction) Route() string { return "liquidator" }
// func (msg MsgStartSurplusAuction) Type() string { return "start_surplus_auction" } // TODO snake case?
// func (msg MsgStartSurplusAuction) ValidateBasic() sdk.Error {
// if msg.Sender.Empty() {
// return sdk.ErrInternal("invalid (empty) sender address")
// }
// return nil
// }
// func (msg MsgStartSurplusAuction) GetSignBytes() []byte {
// return sdk.MustSortJSON(msgCdc.MustMarshalJSON(msg))
// }
// func (msg MsgStartSurplusAuction) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Sender} }

View File

@ -0,0 +1,100 @@
package types
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
)
// Parameter keys
var (
KeyDebtAuctionSize = []byte("DebtAuctionSize")
KeyCollateralParams = []byte("CollateralParams")
)
// LiquidatorParams store params for the liquidator module
type LiquidatorParams struct {
DebtAuctionSize sdk.Int
//SurplusAuctionSize sdk.Int
CollateralParams []CollateralParams
}
// NewLiquidatorParams returns a new params object for the liquidator module
func NewLiquidatorParams(debtAuctionSize sdk.Int, collateralParams []CollateralParams) LiquidatorParams {
return LiquidatorParams{
DebtAuctionSize: debtAuctionSize,
CollateralParams: collateralParams,
}
}
// String implements fmt.Stringer
func (p LiquidatorParams) String() string {
out := fmt.Sprintf(`Params:
Debt Auction Size: %s
Collateral Params: `,
p.DebtAuctionSize,
)
for _, cp := range p.CollateralParams {
out += fmt.Sprintf(`
%s`, cp.String())
}
return out
}
// CollateralParams params storing information about each collateral for the liquidator module
type CollateralParams struct {
Denom string // Coin name of collateral type
AuctionSize sdk.Int // Max amount of collateral to sell off in any one auction. Known as lump in Maker.
// LiquidationPenalty
}
// String implements stringer interface
func (cp CollateralParams) String() string {
return fmt.Sprintf(`
Denom: %s
AuctionSize: %s`, cp.Denom, cp.AuctionSize)
}
// ParamKeyTable for the liquidator module
func ParamKeyTable() subspace.KeyTable {
return subspace.NewKeyTable().RegisterParamSet(&LiquidatorParams{})
}
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
// pairs of liquidator module's parameters.
// nolint
func (p *LiquidatorParams) ParamSetPairs() subspace.ParamSetPairs {
return subspace.ParamSetPairs{
subspace.NewParamSetPair(KeyDebtAuctionSize, &p.DebtAuctionSize),
subspace.NewParamSetPair(KeyCollateralParams, &p.CollateralParams),
}
}
// DefaultParams for the liquidator module
func DefaultParams() LiquidatorParams {
return LiquidatorParams{
DebtAuctionSize: sdk.NewInt(1000),
CollateralParams: []CollateralParams{},
}
}
func (p LiquidatorParams) Validate() error {
if p.DebtAuctionSize.IsNegative() {
return fmt.Errorf("debt auction size should be positive, is %s", p.DebtAuctionSize)
}
denomDupMap := make(map[string]int)
for _, cp := range p.CollateralParams {
_, found := denomDupMap[cp.Denom]
if found {
return fmt.Errorf("duplicate denom: %s", cp.Denom)
}
denomDupMap[cp.Denom] = 1
if cp.AuctionSize.IsNegative() {
return fmt.Errorf(
"auction size for each collateral should be positive, is %s for %s", cp.AuctionSize, cp.Denom,
)
}
}
return nil
}

View File

@ -0,0 +1,22 @@
package types
import(
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
)
const (
QueryGetOutstandingDebt = "outstanding_debt" // Get the outstanding seized debt
)
type SeizeAndStartCollateralAuctionRequest struct {
BaseReq rest.BaseReq `json:"base_req"`
Sender sdk.AccAddress `json:"sender"`
CdpOwner sdk.AccAddress `json:"cdp_owner"`
CollateralDenom string `json:"collateral_denom"`
}
type StartDebtAuctionRequest struct {
BaseReq rest.BaseReq `json:"base_req"`
Sender sdk.AccAddress `json:"sender"` // TODO use baseReq.From instead?
}

View File

@ -0,0 +1,30 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// SeizedDebt tracks debt seized from liquidated CDPs.
type SeizedDebt struct {
Total sdk.Int // Total debt seized from CDPs. Known as Awe in maker.
SentToAuction sdk.Int // Portion of seized debt that has had a (reverse) auction was started for it. Known as Ash in maker.
// SentToAuction should always be < Total
}
// Available gets the seized debt that has not been sent for auction. Known as Woe in maker.
func (sd SeizedDebt) Available() sdk.Int {
return sd.Total.Sub(sd.SentToAuction)
}
// Settle reduces the amount of debt
func (sd SeizedDebt) Settle(amount sdk.Int) (SeizedDebt, sdk.Error) {
if amount.IsNegative() {
return sd, sdk.ErrInternal("tried to settle a negative amount")
}
if amount.GT(sd.Total) {
return sd, sdk.ErrInternal("tried to settle more debt than exists")
}
sd.Total = sd.Total.Sub(amount)
sd.SentToAuction = sdk.MaxInt(sd.SentToAuction.Sub(amount), sdk.ZeroInt())
return sd, nil
}

64
x/pricefeed/alias.go Normal file
View File

@ -0,0 +1,64 @@
// nolint
// autogenerated code using github.com/rigelrozanski/multitool
// aliases generated for the following subdirectories:
// ALIASGEN: github.com/kava-labs/kava/x/pricefeed/types/
package pricefeed
import (
"github.com/kava-labs/kava/x/pricefeed/types"
)
const (
DefaultCodespace = types.DefaultCodespace
CodeEmptyInput = types.CodeEmptyInput
CodeExpired = types.CodeExpired
CodeInvalidPrice = types.CodeInvalidPrice
CodeInvalidAsset = types.CodeInvalidAsset
CodeInvalidOracle = types.CodeInvalidOracle
ModuleName = types.ModuleName
StoreKey = types.StoreKey
RouterKey = types.RouterKey
TypeMsgPostPrice = types.TypeMsgPostPrice
QueryCurrentPrice = types.QueryCurrentPrice
QueryRawPrices = types.QueryRawPrices
QueryAssets = types.QueryAssets
)
var (
// functions aliases
RegisterCodec = types.RegisterCodec
ErrEmptyInput = types.ErrEmptyInput
ErrExpired = types.ErrExpired
ErrNoValidPrice = types.ErrNoValidPrice
ErrInvalidAsset = types.ErrInvalidAsset
ErrInvalidOracle = types.ErrInvalidOracle
NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState
ValidateGenesis = types.ValidateGenesis
NewMsgPostPrice = types.NewMsgPostPrice
ParamKeyTable = types.ParamKeyTable
NewAssetParams = types.NewAssetParams
DefaultAssetParams = types.DefaultAssetParams
NewOracleParams = types.NewOracleParams
DefaultOracleParams = types.DefaultOracleParams
// variable aliases
ModuleCdc = types.ModuleCdc
ParamStoreKeyOracles = types.ParamStoreKeyOracles
ParamStoreKeyAssets = types.ParamStoreKeyAssets
)
type (
GenesisState = types.GenesisState
MsgPostPrice = types.MsgPostPrice
AssetParams = types.AssetParams
OracleParams = types.OracleParams
ParamSubspace = types.ParamSubspace
QueryRawPricesResp = types.QueryRawPricesResp
QueryAssetsResp = types.QueryAssetsResp
Asset = types.Asset
Oracle = types.Oracle
CurrentPrice = types.CurrentPrice
PostedPrice = types.PostedPrice
SortDecs = types.SortDecs
)

View File

@ -0,0 +1,93 @@
package cli
import (
"fmt"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/kava-labs/kava/x/pricefeed/types"
"github.com/spf13/cobra"
)
// GetQueryCmd returns the cli query commands for this module
func GetQueryCmd(storeKey string, cdc *codec.Codec) *cobra.Command {
// Group nameservice queries under a subcommand
pricefeedQueryCmd := &cobra.Command{
Use: types.ModuleName,
Short: "Querying commands for the pricefeed module",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
pricefeedQueryCmd.AddCommand(client.GetCommands(
GetCmdCurrentPrice(storeKey, cdc),
GetCmdRawPrices(storeKey, cdc),
GetCmdAssets(storeKey, cdc),
)...)
return pricefeedQueryCmd
}
// GetCmdCurrentPrice queries the current price of an asset
func GetCmdCurrentPrice(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "price [assetCode]",
Short: "get the current price of an asset",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
assetCode := args[0]
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/price/%s", queryRoute, assetCode), nil)
if err != nil {
fmt.Printf("error when querying current price - %s", err)
fmt.Printf("could not get current price for - %s \n", assetCode)
return nil
}
var out types.CurrentPrice
cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out)
},
}
}
// GetCmdRawPrices queries the current price of an asset
func GetCmdRawPrices(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "rawprices [assetCode]",
Short: "get the raw oracle prices for an asset",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
assetCode := args[0]
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/rawprices/%s", queryRoute, assetCode), nil)
if err != nil {
fmt.Printf("could not get raw prices for - %s \n", assetCode)
return nil
}
var out types.QueryRawPricesResp
cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out)
},
}
}
// GetCmdAssets queries list of assets in the pricefeed
func GetCmdAssets(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "assets",
Short: "get the assets in the pricefeed",
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/assets", queryRoute), nil)
if err != nil {
fmt.Printf("could not get assets")
return nil
}
var out types.QueryAssetsResp
cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out)
},
}
}

View File

@ -0,0 +1,63 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/pricefeed/types"
)
// GetTxCmd returns the transaction commands for this module
func GetTxCmd(storeKey string, cdc *codec.Codec) *cobra.Command {
pricefeedTxCmd := &cobra.Command{
Use: types.ModuleName,
Short: "Pricefeed transactions subcommands",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
pricefeedTxCmd.AddCommand(client.PostCommands(
GetCmdPostPrice(cdc),
)...)
return pricefeedTxCmd
}
// GetCmdPostPrice cli command for posting prices.
func GetCmdPostPrice(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "postprice [assetCode] [price] [expiry]",
Short: "post the latest price for a particular asset",
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
// if err := cliCtx.EnsureAccountExists(); err != nil {
// return err
// }
price, err := sdk.NewDecFromStr(args[1])
if err != nil {
return err
}
expiry, ok := sdk.NewIntFromString(args[2])
if !ok {
fmt.Printf("invalid expiry - %s \n", args[2])
return nil
}
msg := types.NewMsgPostPrice(cliCtx.GetFromAddress(), args[0], price, expiry)
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}

View File

@ -0,0 +1,113 @@
package rest
import (
"fmt"
"net/http"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/gorilla/mux"
"github.com/kava-labs/kava/x/pricefeed/types"
)
const (
restName = "assetCode"
)
type postPriceReq struct {
BaseReq rest.BaseReq `json:"base_req"`
AssetCode string `json:"asset_code"`
Price string `json:"price"`
Expiry string `json:"expiry"`
}
// RegisterRoutes - Central function to define routes that get registered by the main application
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, storeName string) {
r.HandleFunc(fmt.Sprintf("/%s/rawprices", storeName), postPriceHandler(cliCtx)).Methods("PUT")
r.HandleFunc(fmt.Sprintf("/%s/rawprices/{%s}", storeName, restName), getRawPricesHandler(cliCtx, storeName)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/currentprice/{%s}", storeName, restName), getCurrentPriceHandler(cliCtx, storeName)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/assets", storeName), getAssetsHandler(cliCtx, storeName)).Methods("GET")
}
func postPriceHandler(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req postPriceReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
rest.WriteErrorResponse(w, http.StatusBadRequest, "failed to parse request")
return
}
baseReq := req.BaseReq.Sanitize()
if !baseReq.ValidateBasic(w) {
return
}
addr, err := sdk.AccAddressFromBech32(baseReq.From)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
price, err := sdk.NewDecFromStr(req.Price)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
expiry, ok := sdk.NewIntFromString(req.Expiry)
if !ok {
rest.WriteErrorResponse(w, http.StatusBadRequest, "invalid expiry")
return
}
// create the message
msg := types.NewMsgPostPrice(addr, req.AssetCode, price, expiry)
err = msg.ValidateBasic()
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg})
}
}
func getRawPricesHandler(cliCtx context.CLIContext, storeName string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
paramType := vars[restName]
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/rawprices/%s", storeName, paramType), nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}
func getCurrentPriceHandler(cliCtx context.CLIContext, storeName string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
paramType := vars[restName]
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/price/%s", storeName, paramType), nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}
func getAssetsHandler(cliCtx context.CLIContext, storeName string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/assets/", storeName), nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}

6
x/pricefeed/doc.go Normal file
View File

@ -0,0 +1,6 @@
/*
Package pricefeed allows a group of white-listed oracles to post price information of specific assets that are tracked by the system. For each asset, the module computes the median of all posted prices by white-listed oracles and takes that as the current price value.
*/
package pricefeed

51
x/pricefeed/genesis.go Normal file
View File

@ -0,0 +1,51 @@
package pricefeed
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// InitGenesis sets distribution information for genesis.
func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) {
// Set the assets and oracles from params
keeper.SetAssetParams(ctx, data.AssetParams)
keeper.SetOracleParams(ctx ,data.OracleParams)
// Iterate through the posted prices and set them in the store
for _, pp := range data.PostedPrices {
addr, err := sdk.AccAddressFromBech32(pp.OracleAddress)
if err != nil {
panic(err)
}
_, err = keeper.SetPrice(ctx, addr, pp.AssetCode, pp.Price, pp.Expiry)
if err != nil {
panic(err)
}
}
// Set the current price (if any) based on what's now in the store
if err := keeper.SetCurrentPrices(ctx); err != nil {
panic(err)
}
}
// ExportGenesis returns a GenesisState for a given context and keeper.
func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState {
// Get the params for assets and oracles
assetParams := keeper.GetAssetParams(ctx)
oracleParams := keeper.GetOracleParams(ctx)
var postedPrices []PostedPrice
for _, asset := range keeper.GetAssets(ctx) {
pp := keeper.GetRawPrices(ctx, asset.AssetCode)
postedPrices = append(postedPrices, pp...)
}
return GenesisState{
AssetParams: assetParams,
OracleParams: oracleParams,
PostedPrices: postedPrices,
}
}

50
x/pricefeed/handler.go Normal file
View File

@ -0,0 +1,50 @@
package pricefeed
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// NewHandler handles all pricefeed type messages
func NewHandler(k Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) {
case MsgPostPrice:
return HandleMsgPostPrice(ctx, k, msg)
default:
errMsg := fmt.Sprintf("unrecognized pricefeed message type: %T", msg)
return sdk.ErrUnknownRequest(errMsg).Result()
}
}
}
// price feed questions:
// do proposers need to post the round in the message? If not, how do we determine the round?
// HandleMsgPostPrice handles prices posted by oracles
func HandleMsgPostPrice(
ctx sdk.Context,
k Keeper,
msg MsgPostPrice) sdk.Result {
// TODO cleanup message validation and errors
err := k.ValidatePostPrice(ctx, msg)
if err != nil {
return err.Result()
}
k.SetPrice(ctx, msg.From, msg.AssetCode, msg.Price, msg.Expiry)
return sdk.Result{}
}
// EndBlocker updates the current pricefeed
func EndBlocker(ctx sdk.Context, k Keeper) {
// TODO val_state_change.go is relevant if we want to rotate the oracle set
// Running in the end blocker ensures that prices will update at most once per block,
// which seems preferable to having state storage values change in response to multiple transactions
// which occur during a block
//TODO use an iterator and update the prices for all assets in the store
k.SetCurrentPrices(ctx)
return
}

280
x/pricefeed/keeper.go Normal file
View File

@ -0,0 +1,280 @@
package pricefeed
import (
"sort"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// TODO refactor constants to app.go
const (
// QuerierRoute is the querier route for gov
QuerierRoute = ModuleName
// Parameter store default namestore
DefaultParamspace = ModuleName
// Store prefix for the raw pricefeed of an asset
RawPriceFeedPrefix = StoreKey + ":raw:"
// Store prefix for the current price of an asset
CurrentPricePrefix = StoreKey + ":currentprice:"
// Store Prefix for the assets in the pricefeed system
AssetPrefix = StoreKey + ":assets"
// OraclePrefix store prefix for the oracle accounts
OraclePrefix = StoreKey + ":oracles"
)
// Keeper struct for pricefeed module
type Keeper struct {
// The reference to the Paramstore to get and set pricefeed specific params
paramSpace ParamSubspace
// The keys used to access the stores from Context
storeKey sdk.StoreKey
// Codec for binary encoding/decoding
cdc *codec.Codec
// Reserved codespace
codespace sdk.CodespaceType
}
// NewKeeper returns a new keeper for the pricefeed module. It handles:
// - adding oracles
// - adding/removing assets from the pricefeed
func NewKeeper(
storeKey sdk.StoreKey, cdc *codec.Codec, paramSpace ParamSubspace, codespace sdk.CodespaceType,
) Keeper {
return Keeper{
paramSpace: paramSpace,
storeKey: storeKey,
cdc: cdc,
codespace: codespace,
}
}
// // AddOracle adds an Oracle to the store
// func (k Keeper) AddOracle(ctx sdk.Context, address string) {
// oracles := k.GetOracles(ctx)
// oracles = append(oracles, Oracle{OracleAddress: address})
// store := ctx.KVStore(k.storeKey)
// store.Set(
// []byte(OraclePrefix), k.cdc.MustMarshalBinaryBare(oracles),
// )
// }
// // AddAsset adds an asset to the store
// func (k Keeper) AddAsset(
// ctx sdk.Context,
// assetCode string,
// desc string,
// ) {
// assets := k.GetAssets(ctx)
// assets = append(assets, Asset{AssetCode: assetCode, Description: desc})
// store := ctx.KVStore(k.storeKey)
// store.Set(
// []byte(AssetPrefix), k.cdc.MustMarshalBinaryBare(assets),
// )
// }
func (k Keeper) SetAssetParams(ctx sdk.Context, ap AssetParams) {
k.paramSpace.Set(ctx, ParamStoreKeyAssets, &ap)
}
func (k Keeper) SetOracleParams(ctx sdk.Context, op OracleParams) {
k.paramSpace.Set(ctx, ParamStoreKeyOracles, &op)
}
// SetPrice updates the posted price for a specific oracle
func (k Keeper) SetPrice(
ctx sdk.Context,
oracle sdk.AccAddress,
assetCode string,
price sdk.Dec,
expiry sdk.Int) (PostedPrice, sdk.Error) {
// If the expiry is less than or equal to the current blockheight, we consider the price valid
if expiry.GTE(sdk.NewInt(ctx.BlockHeight())) {
store := ctx.KVStore(k.storeKey)
prices := k.GetRawPrices(ctx, assetCode)
var index int
found := false
for i := range prices {
if prices[i].OracleAddress == oracle.String() {
index = i
found = true
break
}
}
// set the price for that particular oracle
if found {
prices[index] = PostedPrice{AssetCode: assetCode, OracleAddress: oracle.String(), Price: price, Expiry: expiry}
} else {
prices = append(prices, PostedPrice{
assetCode, oracle.String(), price, expiry,
})
index = len(prices) - 1
}
store.Set(
[]byte(RawPriceFeedPrefix+assetCode), k.cdc.MustMarshalBinaryBare(prices),
)
return prices[index], nil
}
return PostedPrice{}, ErrExpired(k.codespace)
}
// SetCurrentPrices updates the price of an asset to the meadian of all valid oracle inputs
func (k Keeper) SetCurrentPrices(ctx sdk.Context) sdk.Error {
assets := k.GetAssets(ctx)
for _, v := range assets {
assetCode := v.AssetCode
prices := k.GetRawPrices(ctx, assetCode)
var notExpiredPrices []CurrentPrice
// filter out expired prices
for _, v := range prices {
if v.Expiry.GTE(sdk.NewInt(ctx.BlockHeight())) {
notExpiredPrices = append(notExpiredPrices, CurrentPrice{
AssetCode: v.AssetCode,
Price: v.Price,
Expiry: v.Expiry,
})
}
}
l := len(notExpiredPrices)
var medianPrice sdk.Dec
var expiry sdk.Int
// TODO make threshold for acceptance (ie. require 51% of oracles to have posted valid prices
if l == 0 {
// Error if there are no valid prices in the raw pricefeed
return ErrNoValidPrice(k.codespace)
} else if l == 1 {
// Return immediately if there's only one price
medianPrice = notExpiredPrices[0].Price
expiry = notExpiredPrices[0].Expiry
} else {
// sort the prices
sort.Slice(notExpiredPrices, func(i, j int) bool {
return notExpiredPrices[i].Price.LT(notExpiredPrices[j].Price)
})
// If there's an even number of prices
if l%2 == 0 {
// TODO make sure this is safe.
// Since it's a price and not a balance, division with precision loss is OK.
price1 := notExpiredPrices[l/2-1].Price
price2 := notExpiredPrices[l/2].Price
sum := price1.Add(price2)
divsor, _ := sdk.NewDecFromStr("2")
medianPrice = sum.Quo(divsor)
// TODO Check if safe, makes sense
// Takes the average of the two expiries rounded down to the nearest Int.
expiry = notExpiredPrices[l/2-1].Expiry.Add(notExpiredPrices[l/2].Expiry).Quo(sdk.NewInt(2))
} else {
// integer division, so we'll get an integer back, rounded down
medianPrice = notExpiredPrices[l/2].Price
expiry = notExpiredPrices[l/2].Expiry
}
}
store := ctx.KVStore(k.storeKey)
currentPrice := CurrentPrice{
AssetCode: assetCode,
Price: medianPrice,
Expiry: expiry,
}
store.Set(
[]byte(CurrentPricePrefix+assetCode), k.cdc.MustMarshalBinaryBare(currentPrice),
)
}
return nil
}
func (k Keeper) GetOracleParams(ctx sdk.Context) OracleParams {
var op OracleParams
k.paramSpace.Get(ctx, ParamStoreKeyOracles, &op)
return op
}
// GetOracles returns the oracles in the pricefeed store
func (k Keeper) GetOracles(ctx sdk.Context) []Oracle {
var op OracleParams
k.paramSpace.Get(ctx, ParamStoreKeyOracles, &op)
return op.Oracles
}
func (k Keeper) GetAssetParams(ctx sdk.Context) AssetParams {
var ap AssetParams
k.paramSpace.Get(ctx, ParamStoreKeyAssets, &ap)
return ap
}
// GetAssets returns the assets in the pricefeed store
func (k Keeper) GetAssets(ctx sdk.Context) []Asset {
var ap AssetParams
k.paramSpace.Get(ctx, ParamStoreKeyAssets, &ap)
return ap.Assets
}
// GetAsset returns the asset if it is in the pricefeed system
func (k Keeper) GetAsset(ctx sdk.Context, assetCode string) (Asset, bool) {
assets := k.GetAssets(ctx)
for i := range assets {
if assets[i].AssetCode == assetCode {
return assets[i], true
}
}
return Asset{}, false
}
// GetOracle returns the oracle address as a string if it is in the pricefeed store
func (k Keeper) GetOracle(ctx sdk.Context, oracle string) (Oracle, bool) {
oracles := k.GetOracles(ctx)
for i := range oracles {
if oracles[i].OracleAddress == oracle {
return oracles[i], true
}
}
return Oracle{}, false
}
// GetCurrentPrice fetches the current median price of all oracles for a specific asset
func (k Keeper) GetCurrentPrice(ctx sdk.Context, assetCode string) CurrentPrice {
store := ctx.KVStore(k.storeKey)
bz := store.Get([]byte(CurrentPricePrefix + assetCode))
// TODO panic or return error if not found
var price CurrentPrice
k.cdc.MustUnmarshalBinaryBare(bz, &price)
return price
}
// GetRawPrices fetches the set of all prices posted by oracles for an asset
func (k Keeper) GetRawPrices(ctx sdk.Context, assetCode string) []PostedPrice {
store := ctx.KVStore(k.storeKey)
bz := store.Get([]byte(RawPriceFeedPrefix + assetCode))
var prices []PostedPrice
k.cdc.MustUnmarshalBinaryBare(bz, &prices)
return prices
}
// ValidatePostPrice makes sure the person posting the price is an oracle
func (k Keeper) ValidatePostPrice(ctx sdk.Context, msg MsgPostPrice) sdk.Error {
// TODO implement this
_, assetFound := k.GetAsset(ctx, msg.AssetCode)
if !assetFound {
return ErrInvalidAsset(k.codespace)
}
_, oracleFound := k.GetOracle(ctx, msg.From.String())
if !oracleFound {
return ErrInvalidOracle(k.codespace)
}
return nil
}

124
x/pricefeed/keeper_test.go Normal file
View File

@ -0,0 +1,124 @@
package pricefeed
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
)
// TestKeeper_SetGetAsset tests adding assets to the pricefeed, getting assets from the store
func TestKeeper_SetGetAsset(t *testing.T) {
helper := getMockApp(t, 0, GenesisState{}, nil)
header := abci.Header{Height: helper.mApp.LastBlockHeight() + 1}
helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := helper.mApp.BaseApp.NewContext(false, abci.Header{})
ap := AssetParams{
Assets: []Asset{Asset{AssetCode: "tst", Description: "the future of finance"}},
}
helper.keeper.SetAssetParams(ctx, ap)
assets := helper.keeper.GetAssets(ctx)
require.Equal(t, len(assets), 1)
require.Equal(t, assets[0].AssetCode, "tst")
_, found := helper.keeper.GetAsset(ctx, "tst")
require.Equal(t, found, true)
ap = AssetParams{
Assets: []Asset{
Asset{AssetCode: "tst", Description: "the future of finance"},
Asset{AssetCode: "tst2", Description: "the future of finance"}},
}
helper.keeper.SetAssetParams(ctx, ap)
assets = helper.keeper.GetAssets(ctx)
require.Equal(t, len(assets), 2)
require.Equal(t, assets[0].AssetCode, "tst")
require.Equal(t, assets[1].AssetCode, "tst2")
_, found = helper.keeper.GetAsset(ctx, "nan")
require.Equal(t, found, false)
}
// TestKeeper_GetSetPrice Test Posting the price by an oracle
func TestKeeper_GetSetPrice(t *testing.T) {
helper := getMockApp(t, 2, GenesisState{}, nil)
header := abci.Header{Height: helper.mApp.LastBlockHeight() + 1}
helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := helper.mApp.BaseApp.NewContext(false, abci.Header{})
ap := AssetParams{
Assets: []Asset{Asset{AssetCode: "tst", Description: "the future of finance"}},
}
helper.keeper.SetAssetParams(ctx, ap)
// Set price by oracle 1
_, err := helper.keeper.SetPrice(
ctx, helper.addrs[0], "tst",
sdk.MustNewDecFromStr("0.33"),
sdk.NewInt(10))
require.NoError(t, err)
// Get raw prices
rawPrices := helper.keeper.GetRawPrices(ctx, "tst")
require.Equal(t, len(rawPrices), 1)
require.Equal(t, rawPrices[0].Price.Equal(sdk.MustNewDecFromStr("0.33")), true)
// Set price by oracle 2
_, err = helper.keeper.SetPrice(
ctx, helper.addrs[1], "tst",
sdk.MustNewDecFromStr("0.35"),
sdk.NewInt(10))
require.NoError(t, err)
rawPrices = helper.keeper.GetRawPrices(ctx, "tst")
require.Equal(t, len(rawPrices), 2)
require.Equal(t, rawPrices[1].Price.Equal(sdk.MustNewDecFromStr("0.35")), true)
// Update Price by Oracle 1
_, err = helper.keeper.SetPrice(
ctx, helper.addrs[0], "tst",
sdk.MustNewDecFromStr("0.37"),
sdk.NewInt(10))
require.NoError(t, err)
rawPrices = helper.keeper.GetRawPrices(ctx, "tst")
require.Equal(t, rawPrices[0].Price.Equal(sdk.MustNewDecFromStr("0.37")), true)
}
// TestKeeper_GetSetCurrentPrice Test Setting the median price of an Asset
func TestKeeper_GetSetCurrentPrice(t *testing.T) {
helper := getMockApp(t, 4, GenesisState{}, nil)
header := abci.Header{Height: helper.mApp.LastBlockHeight() + 1}
helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header})
ctx := helper.mApp.BaseApp.NewContext(false, abci.Header{})
// Odd number of oracles
ap := AssetParams{
Assets: []Asset{Asset{AssetCode: "tst", Description: "the future of finance"}},
}
helper.keeper.SetAssetParams(ctx, ap)
helper.keeper.SetPrice(
ctx, helper.addrs[0], "tst",
sdk.MustNewDecFromStr("0.33"),
sdk.NewInt(10))
helper.keeper.SetPrice(
ctx, helper.addrs[1], "tst",
sdk.MustNewDecFromStr("0.35"),
sdk.NewInt(10))
helper.keeper.SetPrice(
ctx, helper.addrs[2], "tst",
sdk.MustNewDecFromStr("0.34"),
sdk.NewInt(10))
// Set current price
err := helper.keeper.SetCurrentPrices(ctx)
require.NoError(t, err)
// Get Current price
price := helper.keeper.GetCurrentPrice(ctx, "tst")
require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.34")), true)
// Even number of oracles
helper.keeper.SetPrice(
ctx, helper.addrs[3], "tst",
sdk.MustNewDecFromStr("0.36"),
sdk.NewInt(10))
err = helper.keeper.SetCurrentPrices(ctx)
require.NoError(t, err)
price = helper.keeper.GetCurrentPrice(ctx, "tst")
require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.345")), true)
}

129
x/pricefeed/module.go Normal file
View File

@ -0,0 +1,129 @@
package pricefeed
import (
"encoding/json"
"github.com/gorilla/mux"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/kava-labs/kava/x/pricefeed/client/cli"
"github.com/kava-labs/kava/x/pricefeed/client/rest"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
)
// AppModuleBasic app module basics object
type AppModuleBasic struct{}
// Name get module name
func (AppModuleBasic) Name() string {
return ModuleName
}
// RegisterCodec register module codec
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
RegisterCodec(cdc)
}
// DefaultGenesis default genesis state
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
}
// ValidateGenesis module validate genesis
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var data GenesisState
err := ModuleCdc.UnmarshalJSON(bz, &data)
if err != nil {
return err
}
return ValidateGenesis(data)
}
// RegisterRESTRoutes register rest routes
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
rest.RegisterRoutes(ctx, rtr, StoreKey)
}
// GetTxCmd get the root tx command of this module
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetTxCmd(StoreKey, cdc)
}
// GetQueryCmd get the root query command of this module
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(StoreKey, cdc)
}
// AppModule app module type
type AppModule struct {
AppModuleBasic
keeper Keeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
}
}
// Name module name
func (AppModule) Name() string {
return ModuleName
}
// RegisterInvariants register module invariants
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// Route module message route name
func (AppModule) Route() string {
return ModuleName
}
// NewHandler module handler
func (am AppModule) NewHandler() sdk.Handler {
return NewHandler(am.keeper)
}
// QuerierRoute module querier route name
func (AppModule) QuerierRoute() string {
return ModuleName
}
// NewQuerierHandler module querier
func (am AppModule) NewQuerierHandler() sdk.Querier {
return NewQuerier(am.keeper)
}
// BeginBlock module begin-block
func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {}
// EndBlock module end-block
func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
EndBlocker(ctx, am.keeper)
return []abci.ValidatorUpdate{}
}
// InitGenesis module init-genesis
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState GenesisState
ModuleCdc.MustUnmarshalJSON(data, &genesisState)
InitGenesis(ctx, am.keeper, genesisState)
return []abci.ValidatorUpdate{}
}
// ExportGenesis module export genesis
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return ModuleCdc.MustMarshalJSON(gs)
}

74
x/pricefeed/querier.go Normal file
View File

@ -0,0 +1,74 @@
package pricefeed
import (
"github.com/cosmos/cosmos-sdk/codec"
abci "github.com/tendermint/tendermint/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// NewQuerier is the module level router for state queries
func NewQuerier(keeper Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
switch path[0] {
case QueryCurrentPrice:
return queryCurrentPrice(ctx, path[1:], req, keeper)
case QueryRawPrices:
return queryRawPrices(ctx, path[1:], req, keeper)
case QueryAssets:
return queryAssets(ctx, req, keeper)
default:
return nil, sdk.ErrUnknownRequest("unknown pricefeed query endpoint")
}
}
}
func queryCurrentPrice(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) {
assetCode := path[0]
_, found := keeper.GetAsset(ctx, assetCode)
if !found {
return []byte{}, sdk.ErrUnknownRequest("asset not found")
}
currentPrice := keeper.GetCurrentPrice(ctx, assetCode)
bz, err2 := codec.MarshalJSONIndent(keeper.cdc, currentPrice)
if err2 != nil {
panic("could not marshal result to JSON")
}
return bz, nil
}
func queryRawPrices(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) {
var priceList QueryRawPricesResp
assetCode := path[0]
_, found := keeper.GetAsset(ctx, assetCode)
if !found {
return []byte{}, sdk.ErrUnknownRequest("asset not found")
}
rawPrices := keeper.GetRawPrices(ctx, assetCode)
for _, price := range rawPrices {
priceList = append(priceList, price.String())
}
bz, err2 := codec.MarshalJSONIndent(keeper.cdc, priceList)
if err2 != nil {
panic("could not marshal result to JSON")
}
return bz, nil
}
func queryAssets(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) {
var assetList QueryAssetsResp
assets := keeper.GetAssets(ctx)
for _, asset := range assets {
assetList = append(assetList, asset.String())
}
bz, err2 := codec.MarshalJSONIndent(keeper.cdc, assetList)
if err2 != nil {
panic("could not marshal result to JSON")
}
return bz, nil
}

View File

@ -0,0 +1,57 @@
package pricefeed
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/mock"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
)
type testHelper struct {
mApp *mock.App
keeper Keeper
addrs []sdk.AccAddress
pubKeys []crypto.PubKey
privKeys []crypto.PrivKey
}
func getMockApp(t *testing.T, numGenAccs int, genState GenesisState, genAccs []authexported.Account) testHelper {
mApp := mock.NewApp()
RegisterCodec(mApp.Cdc)
keyPricefeed := sdk.NewKVStoreKey(StoreKey)
pk := mApp.ParamsKeeper
keeper := NewKeeper(keyPricefeed, mApp.Cdc, pk.Subspace(DefaultParamspace).WithKeyTable(ParamKeyTable()), DefaultCodespace)
// Register routes
mApp.Router().AddRoute(RouterKey, NewHandler(keeper))
mApp.SetEndBlocker(getEndBlocker(keeper))
require.NoError(t, mApp.CompleteSetup(keyPricefeed))
valTokens := sdk.TokensFromConsensusPower(42)
var (
addrs []sdk.AccAddress
pubKeys []crypto.PubKey
privKeys []crypto.PrivKey
)
if genAccs == nil || len(genAccs) == 0 {
genAccs, addrs, pubKeys, privKeys = mock.CreateGenAccounts(numGenAccs,
sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, valTokens)))
}
mock.SetGenesis(mApp, genAccs)
return testHelper{mApp, keeper, addrs, pubKeys, privKeys}
}
// gov and staking endblocker
func getEndBlocker(keeper Keeper) sdk.EndBlocker {
return func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock {
EndBlocker(ctx, keeper)
return abci.ResponseEndBlock{}
}
}

View File

@ -0,0 +1,16 @@
package types
import (
"github.com/cosmos/cosmos-sdk/codec"
)
var ModuleCdc = codec.New()
func init() {
RegisterCodec(ModuleCdc)
}
// RegisterCode registers concrete types on the Amino code
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgPostPrice{}, "pricefeed/MsgPostPrice", nil)
}

View File

@ -0,0 +1,48 @@
package types
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
// DefaultCodespace codespace for the module
DefaultCodespace sdk.CodespaceType = ModuleName
// CodeEmptyInput error code for empty input errors
CodeEmptyInput sdk.CodeType = 1
// CodeExpired error code for expired prices
CodeExpired sdk.CodeType = 2
// CodeInvalidPrice error code for all input prices expired
CodeInvalidPrice sdk.CodeType = 3
// CodeInvalidAsset error code for invalid asset
CodeInvalidAsset sdk.CodeType = 4
// CodeInvalidOracle error code for invalid oracle
CodeInvalidOracle sdk.CodeType = 5
)
// ErrEmptyInput Error constructor
func ErrEmptyInput(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeEmptyInput, fmt.Sprintf("Input must not be empty."))
}
// ErrExpired Error constructor for posted price messages with expired price
func ErrExpired(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeExpired, fmt.Sprintf("Price is expired."))
}
// ErrNoValidPrice Error constructor for posted price messages with expired price
func ErrNoValidPrice(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidPrice, fmt.Sprintf("All input prices are expired."))
}
// ErrInvalidAsset Error constructor for posted price messages for invalid assets
func ErrInvalidAsset(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidAsset, fmt.Sprintf("Asset code does not exist."))
}
// ErrInvalidOracle Error constructor for posted price messages for invalid oracles
func ErrInvalidOracle(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidOracle, fmt.Sprintf("Oracle does not exist or not authorized."))
}

View File

@ -0,0 +1,63 @@
package types
import (
"bytes"
"fmt"
)
// GenesisState - pricefeed state that must be provided at genesis
type GenesisState struct {
AssetParams AssetParams `json:"asset_params" yaml:"asset_params"`
OracleParams OracleParams `json:"oracle_params" yaml:"oracle_params"`
PostedPrices []PostedPrice `json:"posted_prices" yaml:"posted_prices"`
}
// NewGenesisState creates a new genesis state for the pricefeed module
func NewGenesisState(ap AssetParams, op OracleParams, pp []PostedPrice) GenesisState {
return GenesisState{
AssetParams: ap,
OracleParams: op,
PostedPrices: pp,
}
}
// DefaultGenesisState defines default GenesisState for pricefeed
func DefaultGenesisState() GenesisState {
return NewGenesisState(
DefaultAssetParams(),
DefaultOracleParams(),
[]PostedPrice{},
)
}
// Equal checks whether two gov GenesisState structs are equivalent
func (data GenesisState) Equal(data2 GenesisState) bool {
b1 := ModuleCdc.MustMarshalBinaryBare(data)
b2 := ModuleCdc.MustMarshalBinaryBare(data2)
return bytes.Equal(b1, b2)
}
// IsEmpty returns true if a GenesisState is empty
func (data GenesisState) IsEmpty() bool {
return data.Equal(GenesisState{})
}
// ValidateGenesis performs basic validation of genesis data returning an
// error for any failed validation criteria.
func ValidateGenesis(data GenesisState) error {
// iterate over assets and verify them
for _, asset := range data.AssetParams.Assets {
if asset.AssetCode == "" {
return fmt.Errorf("invalid asset: %s. missing asset code", asset.String())
}
}
// iterate over oracles and verify them
for _, oracle := range data.OracleParams.Oracles {
if oracle.OracleAddress == "" {
return fmt.Errorf("invalid oracle: %s. missing oracle address", oracle.String())
}
}
return nil
}

12
x/pricefeed/types/key.go Normal file
View File

@ -0,0 +1,12 @@
package types
const (
// ModuleName The name that will be used throughout the module
ModuleName = "pricefeed"
// StoreKey Top level store key where all module items will be stored
StoreKey = ModuleName
// RouterKey Top level router key
RouterKey = ModuleName
)

68
x/pricefeed/types/msgs.go Normal file
View File

@ -0,0 +1,68 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
// TypeMsgPostPrice type of PostPrice msg
TypeMsgPostPrice = "post_price"
)
// MsgPostPrice struct representing a posted price message.
// Used by oracles to input prices to the pricefeed
type MsgPostPrice struct {
From sdk.AccAddress // client that sent in this address
AssetCode string // asset code used by exchanges/api
Price sdk.Dec // price in decimal (max precision 18)
Expiry sdk.Int // block height
}
// NewMsgPostPrice creates a new post price msg
func NewMsgPostPrice(
from sdk.AccAddress,
assetCode string,
price sdk.Dec,
expiry sdk.Int) MsgPostPrice {
return MsgPostPrice{
From: from,
AssetCode: assetCode,
Price: price,
Expiry: expiry,
}
}
// Route Implements Msg.
func (msg MsgPostPrice) Route() string { return RouterKey }
// Type Implements Msg
func (msg MsgPostPrice) Type() string { return TypeMsgPostPrice }
// GetSignBytes Implements Msg.
func (msg MsgPostPrice) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners Implements Msg.
func (msg MsgPostPrice) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.From}
}
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgPostPrice) ValidateBasic() sdk.Error {
if msg.From.Empty() {
return sdk.ErrInternal("invalid (empty) bidder address")
}
if len(msg.AssetCode) == 0 {
return sdk.ErrInternal("invalid (empty) asset code")
}
if msg.Price.LT(sdk.ZeroDec()) {
return sdk.ErrInternal("invalid (negative) price")
}
if msg.Expiry.LT(sdk.ZeroInt()) {
return sdk.ErrInternal("invalid (negative) expiry")
}
// TODO check coin denoms
return nil
}

View File

@ -0,0 +1,40 @@
package types
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
func TestMsgPlaceBid_ValidateBasic(t *testing.T) {
addr := sdk.AccAddress([]byte("someName"))
// oracles := []Oracle{Oracle{
// OracleAddress: addr.String(),
// }}
price, _ := sdk.NewDecFromStr("0.3005")
expiry, _ := sdk.NewIntFromString("10")
negativeExpiry, _ := sdk.NewIntFromString("-3")
negativePrice, _ := sdk.NewDecFromStr("-3.05")
tests := []struct {
name string
msg MsgPostPrice
expectPass bool
}{
{"normal", MsgPostPrice{addr, "xrp", price, expiry}, true},
{"emptyAddr", MsgPostPrice{sdk.AccAddress{}, "xrp", price, expiry}, false},
{"emptyAsset", MsgPostPrice{addr, "", price, expiry}, false},
{"negativePrice", MsgPostPrice{addr, "xrp", negativePrice, expiry}, false},
{"negativeExpiry", MsgPostPrice{addr, "xrp", price, negativeExpiry}, false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.expectPass {
require.Nil(t, tc.msg.ValidateBasic())
} else {
require.NotNil(t, tc.msg.ValidateBasic())
}
})
}
}

View File

@ -0,0 +1,85 @@
package types
import (
"fmt"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
params "github.com/cosmos/cosmos-sdk/x/params/subspace"
)
// Parameter store key
var (
// ParamStoreKeyOracles Param store key for oracles
ParamStoreKeyOracles = []byte("oracles")
// ParamStoreKeyAssets Param store key for assets
ParamStoreKeyAssets = []byte("assets")
)
// ParamKeyTable Key declaration for parameters
func ParamKeyTable() params.KeyTable {
return params.NewKeyTable(
ParamStoreKeyOracles, OracleParams{},
ParamStoreKeyAssets, AssetParams{},
)
}
// AssetParams params for assets. Can be altered via governance
type AssetParams struct {
Assets []Asset `json:"assets,omitempty" yaml:"assets,omitempty"` // Array containing the assets supported by the pricefeed
}
// NewAssetParams creates a new AssetParams object
func NewAssetParams(assets []Asset) AssetParams {
return AssetParams{
Assets: assets,
}
}
// DefaultAssetParams default params for assets
func DefaultAssetParams() AssetParams {
return NewAssetParams([]Asset{})
}
// implements fmt.stringer
func (ap AssetParams) String() string {
var assetListString []string
for _, asset := range ap.Assets {
assetListString = append(assetListString, asset.String())
}
return strings.TrimSpace(fmt.Sprintf(`Asset Params:
Assets: %s\`, strings.Join(assetListString, ", ")))
}
// OracleParams params for assets. Can be altered via governance
type OracleParams struct {
Oracles []Oracle `json:"oracles,omitempty" yaml:"oracles,omitempty"` // Array containing the oracles supported by the pricefeed
}
// NewOracleParams creates a new OracleParams object
func NewOracleParams(oracles []Oracle) OracleParams {
return OracleParams{
Oracles: oracles,
}
}
// DefaultOracleParams default params for assets
func DefaultOracleParams() OracleParams {
return NewOracleParams([]Oracle{})
}
// implements fmt.stringer
func (op OracleParams) String() string {
var oracleListString []string
for _, oracle := range op.Oracles {
oracleListString = append(oracleListString, oracle.String())
}
return strings.TrimSpace(fmt.Sprintf(`Oracle Params:
Oracles: %s\`, strings.Join(oracleListString, ", ")))
}
// ParamSubspace defines the expected Subspace interface for parameters
type ParamSubspace interface {
Get(ctx sdk.Context, key []byte, ptr interface{})
Set(ctx sdk.Context, key []byte, param interface{})
}

View File

@ -0,0 +1,35 @@
package types
import (
"strings"
)
// price Takes an [assetcode] and returns CurrentPrice for that asset
// pricefeed Takes an [assetcode] and returns the raw []PostedPrice for that asset
// assets Returns []Assets in the pricefeed system
const (
// QueryCurrentPrice command for current price queries
QueryCurrentPrice = "price"
// QueryRawPrices command for raw price queries
QueryRawPrices = "rawprices"
// QueryAssets command for assets query
QueryAssets = "assets"
)
// QueryRawPricesResp response to a rawprice query
type QueryRawPricesResp []string
// implement fmt.Stringer
func (n QueryRawPricesResp) String() string {
return strings.Join(n[:], "\n")
}
// QueryAssetsResp response to a assets query
type QueryAssetsResp []string
// implement fmt.Stringer
func (n QueryAssetsResp) String() string {
return strings.Join(n[:], "\n")
}

View File

@ -0,0 +1,67 @@
package types
import (
"fmt"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Asset struct that represents an asset in the pricefeed
type Asset struct {
AssetCode string `json:"asset_code"`
Description string `json:"description"`
}
// implement fmt.Stringer
func (a Asset) String() string {
return strings.TrimSpace(fmt.Sprintf(`AssetCode: %s
Description: %s`, a.AssetCode, a.Description))
}
// Oracle struct that documents which address an oracle is using
type Oracle struct {
OracleAddress string `json:"oracle_address"`
}
// implement fmt.Stringer
func (o Oracle) String() string {
return strings.TrimSpace(fmt.Sprintf(`OracleAddress: %s`, o.OracleAddress))
}
// CurrentPrice struct that contains the metadata of a current price for a particular asset in the pricefeed module.
type CurrentPrice struct {
AssetCode string `json:"asset_code"`
Price sdk.Dec `json:"price"`
Expiry sdk.Int `json:"expiry"`
}
// PostedPrice struct represented a price for an asset posted by a specific oracle
type PostedPrice struct {
AssetCode string `json:"asset_code"`
OracleAddress string `json:"oracle_address"`
Price sdk.Dec `json:"price"`
Expiry sdk.Int `json:"expiry"`
}
// implement fmt.Stringer
func (cp CurrentPrice) String() string {
return strings.TrimSpace(fmt.Sprintf(`AssetCode: %s
Price: %s
Expiry: %s`, cp.AssetCode, cp.Price, cp.Expiry))
}
// implement fmt.Stringer
func (pp PostedPrice) String() string {
return strings.TrimSpace(fmt.Sprintf(`AssetCode: %s
OracleAddress: %s
Price: %s
Expiry: %s`, pp.AssetCode, pp.OracleAddress, pp.Price, pp.Expiry))
}
// SortDecs provides the interface needed to sort sdk.Dec slices
type SortDecs []sdk.Dec
func (a SortDecs) Len() int { return len(a) }
func (a SortDecs) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a SortDecs) Less(i, j int) bool { return a[i].LT(a[j]) }