From 83d523a8568882a68b5a6bd4e635d74a6d5656f0 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Mon, 25 Nov 2019 14:46:02 -0500 Subject: [PATCH 01/99] feat: migrate modules from kava-devnet --- x/auction/abci.go | 23 ++ x/auction/abci_test.go | 28 ++ x/auction/alias.go | 66 ++++ x/auction/app_test.go | 171 +++++++++ x/auction/client/cli/query.go | 49 +++ x/auction/client/cli/tx.go | 67 ++++ x/auction/client/rest/query.go | 30 ++ x/auction/client/rest/rest.go | 13 + x/auction/client/rest/tx.go | 86 +++++ x/auction/doc.go | 14 + x/auction/genesis.go | 30 ++ x/auction/handler.go | 30 ++ x/auction/keeper/keeper.go | 314 ++++++++++++++++ x/auction/keeper/keeper_test.go | 88 +++++ x/auction/keeper/params.go | 17 + x/auction/keeper/querier.go | 40 ++ x/auction/keeper/test_common.go | 35 ++ x/auction/module.go | 131 +++++++ x/auction/types/auctions.go | 307 +++++++++++++++ x/auction/types/auctions_test.go | 403 ++++++++++++++++++++ x/auction/types/codec.go | 23 ++ x/auction/types/expected_keepers.go | 10 + x/auction/types/genesis.go | 47 +++ x/auction/types/keys.go | 15 + x/auction/types/msg.go | 93 +++++ x/auction/types/msg_test.go | 32 ++ x/auction/types/params.go | 98 +++++ x/auction/types/quierier.go | 18 + x/auction/types/utils.go | 9 + x/cdp/alias.go | 61 +++ x/cdp/app_test.go | 67 ++++ x/cdp/client/cli/query.go | 167 +++++++++ x/cdp/client/cli/tx.go | 61 +++ x/cdp/client/module_client.go | 51 +++ x/cdp/client/rest/query.go | 100 +++++ x/cdp/client/rest/rest.go | 13 + x/cdp/client/rest/tx.go | 69 ++++ x/cdp/doc.go | 27 ++ x/cdp/genesis.go | 47 +++ x/cdp/handler.go | 30 ++ x/cdp/keeper/bank_test.go | 67 ++++ x/cdp/keeper/keeper.go | 499 +++++++++++++++++++++++++ x/cdp/keeper/keeper_test.go | 356 ++++++++++++++++++ x/cdp/keeper/params.go | 19 + x/cdp/keeper/querier.go | 83 ++++ x/cdp/keeper/test_common.go | 88 +++++ x/cdp/module.go | 132 +++++++ x/cdp/test_common.go | 52 +++ x/cdp/types/codec.go | 19 + x/cdp/types/expected_keepers.go | 22 ++ x/cdp/types/genesis.go | 37 ++ x/cdp/types/keys.go | 15 + x/cdp/types/msg.go | 55 +++ x/cdp/types/params.go | 138 +++++++ x/cdp/types/querier.go | 25 ++ x/cdp/types/types.go | 77 ++++ x/liquidator/alias.go | 52 +++ x/liquidator/client/cli/query.go | 49 +++ x/liquidator/client/cli/tx.go | 98 +++++ x/liquidator/client/rest/query.go | 30 ++ x/liquidator/client/rest/rest.go | 13 + x/liquidator/client/rest/tx.go | 73 ++++ x/liquidator/doc.go | 21 ++ x/liquidator/genesis.go | 19 + x/liquidator/handler.go | 54 +++ x/liquidator/keeper/keeper.go | 204 ++++++++++ x/liquidator/keeper/keeper_test.go | 136 +++++++ x/liquidator/keeper/params.go | 18 + x/liquidator/keeper/querier.go | 45 +++ x/liquidator/keeper/test_common.go | 170 +++++++++ x/liquidator/module.go | 130 +++++++ x/liquidator/types/codec.go | 20 + x/liquidator/types/expected_keepers.go | 32 ++ x/liquidator/types/genesis.go | 22 ++ x/liquidator/types/keys.go | 18 + x/liquidator/types/msg.go | 97 +++++ x/liquidator/types/params.go | 100 +++++ x/liquidator/types/querier.go | 22 ++ x/liquidator/types/types.go | 30 ++ x/pricefeed/alias.go | 64 ++++ x/pricefeed/client/cli/query.go | 93 +++++ x/pricefeed/client/cli/tx.go | 63 ++++ x/pricefeed/client/rest/rest.go | 113 ++++++ x/pricefeed/doc.go | 6 + x/pricefeed/genesis.go | 51 +++ x/pricefeed/handler.go | 50 +++ x/pricefeed/keeper.go | 280 ++++++++++++++ x/pricefeed/keeper_test.go | 124 ++++++ x/pricefeed/module.go | 129 +++++++ x/pricefeed/querier.go | 74 ++++ x/pricefeed/test_common.go | 57 +++ x/pricefeed/types/codec.go | 16 + x/pricefeed/types/errors.go | 48 +++ x/pricefeed/types/genesis.go | 63 ++++ x/pricefeed/types/key.go | 12 + x/pricefeed/types/msgs.go | 68 ++++ x/pricefeed/types/msgs_test.go | 40 ++ x/pricefeed/types/params.go | 85 +++++ x/pricefeed/types/querier.go | 35 ++ x/pricefeed/types/types.go | 67 ++++ 100 files changed, 7655 insertions(+) create mode 100644 x/auction/abci.go create mode 100644 x/auction/abci_test.go create mode 100644 x/auction/alias.go create mode 100644 x/auction/app_test.go create mode 100644 x/auction/client/cli/query.go create mode 100644 x/auction/client/cli/tx.go create mode 100644 x/auction/client/rest/query.go create mode 100644 x/auction/client/rest/rest.go create mode 100644 x/auction/client/rest/tx.go create mode 100644 x/auction/doc.go create mode 100644 x/auction/genesis.go create mode 100644 x/auction/handler.go create mode 100644 x/auction/keeper/keeper.go create mode 100644 x/auction/keeper/keeper_test.go create mode 100644 x/auction/keeper/params.go create mode 100644 x/auction/keeper/querier.go create mode 100644 x/auction/keeper/test_common.go create mode 100644 x/auction/module.go create mode 100644 x/auction/types/auctions.go create mode 100644 x/auction/types/auctions_test.go create mode 100644 x/auction/types/codec.go create mode 100644 x/auction/types/expected_keepers.go create mode 100644 x/auction/types/genesis.go create mode 100644 x/auction/types/keys.go create mode 100644 x/auction/types/msg.go create mode 100644 x/auction/types/msg_test.go create mode 100644 x/auction/types/params.go create mode 100644 x/auction/types/quierier.go create mode 100644 x/auction/types/utils.go create mode 100644 x/cdp/alias.go create mode 100644 x/cdp/app_test.go create mode 100644 x/cdp/client/cli/query.go create mode 100644 x/cdp/client/cli/tx.go create mode 100644 x/cdp/client/module_client.go create mode 100644 x/cdp/client/rest/query.go create mode 100644 x/cdp/client/rest/rest.go create mode 100644 x/cdp/client/rest/tx.go create mode 100644 x/cdp/doc.go create mode 100644 x/cdp/genesis.go create mode 100644 x/cdp/handler.go create mode 100644 x/cdp/keeper/bank_test.go create mode 100644 x/cdp/keeper/keeper.go create mode 100644 x/cdp/keeper/keeper_test.go create mode 100644 x/cdp/keeper/params.go create mode 100644 x/cdp/keeper/querier.go create mode 100644 x/cdp/keeper/test_common.go create mode 100644 x/cdp/module.go create mode 100644 x/cdp/test_common.go create mode 100644 x/cdp/types/codec.go create mode 100644 x/cdp/types/expected_keepers.go create mode 100644 x/cdp/types/genesis.go create mode 100644 x/cdp/types/keys.go create mode 100644 x/cdp/types/msg.go create mode 100644 x/cdp/types/params.go create mode 100644 x/cdp/types/querier.go create mode 100644 x/cdp/types/types.go create mode 100644 x/liquidator/alias.go create mode 100644 x/liquidator/client/cli/query.go create mode 100644 x/liquidator/client/cli/tx.go create mode 100644 x/liquidator/client/rest/query.go create mode 100644 x/liquidator/client/rest/rest.go create mode 100644 x/liquidator/client/rest/tx.go create mode 100644 x/liquidator/doc.go create mode 100644 x/liquidator/genesis.go create mode 100644 x/liquidator/handler.go create mode 100644 x/liquidator/keeper/keeper.go create mode 100644 x/liquidator/keeper/keeper_test.go create mode 100644 x/liquidator/keeper/params.go create mode 100644 x/liquidator/keeper/querier.go create mode 100644 x/liquidator/keeper/test_common.go create mode 100644 x/liquidator/module.go create mode 100644 x/liquidator/types/codec.go create mode 100644 x/liquidator/types/expected_keepers.go create mode 100644 x/liquidator/types/genesis.go create mode 100644 x/liquidator/types/keys.go create mode 100644 x/liquidator/types/msg.go create mode 100644 x/liquidator/types/params.go create mode 100644 x/liquidator/types/querier.go create mode 100644 x/liquidator/types/types.go create mode 100644 x/pricefeed/alias.go create mode 100644 x/pricefeed/client/cli/query.go create mode 100644 x/pricefeed/client/cli/tx.go create mode 100644 x/pricefeed/client/rest/rest.go create mode 100644 x/pricefeed/doc.go create mode 100644 x/pricefeed/genesis.go create mode 100644 x/pricefeed/handler.go create mode 100644 x/pricefeed/keeper.go create mode 100644 x/pricefeed/keeper_test.go create mode 100644 x/pricefeed/module.go create mode 100644 x/pricefeed/querier.go create mode 100644 x/pricefeed/test_common.go create mode 100644 x/pricefeed/types/codec.go create mode 100644 x/pricefeed/types/errors.go create mode 100644 x/pricefeed/types/genesis.go create mode 100644 x/pricefeed/types/key.go create mode 100644 x/pricefeed/types/msgs.go create mode 100644 x/pricefeed/types/msgs_test.go create mode 100644 x/pricefeed/types/params.go create mode 100644 x/pricefeed/types/querier.go create mode 100644 x/pricefeed/types/types.go diff --git a/x/auction/abci.go b/x/auction/abci.go new file mode 100644 index 00000000..164b35b7 --- /dev/null +++ b/x/auction/abci.go @@ -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? + } + } +} diff --git a/x/auction/abci_test.go b/x/auction/abci_test.go new file mode 100644 index 00000000..2fb36bda --- /dev/null +++ b/x/auction/abci_test.go @@ -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) +} diff --git a/x/auction/alias.go b/x/auction/alias.go new file mode 100644 index 00000000..58040464 --- /dev/null +++ b/x/auction/alias.go @@ -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 +) diff --git a/x/auction/app_test.go b/x/auction/app_test.go new file mode 100644 index 00000000..20106efb --- /dev/null +++ b/x/auction/app_test.go @@ -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 +} diff --git a/x/auction/client/cli/query.go b/x/auction/client/cli/query.go new file mode 100644 index 00000000..3c5127fa --- /dev/null +++ b/x/auction/client/cli/query.go @@ -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) + }, + } +} diff --git a/x/auction/client/cli/tx.go b/x/auction/client/cli/tx.go new file mode 100644 index 00000000..c663658b --- /dev/null +++ b/x/auction/client/cli/tx.go @@ -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}) + }, + } +} diff --git a/x/auction/client/rest/query.go b/x/auction/client/rest/query.go new file mode 100644 index 00000000..87a21915 --- /dev/null +++ b/x/auction/client/rest/query.go @@ -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) + } +} diff --git a/x/auction/client/rest/rest.go b/x/auction/client/rest/rest.go new file mode 100644 index 00000000..84eb85fd --- /dev/null +++ b/x/auction/client/rest/rest.go @@ -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) +} diff --git a/x/auction/client/rest/tx.go b/x/auction/client/rest/tx.go new file mode 100644 index 00000000..61c9e18f --- /dev/null +++ b/x/auction/client/rest/tx.go @@ -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}) + + } +} diff --git a/x/auction/doc.go b/x/auction/doc.go new file mode 100644 index 00000000..79e8def7 --- /dev/null +++ b/x/auction/doc.go @@ -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 diff --git a/x/auction/genesis.go b/x/auction/genesis.go new file mode 100644 index 00000000..1b7a327c --- /dev/null +++ b/x/auction/genesis.go @@ -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) +} diff --git a/x/auction/handler.go b/x/auction/handler.go new file mode 100644 index 00000000..ec238957 --- /dev/null +++ b/x/auction/handler.go @@ -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{} +} diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go new file mode 100644 index 00000000..1ffc561b --- /dev/null +++ b/x/auction/keeper/keeper.go @@ -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 +} diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go new file mode 100644 index 00000000..6c77b32e --- /dev/null +++ b/x/auction/keeper/keeper_test.go @@ -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 +} diff --git a/x/auction/keeper/params.go b/x/auction/keeper/params.go new file mode 100644 index 00000000..c832caf5 --- /dev/null +++ b/x/auction/keeper/params.go @@ -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, ¶ms) +} + +// GetParams gets the auth module's parameters. +func (k Keeper) GetParams(ctx sdk.Context) (params types.AuctionParams) { + k.paramSubspace.GetParamSet(ctx, ¶ms) + return +} \ No newline at end of file diff --git a/x/auction/keeper/querier.go b/x/auction/keeper/querier.go new file mode 100644 index 00000000..15748733 --- /dev/null +++ b/x/auction/keeper/querier.go @@ -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 +} diff --git a/x/auction/keeper/test_common.go b/x/auction/keeper/test_common.go new file mode 100644 index 00000000..b5702438 --- /dev/null +++ b/x/auction/keeper/test_common.go @@ -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 +} diff --git a/x/auction/module.go b/x/auction/module.go new file mode 100644 index 00000000..7e0f1bd7 --- /dev/null +++ b/x/auction/module.go @@ -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{} +} diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go new file mode 100644 index 00000000..386fb232 --- /dev/null +++ b/x/auction/types/auctions.go @@ -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 +} diff --git a/x/auction/types/auctions_test.go b/x/auction/types/auctions_test.go new file mode 100644 index 00000000..b2252f55 --- /dev/null +++ b/x/auction/types/auctions_test.go @@ -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) +} diff --git a/x/auction/types/codec.go b/x/auction/types/codec.go new file mode 100644 index 00000000..00e97c8b --- /dev/null +++ b/x/auction/types/codec.go @@ -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) +} diff --git a/x/auction/types/expected_keepers.go b/x/auction/types/expected_keepers.go new file mode 100644 index 00000000..5a99550d --- /dev/null +++ b/x/auction/types/expected_keepers.go @@ -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) +} diff --git a/x/auction/types/genesis.go b/x/auction/types/genesis.go new file mode 100644 index 00000000..31c1efb5 --- /dev/null +++ b/x/auction/types/genesis.go @@ -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 +} diff --git a/x/auction/types/keys.go b/x/auction/types/keys.go new file mode 100644 index 00000000..cf2153b3 --- /dev/null +++ b/x/auction/types/keys.go @@ -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 +) diff --git a/x/auction/types/msg.go b/x/auction/types/msg.go new file mode 100644 index 00000000..5cdc60e3 --- /dev/null +++ b/x/auction/types/msg.go @@ -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} +// } diff --git a/x/auction/types/msg_test.go b/x/auction/types/msg_test.go new file mode 100644 index 00000000..cfb51b5c --- /dev/null +++ b/x/auction/types/msg_test.go @@ -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()) + } + }) + } +} diff --git a/x/auction/types/params.go b/x/auction/types/params.go new file mode 100644 index 00000000..527cd844 --- /dev/null +++ b/x/auction/types/params.go @@ -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 +} diff --git a/x/auction/types/quierier.go b/x/auction/types/quierier.go new file mode 100644 index 00000000..571c3908 --- /dev/null +++ b/x/auction/types/quierier.go @@ -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") +} diff --git a/x/auction/types/utils.go b/x/auction/types/utils.go new file mode 100644 index 00000000..b1aad11e --- /dev/null +++ b/x/auction/types/utils.go @@ -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 +} diff --git a/x/cdp/alias.go b/x/cdp/alias.go new file mode 100644 index 00000000..f22e0366 --- /dev/null +++ b/x/cdp/alias.go @@ -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 +) diff --git a/x/cdp/app_test.go b/x/cdp/app_test.go new file mode 100644 index 00000000..39737d43 --- /dev/null +++ b/x/cdp/app_test.go @@ -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))) +} diff --git a/x/cdp/client/cli/query.go b/x/cdp/client/cli/query.go new file mode 100644 index 00000000..e1cdaefd --- /dev/null +++ b/x/cdp/client/cli/query.go @@ -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) + }, + } +} diff --git a/x/cdp/client/cli/tx.go b/x/cdp/client/cli/tx.go new file mode 100644 index 00000000..5bb0a0a7 --- /dev/null +++ b/x/cdp/client/cli/tx.go @@ -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}) + }, + } +} diff --git a/x/cdp/client/module_client.go b/x/cdp/client/module_client.go new file mode 100644 index 00000000..799aaa0c --- /dev/null +++ b/x/cdp/client/module_client.go @@ -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 +} diff --git a/x/cdp/client/rest/query.go b/x/cdp/client/rest/query.go new file mode 100644 index 00000000..94d93d2c --- /dev/null +++ b/x/cdp/client/rest/query.go @@ -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) + } +} diff --git a/x/cdp/client/rest/rest.go b/x/cdp/client/rest/rest.go new file mode 100644 index 00000000..84eb85fd --- /dev/null +++ b/x/cdp/client/rest/rest.go @@ -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) +} diff --git a/x/cdp/client/rest/tx.go b/x/cdp/client/rest/tx.go new file mode 100644 index 00000000..e1459a46 --- /dev/null +++ b/x/cdp/client/rest/tx.go @@ -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}) + } +} diff --git a/x/cdp/doc.go b/x/cdp/doc.go new file mode 100644 index 00000000..1207e925 --- /dev/null +++ b/x/cdp/doc.go @@ -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 diff --git a/x/cdp/genesis.go b/x/cdp/genesis.go new file mode 100644 index 00000000..5bd639fa --- /dev/null +++ b/x/cdp/genesis.go @@ -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, + } +} diff --git a/x/cdp/handler.go b/x/cdp/handler.go new file mode 100644 index 00000000..c7cd84d6 --- /dev/null +++ b/x/cdp/handler.go @@ -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{} +} diff --git a/x/cdp/keeper/bank_test.go b/x/cdp/keeper/bank_test.go new file mode 100644 index 00000000..ddac5479 --- /dev/null +++ b/x/cdp/keeper/bank_test.go @@ -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)) + }) + } +} diff --git a/x/cdp/keeper/keeper.go b/x/cdp/keeper/keeper.go new file mode 100644 index 00000000..ee9f2818 --- /dev/null +++ b/x/cdp/keeper/keeper.go @@ -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 +} diff --git a/x/cdp/keeper/keeper_test.go b/x/cdp/keeper/keeper_test.go new file mode 100644 index 00000000..17f03000 --- /dev/null +++ b/x/cdp/keeper/keeper_test.go @@ -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) +} diff --git a/x/cdp/keeper/params.go b/x/cdp/keeper/params.go new file mode 100644 index 00000000..4b12e9ae --- /dev/null +++ b/x/cdp/keeper/params.go @@ -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) +} \ No newline at end of file diff --git a/x/cdp/keeper/querier.go b/x/cdp/keeper/querier.go new file mode 100644 index 00000000..c8d3f04f --- /dev/null +++ b/x/cdp/keeper/querier.go @@ -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 +} diff --git a/x/cdp/keeper/test_common.go b/x/cdp/keeper/test_common.go new file mode 100644 index 00000000..474b069e --- /dev/null +++ b/x/cdp/keeper/test_common.go @@ -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"}, + } +} diff --git a/x/cdp/module.go b/x/cdp/module.go new file mode 100644 index 00000000..792b2cbe --- /dev/null +++ b/x/cdp/module.go @@ -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{} +} diff --git a/x/cdp/test_common.go b/x/cdp/test_common.go new file mode 100644 index 00000000..54066f56 --- /dev/null +++ b/x/cdp/test_common.go @@ -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...) } diff --git a/x/cdp/types/codec.go b/x/cdp/types/codec.go new file mode 100644 index 00000000..118eb582 --- /dev/null +++ b/x/cdp/types/codec.go @@ -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) +} diff --git a/x/cdp/types/expected_keepers.go b/x/cdp/types/expected_keepers.go new file mode 100644 index 00000000..fe41bed2 --- /dev/null +++ b/x/cdp/types/expected_keepers.go @@ -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 +} diff --git a/x/cdp/types/genesis.go b/x/cdp/types/genesis.go new file mode 100644 index 00000000..96596e46 --- /dev/null +++ b/x/cdp/types/genesis.go @@ -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 +} diff --git a/x/cdp/types/keys.go b/x/cdp/types/keys.go new file mode 100644 index 00000000..e096801d --- /dev/null +++ b/x/cdp/types/keys.go @@ -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 +) \ No newline at end of file diff --git a/x/cdp/types/msg.go b/x/cdp/types/msg.go new file mode 100644 index 00000000..dc9bd18d --- /dev/null +++ b/x/cdp/types/msg.go @@ -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 +} diff --git a/x/cdp/types/params.go b/x/cdp/types/params.go new file mode 100644 index 00000000..d4afa0dd --- /dev/null +++ b/x/cdp/types/params.go @@ -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"}, + } +} diff --git a/x/cdp/types/querier.go b/x/cdp/types/querier.go new file mode 100644 index 00000000..0f1c6fe1 --- /dev/null +++ b/x/cdp/types/querier.go @@ -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"` +} diff --git a/x/cdp/types/types.go b/x/cdp/types/types.go new file mode 100644 index 00000000..02051ed4 --- /dev/null +++ b/x/cdp/types/types.go @@ -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 +} diff --git a/x/liquidator/alias.go b/x/liquidator/alias.go new file mode 100644 index 00000000..da53e9d2 --- /dev/null +++ b/x/liquidator/alias.go @@ -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 +) diff --git a/x/liquidator/client/cli/query.go b/x/liquidator/client/cli/query.go new file mode 100644 index 00000000..45ea4d91 --- /dev/null +++ b/x/liquidator/client/cli/query.go @@ -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) + }, + } +} diff --git a/x/liquidator/client/cli/tx.go b/x/liquidator/client/cli/tx.go new file mode 100644 index 00000000..bfd72d45 --- /dev/null +++ b/x/liquidator/client/cli/tx.go @@ -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 +} diff --git a/x/liquidator/client/rest/query.go b/x/liquidator/client/rest/query.go new file mode 100644 index 00000000..7fea3a3d --- /dev/null +++ b/x/liquidator/client/rest/query.go @@ -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 + } +} diff --git a/x/liquidator/client/rest/rest.go b/x/liquidator/client/rest/rest.go new file mode 100644 index 00000000..6369945e --- /dev/null +++ b/x/liquidator/client/rest/rest.go @@ -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) +} \ No newline at end of file diff --git a/x/liquidator/client/rest/tx.go b/x/liquidator/client/rest/tx.go new file mode 100644 index 00000000..e854fe93 --- /dev/null +++ b/x/liquidator/client/rest/tx.go @@ -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}) + } +} diff --git a/x/liquidator/doc.go b/x/liquidator/doc.go new file mode 100644 index 00000000..ee48245b --- /dev/null +++ b/x/liquidator/doc.go @@ -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 diff --git a/x/liquidator/genesis.go b/x/liquidator/genesis.go new file mode 100644 index 00000000..7c3b5adf --- /dev/null +++ b/x/liquidator/genesis.go @@ -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, + } +} diff --git a/x/liquidator/handler.go b/x/liquidator/handler.go new file mode 100644 index 00000000..ace3fb9d --- /dev/null +++ b/x/liquidator/handler.go @@ -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 +// } diff --git a/x/liquidator/keeper/keeper.go b/x/liquidator/keeper/keeper.go new file mode 100644 index 00000000..e5a15014 --- /dev/null +++ b/x/liquidator/keeper/keeper.go @@ -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) +} diff --git a/x/liquidator/keeper/keeper_test.go b/x/liquidator/keeper/keeper_test.go new file mode 100644 index 00000000..d69b5547 --- /dev/null +++ b/x/liquidator/keeper/keeper_test.go @@ -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) +} diff --git a/x/liquidator/keeper/params.go b/x/liquidator/keeper/params.go new file mode 100644 index 00000000..71d0f31b --- /dev/null +++ b/x/liquidator/keeper/params.go @@ -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, ¶ms) + return params +} + +// SetParams sets params for the liquidator module +func (k Keeper) SetParams(ctx sdk.Context, params types.LiquidatorParams) { + k.paramSubspace.SetParamSet(ctx, ¶ms) +} \ No newline at end of file diff --git a/x/liquidator/keeper/querier.go b/x/liquidator/keeper/querier.go new file mode 100644 index 00000000..4bff0ad5 --- /dev/null +++ b/x/liquidator/keeper/querier.go @@ -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 +} diff --git a/x/liquidator/keeper/test_common.go b/x/liquidator/keeper/test_common.go new file mode 100644 index 00000000..62751c9f --- /dev/null +++ b/x/liquidator/keeper/test_common.go @@ -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), + }, + }, + } +} diff --git a/x/liquidator/module.go b/x/liquidator/module.go new file mode 100644 index 00000000..7f163de8 --- /dev/null +++ b/x/liquidator/module.go @@ -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{} +} diff --git a/x/liquidator/types/codec.go b/x/liquidator/types/codec.go new file mode 100644 index 00000000..da70df34 --- /dev/null +++ b/x/liquidator/types/codec.go @@ -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) +} diff --git a/x/liquidator/types/expected_keepers.go b/x/liquidator/types/expected_keepers.go new file mode 100644 index 00000000..deea2bd4 --- /dev/null +++ b/x/liquidator/types/expected_keepers.go @@ -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) +} diff --git a/x/liquidator/types/genesis.go b/x/liquidator/types/genesis.go new file mode 100644 index 00000000..0b4bdf62 --- /dev/null +++ b/x/liquidator/types/genesis.go @@ -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 +} diff --git a/x/liquidator/types/keys.go b/x/liquidator/types/keys.go new file mode 100644 index 00000000..9e4ffd6b --- /dev/null +++ b/x/liquidator/types/keys.go @@ -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 +) \ No newline at end of file diff --git a/x/liquidator/types/msg.go b/x/liquidator/types/msg.go new file mode 100644 index 00000000..3b5cbe81 --- /dev/null +++ b/x/liquidator/types/msg.go @@ -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} } diff --git a/x/liquidator/types/params.go b/x/liquidator/types/params.go new file mode 100644 index 00000000..c72c8285 --- /dev/null +++ b/x/liquidator/types/params.go @@ -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 +} diff --git a/x/liquidator/types/querier.go b/x/liquidator/types/querier.go new file mode 100644 index 00000000..5fb8d6be --- /dev/null +++ b/x/liquidator/types/querier.go @@ -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? +} \ No newline at end of file diff --git a/x/liquidator/types/types.go b/x/liquidator/types/types.go new file mode 100644 index 00000000..fdf1c536 --- /dev/null +++ b/x/liquidator/types/types.go @@ -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 +} diff --git a/x/pricefeed/alias.go b/x/pricefeed/alias.go new file mode 100644 index 00000000..61be278c --- /dev/null +++ b/x/pricefeed/alias.go @@ -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 +) diff --git a/x/pricefeed/client/cli/query.go b/x/pricefeed/client/cli/query.go new file mode 100644 index 00000000..3de15af3 --- /dev/null +++ b/x/pricefeed/client/cli/query.go @@ -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) + }, + } +} diff --git a/x/pricefeed/client/cli/tx.go b/x/pricefeed/client/cli/tx.go new file mode 100644 index 00000000..8c505856 --- /dev/null +++ b/x/pricefeed/client/cli/tx.go @@ -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}) + }, + } +} diff --git a/x/pricefeed/client/rest/rest.go b/x/pricefeed/client/rest/rest.go new file mode 100644 index 00000000..ea6990a6 --- /dev/null +++ b/x/pricefeed/client/rest/rest.go @@ -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) + } +} diff --git a/x/pricefeed/doc.go b/x/pricefeed/doc.go new file mode 100644 index 00000000..0d403145 --- /dev/null +++ b/x/pricefeed/doc.go @@ -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 diff --git a/x/pricefeed/genesis.go b/x/pricefeed/genesis.go new file mode 100644 index 00000000..2dafc2b3 --- /dev/null +++ b/x/pricefeed/genesis.go @@ -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, + } +} diff --git a/x/pricefeed/handler.go b/x/pricefeed/handler.go new file mode 100644 index 00000000..1b54d4e4 --- /dev/null +++ b/x/pricefeed/handler.go @@ -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 +} diff --git a/x/pricefeed/keeper.go b/x/pricefeed/keeper.go new file mode 100644 index 00000000..40473812 --- /dev/null +++ b/x/pricefeed/keeper.go @@ -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 +} diff --git a/x/pricefeed/keeper_test.go b/x/pricefeed/keeper_test.go new file mode 100644 index 00000000..9ee5f857 --- /dev/null +++ b/x/pricefeed/keeper_test.go @@ -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) + +} diff --git a/x/pricefeed/module.go b/x/pricefeed/module.go new file mode 100644 index 00000000..7b52be5a --- /dev/null +++ b/x/pricefeed/module.go @@ -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) +} diff --git a/x/pricefeed/querier.go b/x/pricefeed/querier.go new file mode 100644 index 00000000..ecc7936c --- /dev/null +++ b/x/pricefeed/querier.go @@ -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 +} \ No newline at end of file diff --git a/x/pricefeed/test_common.go b/x/pricefeed/test_common.go new file mode 100644 index 00000000..5fea3df1 --- /dev/null +++ b/x/pricefeed/test_common.go @@ -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{} + } +} diff --git a/x/pricefeed/types/codec.go b/x/pricefeed/types/codec.go new file mode 100644 index 00000000..cd0437e3 --- /dev/null +++ b/x/pricefeed/types/codec.go @@ -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) +} diff --git a/x/pricefeed/types/errors.go b/x/pricefeed/types/errors.go new file mode 100644 index 00000000..7a7c4548 --- /dev/null +++ b/x/pricefeed/types/errors.go @@ -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.")) +} diff --git a/x/pricefeed/types/genesis.go b/x/pricefeed/types/genesis.go new file mode 100644 index 00000000..d798e6d8 --- /dev/null +++ b/x/pricefeed/types/genesis.go @@ -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 +} diff --git a/x/pricefeed/types/key.go b/x/pricefeed/types/key.go new file mode 100644 index 00000000..e39d2d56 --- /dev/null +++ b/x/pricefeed/types/key.go @@ -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 +) diff --git a/x/pricefeed/types/msgs.go b/x/pricefeed/types/msgs.go new file mode 100644 index 00000000..3aa23ece --- /dev/null +++ b/x/pricefeed/types/msgs.go @@ -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 +} diff --git a/x/pricefeed/types/msgs_test.go b/x/pricefeed/types/msgs_test.go new file mode 100644 index 00000000..73857091 --- /dev/null +++ b/x/pricefeed/types/msgs_test.go @@ -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()) + } + }) + } +} diff --git a/x/pricefeed/types/params.go b/x/pricefeed/types/params.go new file mode 100644 index 00000000..a3d86768 --- /dev/null +++ b/x/pricefeed/types/params.go @@ -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{}) +} diff --git a/x/pricefeed/types/querier.go b/x/pricefeed/types/querier.go new file mode 100644 index 00000000..7439c362 --- /dev/null +++ b/x/pricefeed/types/querier.go @@ -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") +} + diff --git a/x/pricefeed/types/types.go b/x/pricefeed/types/types.go new file mode 100644 index 00000000..e07d9b55 --- /dev/null +++ b/x/pricefeed/types/types.go @@ -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]) } From d5da161dd81187d3fc173eff55e3594b2977c111 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Wed, 27 Nov 2019 09:45:59 -0500 Subject: [PATCH 02/99] feat: draft params for pricefeed --- go.sum | 1 + x/pricefeed/abci.go | 20 ++ x/pricefeed/alias.go | 53 +++-- x/pricefeed/client/cli/tx.go | 6 +- x/pricefeed/client/rest/rest.go | 5 +- x/pricefeed/genesis.go | 24 +- x/pricefeed/handler.go | 16 +- x/pricefeed/keeper.go | 280 ------------------------ x/pricefeed/keeper/keeper.go | 160 ++++++++++++++ x/pricefeed/keeper/keeper_test.go | 141 ++++++++++++ x/pricefeed/keeper/params.go | 64 ++++++ x/pricefeed/{ => keeper}/querier.go | 20 +- x/pricefeed/{ => keeper}/test_common.go | 25 +-- x/pricefeed/keeper_test.go | 124 ----------- x/pricefeed/types/asset.go | 98 +++++++++ x/pricefeed/types/genesis.go | 26 +-- x/pricefeed/types/key.go | 18 ++ x/pricefeed/types/msgs.go | 9 +- x/pricefeed/types/msgs_test.go | 5 +- x/pricefeed/types/params.go | 89 ++++---- x/pricefeed/types/types.go | 67 ------ 21 files changed, 620 insertions(+), 631 deletions(-) create mode 100644 x/pricefeed/abci.go delete mode 100644 x/pricefeed/keeper.go create mode 100644 x/pricefeed/keeper/keeper.go create mode 100644 x/pricefeed/keeper/keeper_test.go create mode 100644 x/pricefeed/keeper/params.go rename x/pricefeed/{ => keeper}/querier.go (87%) rename x/pricefeed/{ => keeper}/test_common.go (55%) delete mode 100644 x/pricefeed/keeper_test.go create mode 100644 x/pricefeed/types/asset.go delete mode 100644 x/pricefeed/types/types.go diff --git a/go.sum b/go.sum index b61a9570..b8895b5c 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,7 @@ github.com/cosmos/cosmos-sdk v0.34.4-0.20191010155330-64a27412505c/go.mod h1:Fxj github.com/cosmos/cosmos-sdk v0.34.4-0.20191010193331-18de630d0ae1 h1:yb+E8HGzFnO0YwLS6OCBIAVWtN8KfCYoKeO9mgAmQN0= github.com/cosmos/cosmos-sdk v0.34.4-0.20191010193331-18de630d0ae1/go.mod h1:IGBhkbOK1ebLqMWjtgo99zUxWHsA5IOb6N9CI8nHs0Y= github.com/cosmos/cosmos-sdk v0.37.1 h1:mz5W3Au32VIPPtrY65dheVYeVDSFfS3eSSmuIj+cXsI= +github.com/cosmos/cosmos-sdk v0.37.4 h1:1ioXxkpiS+wOgaUbROeDIyuF7hciU5nti0TSyBmV2Ok= github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8 h1:Iwin12wRQtyZhH6FV3ykFcdGNlYEzoeR0jN8Vn+JWsI= github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= github.com/cosmos/ledger-cosmos-go v0.10.3 h1:Qhi5yTR5Pg1CaTpd00pxlGwNl4sFRdtK1J96OTjeFFc= diff --git a/x/pricefeed/abci.go b/x/pricefeed/abci.go new file mode 100644 index 00000000..67a0cb0b --- /dev/null +++ b/x/pricefeed/abci.go @@ -0,0 +1,20 @@ +package pricefeed + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// EndBlocker updates the current pricefeed +func EndBlocker(ctx sdk.Context, k Keeper) { + // Update the current price of each asset. + for _, a := range k.GetAssetParams(ctx) { + if a.Active { + err := k.SetCurrentPrices(ctx, a.AssetCode) + if err != nil { + // TODO emit an event that price failed to update + continue + } + } + } + return +} diff --git a/x/pricefeed/alias.go b/x/pricefeed/alias.go index 61be278c..bbe6e654 100644 --- a/x/pricefeed/alias.go +++ b/x/pricefeed/alias.go @@ -2,26 +2,34 @@ // autogenerated code using github.com/rigelrozanski/multitool // aliases generated for the following subdirectories: // ALIASGEN: github.com/kava-labs/kava/x/pricefeed/types/ +// ALIASGEN: github.com/kava-labs/kava/x/pricefeed/keeper/ package pricefeed import ( + "github.com/kava-labs/kava/x/pricefeed/keeper" "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 + 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 + QuerierRoute = types.QuerierRoute + DefaultParamspace = types.DefaultParamspace + RawPriceFeedPrefix = types.RawPriceFeedPrefix + CurrentPricePrefix = types.CurrentPricePrefix + AssetPrefix = types.AssetPrefix + OraclePrefix = types.OraclePrefix + TypeMsgPostPrice = types.TypeMsgPostPrice + QueryCurrentPrice = types.QueryCurrentPrice + QueryRawPrices = types.QueryRawPrices + QueryAssets = types.QueryAssets ) var ( @@ -37,28 +45,29 @@ var ( ValidateGenesis = types.ValidateGenesis NewMsgPostPrice = types.NewMsgPostPrice ParamKeyTable = types.ParamKeyTable - NewAssetParams = types.NewAssetParams - DefaultAssetParams = types.DefaultAssetParams - NewOracleParams = types.NewOracleParams - DefaultOracleParams = types.DefaultOracleParams + NewParams = types.NewParams + DefaultParams = types.DefaultParams + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier // variable aliases - ModuleCdc = types.ModuleCdc - ParamStoreKeyOracles = types.ParamStoreKeyOracles - ParamStoreKeyAssets = types.ParamStoreKeyAssets + ModuleCdc = types.ModuleCdc + KeyAssets = types.KeyAssets ) type ( GenesisState = types.GenesisState MsgPostPrice = types.MsgPostPrice - AssetParams = types.AssetParams - OracleParams = types.OracleParams + Params = types.Params ParamSubspace = types.ParamSubspace QueryRawPricesResp = types.QueryRawPricesResp QueryAssetsResp = types.QueryAssetsResp Asset = types.Asset + Assets = types.Assets Oracle = types.Oracle + Oracles = types.Oracles CurrentPrice = types.CurrentPrice PostedPrice = types.PostedPrice SortDecs = types.SortDecs + Keeper = keeper.Keeper ) diff --git a/x/pricefeed/client/cli/tx.go b/x/pricefeed/client/cli/tx.go index 8c505856..199ad2a0 100644 --- a/x/pricefeed/client/cli/tx.go +++ b/x/pricefeed/client/cli/tx.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "time" "github.com/spf13/cobra" @@ -12,6 +13,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth/client/utils" "github.com/kava-labs/kava/x/pricefeed/types" + tmtime "github.com/tendermint/tendermint/types/time" ) // GetTxCmd returns the transaction commands for this module @@ -47,11 +49,13 @@ func GetCmdPostPrice(cdc *codec.Codec) *cobra.Command { if err != nil { return err } - expiry, ok := sdk.NewIntFromString(args[2]) + expiryInt, ok := sdk.NewIntFromString(args[2]) if !ok { fmt.Printf("invalid expiry - %s \n", args[2]) return nil } + expiry := tmtime.Canonical(time.Unix(expiryInt.Int64(), 0)) + msg := types.NewMsgPostPrice(cliCtx.GetFromAddress(), args[0], price, expiry) err = msg.ValidateBasic() if err != nil { diff --git a/x/pricefeed/client/rest/rest.go b/x/pricefeed/client/rest/rest.go index ea6990a6..988d8ea5 100644 --- a/x/pricefeed/client/rest/rest.go +++ b/x/pricefeed/client/rest/rest.go @@ -3,6 +3,7 @@ package rest import ( "fmt" "net/http" + "time" "github.com/cosmos/cosmos-sdk/client/context" sdk "github.com/cosmos/cosmos-sdk/types" @@ -10,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/auth/client/utils" "github.com/gorilla/mux" "github.com/kava-labs/kava/x/pricefeed/types" + tmtime "github.com/tendermint/tendermint/types/time" ) const ( @@ -57,11 +59,12 @@ func postPriceHandler(cliCtx context.CLIContext) http.HandlerFunc { return } - expiry, ok := sdk.NewIntFromString(req.Expiry) + expiryInt, ok := sdk.NewIntFromString(req.Expiry) if !ok { rest.WriteErrorResponse(w, http.StatusBadRequest, "invalid expiry") return } + expiry := tmtime.Canonical(time.Unix(expiryInt.Int64(), 0)) // create the message msg := types.NewMsgPostPrice(addr, req.AssetCode, price, expiry) diff --git a/x/pricefeed/genesis.go b/x/pricefeed/genesis.go index 2dafc2b3..87cb45fc 100644 --- a/x/pricefeed/genesis.go +++ b/x/pricefeed/genesis.go @@ -4,29 +4,25 @@ 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) + keeper.SetParams(ctx, data.Params) // 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) + _, err := keeper.SetPrice(ctx, pp.OracleAddress, 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) + for _, a := range data.Params.Assets { + if a.Active { + _ = keeper.SetCurrentPrices(ctx, a.AssetCode) + } } } @@ -34,18 +30,16 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { // Get the params for assets and oracles - assetParams := keeper.GetAssetParams(ctx) - oracleParams := keeper.GetOracleParams(ctx) + params := keeper.GetParams(ctx) var postedPrices []PostedPrice - for _, asset := range keeper.GetAssets(ctx) { + for _, asset := range keeper.GetAssetParams(ctx) { pp := keeper.GetRawPrices(ctx, asset.AssetCode) postedPrices = append(postedPrices, pp...) } return GenesisState{ - AssetParams: assetParams, - OracleParams: oracleParams, + Params: params, PostedPrices: postedPrices, } } diff --git a/x/pricefeed/handler.go b/x/pricefeed/handler.go index 1b54d4e4..704839f9 100644 --- a/x/pricefeed/handler.go +++ b/x/pricefeed/handler.go @@ -29,22 +29,10 @@ func HandleMsgPostPrice( msg MsgPostPrice) sdk.Result { // TODO cleanup message validation and errors - err := k.ValidatePostPrice(ctx, msg) + _, err := k.GetOracle(ctx, msg.AssetCode, msg.From) if err != nil { - return err.Result() + return ErrInvalidOracle(k.Codespace()).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 -} diff --git a/x/pricefeed/keeper.go b/x/pricefeed/keeper.go deleted file mode 100644 index 40473812..00000000 --- a/x/pricefeed/keeper.go +++ /dev/null @@ -1,280 +0,0 @@ -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 -} diff --git a/x/pricefeed/keeper/keeper.go b/x/pricefeed/keeper/keeper.go new file mode 100644 index 00000000..a5fcfa0d --- /dev/null +++ b/x/pricefeed/keeper/keeper.go @@ -0,0 +1,160 @@ +package keeper + +import ( + "sort" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/params" + + "github.com/kava-labs/kava/x/pricefeed/types" +) + +// Keeper struct for pricefeed module +type Keeper struct { + // The keys used to access the stores from Context + storeKey sdk.StoreKey + // Codec for binary encoding/decoding + cdc *codec.Codec + // The reference to the Paramstore to get and set pricefeed specific params + paramstore params.Subspace + // 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, paramstore params.Subspace, codespace sdk.CodespaceType, +) Keeper { + return Keeper{ + paramstore: paramstore.WithKeyTable(types.ParamKeyTable()), + storeKey: storeKey, + cdc: cdc, + codespace: codespace, + } +} + +// 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 time.Time) (types.PostedPrice, sdk.Error) { + // If the expiry is less than or equal to the current blockheight, we consider the price valid + if expiry.After(ctx.BlockTime()) { + store := ctx.KVStore(k.storeKey) + prices := k.GetRawPrices(ctx, assetCode) + var index int + found := false + for i := range prices { + if prices[i].OracleAddress.Equals(oracle) { + index = i + found = true + break + } + } + // set the price for that particular oracle + if found { + prices[index] = types.PostedPrice{ + AssetCode: assetCode, OracleAddress: oracle, + Price: price, Expiry: expiry} + } else { + prices = append(prices, types.PostedPrice{ + AssetCode: assetCode, OracleAddress: oracle, + Price: price, Expiry: expiry}) + index = len(prices) - 1 + } + + store.Set( + []byte(types.RawPriceFeedPrefix+assetCode), k.cdc.MustMarshalBinaryBare(prices), + ) + return prices[index], nil + } + return types.PostedPrice{}, types.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, assetCode string) sdk.Error { + _, ok := k.GetAsset(ctx, assetCode) + if !ok { + return types.ErrInvalidAsset(k.codespace) + } + prices := k.GetRawPrices(ctx, assetCode) + var notExpiredPrices []types.CurrentPrice + // filter out expired prices + for _, v := range prices { + if v.Expiry.After(ctx.BlockTime()) { + notExpiredPrices = append(notExpiredPrices, types.CurrentPrice{ + AssetCode: v.AssetCode, + Price: v.Price, + }) + } + } + l := len(notExpiredPrices) + var medianPrice sdk.Dec + // 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 types.ErrNoValidPrice(k.codespace) + } else if l == 1 { + // Return immediately if there's only one price + medianPrice = notExpiredPrices[0].Price + } 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) + } else { + // integer division, so we'll get an integer back, rounded down + medianPrice = notExpiredPrices[l/2].Price + } + } + + store := ctx.KVStore(k.storeKey) + currentPrice := types.CurrentPrice{ + AssetCode: assetCode, + Price: medianPrice, + } + store.Set( + []byte(types.CurrentPricePrefix+assetCode), k.cdc.MustMarshalBinaryBare(currentPrice), + ) + + return nil +} + +// GetCurrentPrice fetches the current median price of all oracles for a specific asset +func (k Keeper) GetCurrentPrice(ctx sdk.Context, assetCode string) types.CurrentPrice { + store := ctx.KVStore(k.storeKey) + bz := store.Get([]byte(types.CurrentPricePrefix + assetCode)) + // TODO panic or return error if not found + var price types.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) []types.PostedPrice { + store := ctx.KVStore(k.storeKey) + bz := store.Get([]byte(types.RawPriceFeedPrefix + assetCode)) + var prices []types.PostedPrice + k.cdc.MustUnmarshalBinaryBare(bz, &prices) + return prices +} + +func (k Keeper) Codespace() sdk.CodespaceType { + return k.codespace +} diff --git a/x/pricefeed/keeper/keeper_test.go b/x/pricefeed/keeper/keeper_test.go new file mode 100644 index 00000000..1c93cb9d --- /dev/null +++ b/x/pricefeed/keeper/keeper_test.go @@ -0,0 +1,141 @@ +package keeper + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" + + "github.com/kava-labs/kava/x/pricefeed/types" +) + +// TestKeeper_SetGetAsset tests adding assets to the pricefeed, getting assets from the store +func TestKeeper_SetGetAsset(t *testing.T) { + helper := getMockApp(t, 0, types.GenesisState{}, nil) + header := abci.Header{ + Height: helper.mApp.LastBlockHeight() + 1, + Time: tmtime.Now()} + helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header}) + ctx := helper.mApp.BaseApp.NewContext(false, header) + + ap := types.Params{ + Assets: []types.Asset{ + types.Asset{AssetCode: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + }, + } + helper.keeper.SetParams(ctx, ap) + assets := helper.keeper.GetAssetParams(ctx) + require.Equal(t, len(assets), 1) + require.Equal(t, assets[0].AssetCode, "tstusd") + + _, found := helper.keeper.GetAsset(ctx, "tstusd") + require.Equal(t, found, true) + + ap = types.Params{ + Assets: []types.Asset{ + types.Asset{AssetCode: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + types.Asset{AssetCode: "tst2usd", BaseAsset: "tst2", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + }, + } + helper.keeper.SetParams(ctx, ap) + assets = helper.keeper.GetAssetParams(ctx) + require.Equal(t, len(assets), 2) + require.Equal(t, assets[0].AssetCode, "tstusd") + require.Equal(t, assets[1].AssetCode, "tst2usd") + + _, 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, types.GenesisState{}, nil) + header := abci.Header{ + Height: helper.mApp.LastBlockHeight() + 1, + Time: tmtime.Now()} + helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header}) + ctx := helper.mApp.BaseApp.NewContext(false, header) + ap := types.Params{ + Assets: []types.Asset{ + types.Asset{AssetCode: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + }, + } + helper.keeper.SetParams(ctx, ap) + // Set price by oracle 1 + _, err := helper.keeper.SetPrice( + ctx, helper.addrs[0], "tstusd", + sdk.MustNewDecFromStr("0.33"), + header.Time.Add(1*time.Hour)) + require.NoError(t, err) + // Get raw prices + rawPrices := helper.keeper.GetRawPrices(ctx, "tstusd") + 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], "tstusd", + sdk.MustNewDecFromStr("0.35"), + header.Time.Add(time.Hour*1)) + require.NoError(t, err) + + rawPrices = helper.keeper.GetRawPrices(ctx, "tstusd") + 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], "tstusd", + sdk.MustNewDecFromStr("0.37"), + header.Time.Add(time.Hour*1)) + require.NoError(t, err) + rawPrices = helper.keeper.GetRawPrices(ctx, "tstusd") + 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, types.GenesisState{}, nil) + header := abci.Header{ + Height: helper.mApp.LastBlockHeight() + 1, + Time: tmtime.Now()} + helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header}) + ctx := helper.mApp.BaseApp.NewContext(false, header) + ap := types.Params{ + Assets: []types.Asset{ + types.Asset{AssetCode: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + }, + } + helper.keeper.SetParams(ctx, ap) + helper.keeper.SetPrice( + ctx, helper.addrs[0], "tstusd", + sdk.MustNewDecFromStr("0.33"), + header.Time.Add(time.Hour*1)) + helper.keeper.SetPrice( + ctx, helper.addrs[1], "tstusd", + sdk.MustNewDecFromStr("0.35"), + header.Time.Add(time.Hour*1)) + helper.keeper.SetPrice( + ctx, helper.addrs[2], "tstusd", + sdk.MustNewDecFromStr("0.34"), + header.Time.Add(time.Hour*1)) + // Set current price + err := helper.keeper.SetCurrentPrices(ctx, "tstusd") + require.NoError(t, err) + // Get Current price + price := helper.keeper.GetCurrentPrice(ctx, "tstusd") + require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.34")), true) + + // Even number of oracles + helper.keeper.SetPrice( + ctx, helper.addrs[3], "tstusd", + sdk.MustNewDecFromStr("0.36"), + header.Time.Add(time.Hour*1)) + err = helper.keeper.SetCurrentPrices(ctx, "tstusd") + require.NoError(t, err) + price = helper.keeper.GetCurrentPrice(ctx, "tstusd") + require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.345")), true) + +} diff --git a/x/pricefeed/keeper/params.go b/x/pricefeed/keeper/params.go new file mode 100644 index 00000000..576c9398 --- /dev/null +++ b/x/pricefeed/keeper/params.go @@ -0,0 +1,64 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/pricefeed/types" +) + +// GetParams gets params from the store +func (k Keeper) GetParams(ctx sdk.Context) types.Params { + return types.NewParams(k.GetAssetParams(ctx)) +} + +// SetParams updates params in the store +func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { + k.paramstore.SetParamSet(ctx, ¶ms) +} + +// GetAssetParams get asset params from store +func (k Keeper) GetAssetParams(ctx sdk.Context) types.Assets { + var assets types.Assets + k.paramstore.Get(ctx, types.KeyAssets, &assets) + return assets +} + +// GetOracles returns the oracles in the pricefeed store +func (k Keeper) GetOracles(ctx sdk.Context, assetCode string) (types.Oracles, error) { + + for _, a := range k.GetAssetParams(ctx) { + if assetCode == a.AssetCode { + return a.Oracles, nil + } + } + return types.Oracles{}, fmt.Errorf("asset %s not found", assetCode) +} + +// GetOracle returns the oracle from the store or an error if not found +func (k Keeper) GetOracle(ctx sdk.Context, assetCode string, address sdk.AccAddress) (types.Oracle, error) { + oracles, err := k.GetOracles(ctx, assetCode) + if err != nil { + return types.Oracle{}, fmt.Errorf("asset %s not found", assetCode) + } + for _, o := range oracles { + if address.Equals(o.Address) { + return o, nil + } + } + return types.Oracle{}, fmt.Errorf("oracle %s not found for asset %s", address, assetCode) +} + +// GetAsset returns the asset if it is in the pricefeed system +func (k Keeper) GetAsset(ctx sdk.Context, assetCode string) (types.Asset, bool) { + assets := k.GetAssetParams(ctx) + + for i := range assets { + if assets[i].AssetCode == assetCode { + return assets[i], true + } + } + return types.Asset{}, false + +} diff --git a/x/pricefeed/querier.go b/x/pricefeed/keeper/querier.go similarity index 87% rename from x/pricefeed/querier.go rename to x/pricefeed/keeper/querier.go index ecc7936c..245fa754 100644 --- a/x/pricefeed/querier.go +++ b/x/pricefeed/keeper/querier.go @@ -1,21 +1,23 @@ -package pricefeed +package keeper import ( "github.com/cosmos/cosmos-sdk/codec" - abci "github.com/tendermint/tendermint/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/x/pricefeed/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: + case types.QueryCurrentPrice: return queryCurrentPrice(ctx, path[1:], req, keeper) - case QueryRawPrices: + case types.QueryRawPrices: return queryRawPrices(ctx, path[1:], req, keeper) - case QueryAssets: + case types.QueryAssets: return queryAssets(ctx, req, keeper) default: return nil, sdk.ErrUnknownRequest("unknown pricefeed query endpoint") @@ -41,7 +43,7 @@ func queryCurrentPrice(ctx sdk.Context, path []string, req abci.RequestQuery, ke } func queryRawPrices(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { - var priceList QueryRawPricesResp + var priceList types.QueryRawPricesResp assetCode := path[0] _, found := keeper.GetAsset(ctx, assetCode) if !found { @@ -60,8 +62,8 @@ func queryRawPrices(ctx sdk.Context, path []string, req abci.RequestQuery, keepe } func queryAssets(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { - var assetList QueryAssetsResp - assets := keeper.GetAssets(ctx) + var assetList types.QueryAssetsResp + assets := keeper.GetAssetParams(ctx) for _, asset := range assets { assetList = append(assetList, asset.String()) } @@ -71,4 +73,4 @@ func queryAssets(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []b } return bz, nil -} \ No newline at end of file +} diff --git a/x/pricefeed/test_common.go b/x/pricefeed/keeper/test_common.go similarity index 55% rename from x/pricefeed/test_common.go rename to x/pricefeed/keeper/test_common.go index 5fea3df1..2f0e8206 100644 --- a/x/pricefeed/test_common.go +++ b/x/pricefeed/keeper/test_common.go @@ -1,4 +1,4 @@ -package pricefeed +package keeper import ( "testing" @@ -7,8 +7,9 @@ import ( 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" + + "github.com/kava-labs/kava/x/pricefeed/types" ) type testHelper struct { @@ -19,16 +20,12 @@ type testHelper struct { privKeys []crypto.PrivKey } -func getMockApp(t *testing.T, numGenAccs int, genState GenesisState, genAccs []authexported.Account) testHelper { +func getMockApp(t *testing.T, numGenAccs int, genState types.GenesisState, genAccs []authexported.Account) testHelper { mApp := mock.NewApp() - RegisterCodec(mApp.Cdc) - keyPricefeed := sdk.NewKVStoreKey(StoreKey) + types.RegisterCodec(mApp.Cdc) + keyPricefeed := sdk.NewKVStoreKey(types.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)) + keeper := NewKeeper(keyPricefeed, mApp.Cdc, pk.Subspace(types.DefaultParamspace), types.DefaultCodespace) require.NoError(t, mApp.CompleteSetup(keyPricefeed)) @@ -47,11 +44,3 @@ func getMockApp(t *testing.T, numGenAccs int, genState GenesisState, genAccs []a 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{} - } -} diff --git a/x/pricefeed/keeper_test.go b/x/pricefeed/keeper_test.go deleted file mode 100644 index 9ee5f857..00000000 --- a/x/pricefeed/keeper_test.go +++ /dev/null @@ -1,124 +0,0 @@ -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) - -} diff --git a/x/pricefeed/types/asset.go b/x/pricefeed/types/asset.go new file mode 100644 index 00000000..83ba1c0b --- /dev/null +++ b/x/pricefeed/types/asset.go @@ -0,0 +1,98 @@ +package types + +import ( + "fmt" + "strings" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Asset struct that represents an asset in the pricefeed +type Asset struct { + AssetCode string `json:"asset_code" yaml:"asset_code"` + BaseAsset string `json:"base_asset" yaml:"base_asset"` + QuoteAsset string `json:"quote_asset" yaml:"quote_asset"` + Oracles Oracles `json:"oracles" yaml:"oracles"` + Active bool `json:"active" yaml:"active"` +} + +// implement fmt.Stringer +func (a Asset) String() string { + return fmt.Sprintf(`Asset: + Asset Code: %s + Base Asset: %s + Quote Asset: %s + Oracles: %s + Active: %t`, + a.AssetCode, a.BaseAsset, a.QuoteAsset, a.Oracles, a.Active) +} + +// Assets array type for oracle +type Assets []Asset + +// String implements fmt.Stringer +func (as Assets) String() string { + out := "Assets:\n" + for _, a := range as { + out += fmt.Sprintf("%s\n", a.String()) + } + return strings.TrimSpace(out) +} + +// Oracle struct that documents which address an oracle is using +type Oracle struct { + Address sdk.AccAddress `json:"address" yaml:"address"` +} + +// String implements fmt.Stringer +func (o Oracle) String() string { + return fmt.Sprintf(`Address: %s`, o.Address) +} + +// Oracles array type for oracle +type Oracles []Oracle + +// String implements fmt.Stringer +func (os Oracles) String() string { + out := "Oracles:\n" + for _, o := range os { + out += fmt.Sprintf("%s\n", o.String()) + } + return strings.TrimSpace(out) +} + +// 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" yaml:"asset_code"` + Price sdk.Dec `json:"price" yaml:"price"` +} + +// PostedPrice struct represented a price for an asset posted by a specific oracle +type PostedPrice struct { + AssetCode string `json:"asset_code" yaml:"asset_code"` + OracleAddress sdk.AccAddress `json:"oracle_address" yaml:"oracle_address"` + Price sdk.Dec `json:"price" yaml:"price"` + Expiry time.Time `json:"expiry" yaml:"expiry"` +} + +// implement fmt.Stringer +func (cp CurrentPrice) String() string { + return strings.TrimSpace(fmt.Sprintf(`AssetCode: %s +Price: %s`, cp.AssetCode, cp.Price)) +} + +// 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]) } diff --git a/x/pricefeed/types/genesis.go b/x/pricefeed/types/genesis.go index d798e6d8..775143c1 100644 --- a/x/pricefeed/types/genesis.go +++ b/x/pricefeed/types/genesis.go @@ -2,21 +2,18 @@ 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"` + Params Params `json:"asset_params" yaml:"asset_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 { +func NewGenesisState(p Params, pp []PostedPrice) GenesisState { return GenesisState{ - AssetParams: ap, - OracleParams: op, + Params: p, PostedPrices: pp, } } @@ -24,8 +21,7 @@ func NewGenesisState(ap AssetParams, op OracleParams, pp []PostedPrice) GenesisS // DefaultGenesisState defines default GenesisState for pricefeed func DefaultGenesisState() GenesisState { return NewGenesisState( - DefaultAssetParams(), - DefaultOracleParams(), + DefaultParams(), []PostedPrice{}, ) } @@ -45,19 +41,9 @@ func (data GenesisState) IsEmpty() bool { // 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()) - } + if err := data.Params.Validate(); err != nil { + return err } - return nil } diff --git a/x/pricefeed/types/key.go b/x/pricefeed/types/key.go index e39d2d56..4a20c25a 100644 --- a/x/pricefeed/types/key.go +++ b/x/pricefeed/types/key.go @@ -9,4 +9,22 @@ const ( // RouterKey Top level router key RouterKey = ModuleName + + // QuerierRoute is the querier route for gov + QuerierRoute = ModuleName + + // DefaultParamspace default namestore + DefaultParamspace = ModuleName + + // RawPriceFeedPrefix prefix for the raw pricefeed of an asset + RawPriceFeedPrefix = StoreKey + ":raw:" + + // CurrentPricePrefix prefix for the current price of an asset + CurrentPricePrefix = StoreKey + ":currentprice:" + + // AssetPrefix Prefix for the assets in the pricefeed system + AssetPrefix = StoreKey + ":assets" + + // OraclePrefix store prefix for the oracle accounts + OraclePrefix = StoreKey + ":oracles" ) diff --git a/x/pricefeed/types/msgs.go b/x/pricefeed/types/msgs.go index 3aa23ece..eed227bc 100644 --- a/x/pricefeed/types/msgs.go +++ b/x/pricefeed/types/msgs.go @@ -1,6 +1,8 @@ package types import ( + "time" + sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -15,7 +17,7 @@ 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 + Expiry time.Time // expiry time } // NewMsgPostPrice creates a new post price msg @@ -23,7 +25,7 @@ func NewMsgPostPrice( from sdk.AccAddress, assetCode string, price sdk.Dec, - expiry sdk.Int) MsgPostPrice { + expiry time.Time) MsgPostPrice { return MsgPostPrice{ From: from, AssetCode: assetCode, @@ -60,9 +62,6 @@ func (msg MsgPostPrice) ValidateBasic() sdk.Error { 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 } diff --git a/x/pricefeed/types/msgs_test.go b/x/pricefeed/types/msgs_test.go index 73857091..fe139969 100644 --- a/x/pricefeed/types/msgs_test.go +++ b/x/pricefeed/types/msgs_test.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" + tmtime "github.com/tendermint/tendermint/types/time" ) func TestMsgPlaceBid_ValidateBasic(t *testing.T) { @@ -13,8 +14,7 @@ func TestMsgPlaceBid_ValidateBasic(t *testing.T) { // OracleAddress: addr.String(), // }} price, _ := sdk.NewDecFromStr("0.3005") - expiry, _ := sdk.NewIntFromString("10") - negativeExpiry, _ := sdk.NewIntFromString("-3") + expiry := tmtime.Now() negativePrice, _ := sdk.NewDecFromStr("-3.05") tests := []struct { @@ -26,7 +26,6 @@ func TestMsgPlaceBid_ValidateBasic(t *testing.T) { {"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) { diff --git a/x/pricefeed/types/params.go b/x/pricefeed/types/params.go index a3d86768..c6be85a7 100644 --- a/x/pricefeed/types/params.go +++ b/x/pricefeed/types/params.go @@ -8,74 +8,48 @@ import ( 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") + // KeyAssets store key for assets + KeyAssets = []byte("assets") ) // ParamKeyTable Key declaration for parameters func ParamKeyTable() params.KeyTable { - return params.NewKeyTable( - ParamStoreKeyOracles, OracleParams{}, - ParamStoreKeyAssets, AssetParams{}, - ) + return params.NewKeyTable().RegisterParamSet(&Params{}) } -// 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 +// Params params for pricefeed. Can be altered via governance +type Params struct { + Assets []Asset `json:"assets" yaml:"assets"` // Array containing the assets supported by the pricefeed } -// NewAssetParams creates a new AssetParams object -func NewAssetParams(assets []Asset) AssetParams { - return AssetParams{ +// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs +// pairs of pricefeed module's parameters. +func (p Params) ParamSetPairs() params.ParamSetPairs { + return params.ParamSetPairs{ + {Key: KeyAssets, Value: &p.Assets}, + } +} + +// NewParams creates a new AssetParams object +func NewParams(assets []Asset) Params { + return Params{ Assets: assets, } } -// DefaultAssetParams default params for assets -func DefaultAssetParams() AssetParams { - return NewAssetParams([]Asset{}) +// DefaultParams default params for pricefeed +func DefaultParams() Params { + return NewParams(Assets{}) } -// implements fmt.stringer -func (ap AssetParams) String() string { - var assetListString []string - for _, asset := range ap.Assets { - assetListString = append(assetListString, asset.String()) +// String implements fmt.stringer +func (p Params) String() string { + out := "Params:\n" + for _, a := range p.Assets { + out += a.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, ", "))) + return strings.TrimSpace(out) } // ParamSubspace defines the expected Subspace interface for parameters @@ -83,3 +57,14 @@ type ParamSubspace interface { Get(ctx sdk.Context, key []byte, ptr interface{}) Set(ctx sdk.Context, key []byte, param interface{}) } + +// Validate ensure that params have valid values +func (p Params) Validate() error { + // iterate over assets and verify them + for _, asset := range p.Assets { + if asset.AssetCode == "" { + return fmt.Errorf("invalid asset: %s. missing asset code", asset.String()) + } + } + return nil +} diff --git a/x/pricefeed/types/types.go b/x/pricefeed/types/types.go deleted file mode 100644 index e07d9b55..00000000 --- a/x/pricefeed/types/types.go +++ /dev/null @@ -1,67 +0,0 @@ -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]) } From e85d2f880b326b2f79724fc2465d44f8cc0f7a33 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Thu, 28 Nov 2019 10:53:59 -0600 Subject: [PATCH 03/99] wip: cdp params and types --- x/cdp/app_test.go | 18 ++- x/cdp/genesis.go | 2 +- x/cdp/keeper/keeper_test.go | 37 ++--- x/cdp/keeper/test_common.go | 2 +- x/cdp/test_common.go | 2 +- x/cdp/types/expected_keepers.go | 10 +- x/cdp/types/genesis.go | 14 +- x/cdp/types/params.go | 235 ++++++++++++++++++-------------- x/cdp/types/types.go | 64 +++++---- x/pricefeed/types/params.go | 2 +- 10 files changed, 219 insertions(+), 167 deletions(-) diff --git a/x/cdp/app_test.go b/x/cdp/app_test.go index 39737d43..fed5e0f0 100644 --- a/x/cdp/app_test.go +++ b/x/cdp/app_test.go @@ -2,10 +2,12 @@ package cdp import ( "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/mock" abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" "github.com/kava-labs/kava/x/pricefeed" ) @@ -19,7 +21,7 @@ func TestApp_CreateModifyDeleteCDP(t *testing.T) { 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} + header := abci.Header{Height: mapp.LastBlockHeight() + 1, Time: tmtime.Now()} mapp.BeginBlock(abci.RequestBeginBlock{Header: header}) ctx := mapp.BaseApp.NewContext(false, header) params := CdpParams{ @@ -35,15 +37,19 @@ func TestApp_CreateModifyDeleteCDP(t *testing.T) { } keeper.SetParams(ctx, params) keeper.SetGlobalDebt(ctx, sdk.NewInt(0)) - ap := pricefeed.AssetParams{ - Assets: []pricefeed.Asset{pricefeed.Asset{AssetCode: "xrp", Description: ""}}, + ap := pricefeed.Params{ + Assets: []pricefeed.Asset{ + pricefeed.Asset{ + AssetCode: "xrp", BaseAsset: "xrp", + QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, + }, } - pfKeeper.SetAssetParams(ctx, ap) + pfKeeper.SetParams(ctx, ap) pfKeeper.SetPrice( ctx, sdk.AccAddress{}, "xrp", sdk.MustNewDecFromStr("1.00"), - sdk.NewInt(10)) - pfKeeper.SetCurrentPrices(ctx) + header.Time.Add(time.Hour*1)) + pfKeeper.SetCurrentPrices(ctx, "xrp") mapp.EndBlock(abci.RequestEndBlock{}) mapp.Commit() diff --git a/x/cdp/genesis.go b/x/cdp/genesis.go index 5bd639fa..d50be231 100644 --- a/x/cdp/genesis.go +++ b/x/cdp/genesis.go @@ -9,7 +9,7 @@ import ( 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) + ap := pk.GetParams(ctx) for _, a := range ap.Assets { collateralMap[a.AssetCode] = 1 } diff --git a/x/cdp/keeper/keeper_test.go b/x/cdp/keeper/keeper_test.go index 17f03000..df2b3c0b 100644 --- a/x/cdp/keeper/keeper_test.go +++ b/x/cdp/keeper/keeper_test.go @@ -3,6 +3,7 @@ package keeper import ( "fmt" "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" @@ -12,6 +13,7 @@ import ( "github.com/kava-labs/kava/x/pricefeed" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" ) // 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? @@ -103,27 +105,29 @@ func TestKeeper_ModifyCDP(t *testing.T) { } mock.SetGenesis(mapp, []authexported.Account{&genAcc}) // create a new context - header := abci.Header{Height: mapp.LastBlockHeight() + 1} + header := abci.Header{Height: mapp.LastBlockHeight() + 1, Time: tmtime.Now()} mapp.BeginBlock(abci.RequestBeginBlock{Header: header}) ctx := mapp.BaseApp.NewContext(false, header) keeper.SetParams(ctx, defaultParamsSingle()) // setup store state - ap := pricefeed.AssetParams{ + ap := pricefeed.Params{ Assets: []pricefeed.Asset{ - pricefeed.Asset{AssetCode: "xrp", Description: ""}, + pricefeed.Asset{ + AssetCode: "xrp", BaseAsset: "xrp", + QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, }, } - keeper.pricefeedKeeper.SetAssetParams(ctx, ap) + keeper.pricefeedKeeper.SetParams(ctx, ap) _, err := keeper.pricefeedKeeper.SetPrice( ctx, ownerAddr, "xrp", sdk.MustNewDecFromStr(tc.price), - sdk.NewInt(ctx.BlockHeight()+10)) + header.Time.Add(time.Hour*1)) if err != nil { t.Log("test context height", ctx.BlockHeight()) t.Log(err) t.Log(tc.name) } - err = keeper.pricefeedKeeper.SetCurrentPrices(ctx) + err = keeper.pricefeedKeeper.SetCurrentPrices(ctx, "xrp") if err != nil { t.Log("test context height", ctx.BlockHeight()) t.Log(err) @@ -144,9 +148,6 @@ func TestKeeper_ModifyCDP(t *testing.T) { // 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 { @@ -180,21 +181,23 @@ func TestKeeper_PartialSeizeCDP(t *testing.T) { testAddr := addrs[0] mock.SetGenesis(mapp, genAccs) // setup pricefeed - header := abci.Header{Height: mapp.LastBlockHeight() + 1} + header := abci.Header{Height: mapp.LastBlockHeight() + 1, Time: tmtime.Now()} mapp.BeginBlock(abci.RequestBeginBlock{Header: header}) ctx := mapp.BaseApp.NewContext(false, header) keeper.SetParams(ctx, defaultParamsSingle()) - ap := pricefeed.AssetParams{ + ap := pricefeed.Params{ Assets: []pricefeed.Asset{ - pricefeed.Asset{AssetCode: "xrp", Description: ""}, + pricefeed.Asset{ + AssetCode: "xrp", BaseAsset: "xrp", + QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, }, } - keeper.pricefeedKeeper.SetAssetParams(ctx, ap) + keeper.pricefeedKeeper.SetParams(ctx, ap) keeper.pricefeedKeeper.SetPrice( ctx, sdk.AccAddress{}, collateral, sdk.MustNewDecFromStr("1.00"), - i(10)) - keeper.pricefeedKeeper.SetCurrentPrices(ctx) + header.Time.Add(time.Hour*1)) + keeper.pricefeedKeeper.SetCurrentPrices(ctx, collateral) // Create CDP keeper.SetGlobalDebt(ctx, i(0)) err := keeper.ModifyCDP(ctx, testAddr, collateral, i(10), i(5)) @@ -203,8 +206,8 @@ func TestKeeper_PartialSeizeCDP(t *testing.T) { keeper.pricefeedKeeper.SetPrice( ctx, sdk.AccAddress{}, collateral, sdk.MustNewDecFromStr("0.90"), - i(10)) - keeper.pricefeedKeeper.SetCurrentPrices(ctx) + header.Time.Add(time.Hour*1)) + keeper.pricefeedKeeper.SetCurrentPrices(ctx, collateral) // Seize entire CDP err = keeper.PartialSeizeCDP(ctx, testAddr, collateral, i(10), i(5)) diff --git a/x/cdp/keeper/test_common.go b/x/cdp/keeper/test_common.go index 474b069e..59619e51 100644 --- a/x/cdp/keeper/test_common.go +++ b/x/cdp/keeper/test_common.go @@ -30,7 +30,7 @@ func setUpMockAppWithoutGenesis() (*mock.App, Keeper, []sdk.AccAddress, []crypto 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) + priceFeedKeeper := pricefeed.NewKeeper(keyPriceFeed, mapp.Cdc, pk.Subspace(pricefeed.DefaultParamspace), 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) diff --git a/x/cdp/test_common.go b/x/cdp/test_common.go index 54066f56..5d537315 100644 --- a/x/cdp/test_common.go +++ b/x/cdp/test_common.go @@ -28,7 +28,7 @@ func setUpMockAppWithoutGenesis() (*mock.App, Keeper, PricefeedKeeper) { 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) + priceFeedKeeper := pricefeed.NewKeeper(keyPriceFeed, mapp.Cdc, pk.Subspace(pricefeed.DefaultParamspace), 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) diff --git a/x/cdp/types/expected_keepers.go b/x/cdp/types/expected_keepers.go index fe41bed2..41562789 100644 --- a/x/cdp/types/expected_keepers.go +++ b/x/cdp/types/expected_keepers.go @@ -1,6 +1,8 @@ package types import ( + "time" + sdk "github.com/cosmos/cosmos-sdk/types" pftypes "github.com/kava-labs/kava/x/pricefeed/types" ) @@ -14,9 +16,9 @@ type BankKeeper interface { type PricefeedKeeper interface { GetCurrentPrice(sdk.Context, string) pftypes.CurrentPrice - GetAssetParams(sdk.Context) pftypes.AssetParams + GetParams(sdk.Context) pftypes.Params // 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 + SetParams(sdk.Context, pftypes.Params) + SetPrice(sdk.Context, sdk.AccAddress, string, sdk.Dec, time.Time) (pftypes.PostedPrice, sdk.Error) + SetCurrentPrices(sdk.Context, string) sdk.Error } diff --git a/x/cdp/types/genesis.go b/x/cdp/types/genesis.go index 96596e46..390f9696 100644 --- a/x/cdp/types/genesis.go +++ b/x/cdp/types/genesis.go @@ -1,16 +1,11 @@ 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"` + Params Params `json:"params" yaml:"params"` + CDPs CDPs `json:"cdps" yaml:"cdps"` // don't need to setup CollateralStates as they are created as needed } @@ -18,9 +13,8 @@ type GenesisState struct { // TODO make this empty, load test values independent func DefaultGenesisState() GenesisState { return GenesisState{ - Params: DefaultParams(), - GlobalDebt: sdk.ZeroInt(), - CDPs: CDPs{}, + Params: DefaultParams(), + CDPs: CDPs{}, } } diff --git a/x/cdp/types/params.go b/x/cdp/types/params.go index d4afa0dd..8cee7dcd 100644 --- a/x/cdp/types/params.go +++ b/x/cdp/types/params.go @@ -4,135 +4,166 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/params/subspace" + "github.com/cosmos/cosmos-sdk/x/params" ) -/* -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") + KeyGlobalDebtLimit = []byte("GlobalDebtLimit") + KeyCollateralParams = []byte("CollateralParams") + KeyDebtParams = []byte("DebtParams") + DefaultGlobalDebt = sdk.Coins{} + DefaultCircuitBreaker = false + DefaultCollateralParams = CollateralParams{} + DefaultDebtParams = DebtParams{} ) +// Params governance parameters for cdp module +type Params struct { + CollateralParams CollateralParams `json:"collateral_params" yaml:"collateral_params"` + DebtParams DebtParams `json:"debt_params" yaml:"debt_params"` + GlobalDebtLimit sdk.Coins `json:"global_debt_limit" yaml:"global_debt_limit"` + CircuitBreaker bool `json:"circuit_breaker" yaml:"circuit_breaker"` +} + +// String implements fmt.Stringer +func (p Params) String() string { + return fmt.Sprintf(`Params: + Global Debt Limit: %s + Collateral Params: %s + Debt Params: %s + Circuit Breaker: %t`, + p.GlobalDebtLimit, p.CollateralParams, p.DebtParams, p.CircuitBreaker, + ) +} + +// NewParams returns a new params object +func NewParams(debtLimit sdk.Coins, collateralParams CollateralParams, debtParams DebtParams, breaker bool) Params { + return Params{ + GlobalDebtLimit: debtLimit, + CollateralParams: collateralParams, + DebtParams: debtParams, + CircuitBreaker: breaker, + } +} + +// DefaultParams returns default params for cdp module +func DefaultParams() Params { + return NewParams(DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParams, DefaultCircuitBreaker) +} + +// CollateralParam governance parameters for each collateral type within the cdp module +type CollateralParam struct { + Denom string `json:"denom" yaml:"denom"` // Coin name of collateral type + LiquidationRatio sdk.Dec `json:"liquidation_ratio" yaml:"liquidation_ratio"` // The ratio (Collateral (priced in stable coin) / Debt) under which a CDP will be liquidated + DebtLimit sdk.Coins `json:"debt_limit" yaml:"debt_limit"` // Maximum amount of debt allowed to be drawn from this collateral type + //DebtFloor sdk.Int // used to prevent dust +} + +// String implements fmt.Stringer +func (cp CollateralParam) String() string { + return fmt.Sprintf(`Collateral: + Denom: %s + LiquidationRatio: %s + DebtLimit: %s`, cp.Denom, cp.LiquidationRatio, cp.DebtLimit) +} + +// CollateralParams array of CollateralParam +type CollateralParams []CollateralParam + +// String implements fmt.Stringer +func (cps CollateralParams) String() string { + out := "Collateral Params\n" + for _, cp := range cps { + out += fmt.Sprintf("%s\n", cp) + } + return out +} + +// DebtParam governance params for debt assets +type DebtParam struct { + Denom string `json:"denom" yaml:"denom"` + ReferenceAsset string `json:"reference_asset" yaml:"reference_asset"` + DebtLimit sdk.Coins `json:"debt_limit" yaml:"debt_limit"` +} + +func (dp DebtParam) String() string { + return fmt.Sprintf(`Debt: + Denom: %s + ReferenceAsset: %s + DebtLimit: %s`, dp.Denom, dp.ReferenceAsset, dp.DebtLimit) +} + +// DebtParams array of DebtParam +type DebtParams []DebtParam + +// String implements fmt.Stringer +func (dps DebtParams) String() string { + out := "Debt Params\n" + for _, dp := range dps { + out += fmt.Sprintf("%s\n", dp) + } + return out +} + // ParamKeyTable Key declaration for parameters -func ParamKeyTable() subspace.KeyTable { - return subspace.NewKeyTable().RegisterParamSet(&CdpParams{}) +func ParamKeyTable() params.KeyTable { + return params.NewKeyTable().RegisterParamSet(&Params{}) } // 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{ +func (p *Params) ParamSetPairs() params.ParamSetPairs { + return params.ParamSetPairs{ {KeyGlobalDebtLimit, &p.GlobalDebtLimit}, {KeyCollateralParams, &p.CollateralParams}, - {KeyStableDenoms, &p.StableDenoms}, + {KeyDebtParams, &p.DebtParams}, } } -// 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 { +func (p Params) Validate() error { + debtDenoms := make(map[string]int) + debtParamsDebtLimit := sdk.Coins{} + for _, dp := range p.DebtParams { + _, found := debtDenoms[dp.Denom] + if found { + return fmt.Errorf("duplicate debt denom: %s", dp.Denom) + } + debtDenoms[dp.Denom] = 1 + if dp.DebtLimit.IsAnyNegative() { + return fmt.Errorf("debt limit for all debt tokens should be positive, is %s for %s", dp.DebtLimit, dp.Denom) + } + debtParamsDebtLimit = debtParamsDebtLimit.Add(dp.DebtLimit) + } + if debtParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) { + fmt.Errorf("debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tdebt limits: %s", + p.GlobalDebtLimit, debtParamsDebtLimit) + } + collateralDupMap := make(map[string]int) - denomDupMap := make(map[string]int) - for _, collateral := range p.CollateralParams { - _, found := collateralDupMap[collateral.Denom] + collateralParamsDebtLimit := sdk.Coins{} + for _, cp := range p.CollateralParams { + _, found := collateralDupMap[cp.Denom] if found { - return fmt.Errorf("duplicate denom: %s", collateral.Denom) + return fmt.Errorf("duplicate collateral denom: %s", cp.Denom) } - collateralDupMap[collateral.Denom] = 1 + collateralDupMap[cp.Denom] = 1 - if collateral.DebtLimit.IsNegative() { - return fmt.Errorf("debt limit should be positive, is %s for %s", collateral.DebtLimit, collateral.Denom) + if cp.DebtLimit.IsAnyNegative() { + return fmt.Errorf("debt limit for all collaterals should be positive, is %s for %s", cp.DebtLimit, cp.Denom) } - - // TODO do we want to enforce overcollateralization at this level? -- probably not, as it's technically a governance thing (kevin) + collateralParamsDebtLimit = collateralParamsDebtLimit.Add(cp.DebtLimit) } - if p.GlobalDebtLimit.IsNegative() { - return fmt.Errorf("global debt limit should be positive, is %s", p.GlobalDebtLimit) + if collateralParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) { + fmt.Errorf("collateral debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tcollateral debt limits: %s", + p.GlobalDebtLimit, collateralParamsDebtLimit) } - for _, denom := range p.StableDenoms { - _, found := denomDupMap[denom] - if found { - return fmt.Errorf("duplicate stable denom: %s", denom) - } - denomDupMap[denom] = 1 + if p.GlobalDebtLimit.IsAnyNegative() { + return fmt.Errorf("global debt limit should be positive for all debt tokens, is %s", p.GlobalDebtLimit) } return nil } - -func DefaultParams() CdpParams { - return CdpParams{ - GlobalDebtLimit: sdk.NewInt(0), - CollateralParams: []CollateralParams{}, - StableDenoms: []string{"usdx"}, - } -} diff --git a/x/cdp/types/types.go b/x/cdp/types/types.go index 02051ed4..c4b96cbc 100644 --- a/x/cdp/types/types.go +++ b/x/cdp/types/types.go @@ -3,6 +3,7 @@ package types import ( "fmt" "strings" + "time" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -15,27 +16,41 @@ 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 + CollateralAmount sdk.Coins `json:"collateral_amount" yaml:"collateral_amount"` // Amount of collateral stored in this CDP + Debt sdk.Coins `json:"debt" yaml:"debt"` + AccumulatedFees sdk.Coins `json:"accumulated_fees" yaml:"accumulated_fees"` + FeesUpdated time.Time `json:"fees_updated" yaml:"fees_updated"` // Amount of stable coin drawn from this CDP } +// IsUnderCollateralized checks if cdp is below the liquidation ratio 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)) + collateralValue := sdk.NewDecFromInt(cdp.CollateralAmount.AmountOf(cdp.CollateralDenom)).Mul(price) + minCollateralValue := sdk.NewDec(0) + for _, c := range cdp.Debt { + minCollateralValue = minCollateralValue.Add(liquidationRatio.Mul(c.Amount.ToDec())) + } return collateralValue.LT(minCollateralValue) // TODO LT or LTE? } +// String implements fmt.stringer func (cdp CDP) String() string { return strings.TrimSpace(fmt.Sprintf(`CDP: Owner: %s - Collateral: %s - Debt: %s`, + Collateral Type: %s + Collateral: %s + Debt: %s + Fees: %s + Fees Last Updated: %s`, cdp.Owner, - sdk.NewCoin(cdp.CollateralDenom, cdp.CollateralAmount), - sdk.NewCoin("usdx", cdp.Debt), + cdp.CollateralDenom, + cdp.CollateralAmount, + cdp.Debt, + cdp.AccumulatedFees, + cdp.FeesUpdated, )) } +// CDPs array of CDP type CDPs []CDP // String implements stringer @@ -52,22 +67,23 @@ 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) -} + +// 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 { diff --git a/x/pricefeed/types/params.go b/x/pricefeed/types/params.go index c6be85a7..49d68716 100644 --- a/x/pricefeed/types/params.go +++ b/x/pricefeed/types/params.go @@ -5,7 +5,7 @@ import ( "strings" sdk "github.com/cosmos/cosmos-sdk/types" - params "github.com/cosmos/cosmos-sdk/x/params/subspace" + "github.com/cosmos/cosmos-sdk/x/params" ) var ( From 1a9b8514c925d48f0c980818b177cbe47e3bbca2 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Thu, 28 Nov 2019 11:00:08 -0600 Subject: [PATCH 04/99] fix keys, return validation errors --- x/cdp/types/params.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/x/cdp/types/params.go b/x/cdp/types/params.go index 8cee7dcd..beed781f 100644 --- a/x/cdp/types/params.go +++ b/x/cdp/types/params.go @@ -13,6 +13,7 @@ var ( KeyGlobalDebtLimit = []byte("GlobalDebtLimit") KeyCollateralParams = []byte("CollateralParams") KeyDebtParams = []byte("DebtParams") + KeyCircuitBreaker = []byte("CircuitBreaker") DefaultGlobalDebt = sdk.Coins{} DefaultCircuitBreaker = false DefaultCollateralParams = CollateralParams{} @@ -117,9 +118,10 @@ func ParamKeyTable() params.KeyTable { // nolint func (p *Params) ParamSetPairs() params.ParamSetPairs { return params.ParamSetPairs{ - {KeyGlobalDebtLimit, &p.GlobalDebtLimit}, - {KeyCollateralParams, &p.CollateralParams}, - {KeyDebtParams, &p.DebtParams}, + {Key: KeyGlobalDebtLimit, Value: &p.GlobalDebtLimit}, + {Key: KeyCollateralParams, Value: &p.CollateralParams}, + {Key: KeyDebtParams, Value: &p.DebtParams}, + {Key: KeyCircuitBreaker, Value: &p.CircuitBreaker}, } } @@ -139,7 +141,7 @@ func (p Params) Validate() error { debtParamsDebtLimit = debtParamsDebtLimit.Add(dp.DebtLimit) } if debtParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) { - fmt.Errorf("debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tdebt limits: %s", + return fmt.Errorf("debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tdebt limits: %s", p.GlobalDebtLimit, debtParamsDebtLimit) } @@ -158,7 +160,7 @@ func (p Params) Validate() error { collateralParamsDebtLimit = collateralParamsDebtLimit.Add(cp.DebtLimit) } if collateralParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) { - fmt.Errorf("collateral debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tcollateral debt limits: %s", + return fmt.Errorf("collateral debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tcollateral debt limits: %s", p.GlobalDebtLimit, collateralParamsDebtLimit) } From fc2b7f96521eb6c46ed9d4282cd6080a5a76c6c7 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 29 Nov 2019 10:55:10 +0000 Subject: [PATCH 05/99] add new modules to app --- app/app.go | 69 ++++++++++++++++++++++++++++++++++-------- x/pricefeed/genesis.go | 3 +- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/app/app.go b/app/app.go index be60f667..c3398d5b 100644 --- a/app/app.go +++ b/app/app.go @@ -4,6 +4,10 @@ import ( "io" "os" + "github.com/kava-labs/kava/x/auction" + "github.com/kava-labs/kava/x/cdp" + "github.com/kava-labs/kava/x/liquidator" + "github.com/kava-labs/kava/x/pricefeed" validatorvesting "github.com/kava-labs/kava/x/validator-vesting" abci "github.com/tendermint/tendermint/abci/types" @@ -55,6 +59,10 @@ var ( crisis.AppModuleBasic{}, slashing.AppModuleBasic{}, supply.AppModuleBasic{}, + auction.AppModuleBasic{}, + cdp.AppModuleBasic{}, + liquidator.AppModuleBasic{}, + pricefeed.AppModuleBasic{}, ) // module account permissions @@ -81,17 +89,21 @@ type App struct { tkeys map[string]*sdk.TransientStoreKey // keepers from all the modules - accountKeeper auth.AccountKeeper - bankKeeper bank.Keeper - supplyKeeper supply.Keeper - stakingKeeper staking.Keeper - slashingKeeper slashing.Keeper - mintKeeper mint.Keeper - distrKeeper distr.Keeper - govKeeper gov.Keeper - crisisKeeper crisis.Keeper - paramsKeeper params.Keeper - vvKeeper validatorvesting.Keeper + accountKeeper auth.AccountKeeper + bankKeeper bank.Keeper + supplyKeeper supply.Keeper + stakingKeeper staking.Keeper + slashingKeeper slashing.Keeper + mintKeeper mint.Keeper + distrKeeper distr.Keeper + govKeeper gov.Keeper + crisisKeeper crisis.Keeper + paramsKeeper params.Keeper + vvKeeper validatorvesting.Keeper + auctionKeeper auction.Keeper + cdpKeeper cdp.Keeper + liquidatorKeeper liquidator.Keeper + pricefeedKeeper pricefeed.Keeper // the module manager mm *module.Manager @@ -115,6 +127,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, bam.MainStoreKey, auth.StoreKey, staking.StoreKey, supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey, gov.StoreKey, params.StoreKey, validatorvesting.StoreKey, + auction.StoreKey, cdp.StoreKey, liquidator.StoreKey, pricefeed.StoreKey, ) tkeys := sdk.NewTransientStoreKeys(params.TStoreKey) @@ -136,6 +149,10 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, slashingSubspace := app.paramsKeeper.Subspace(slashing.DefaultParamspace) govSubspace := app.paramsKeeper.Subspace(gov.DefaultParamspace).WithKeyTable(gov.ParamKeyTable()) crisisSubspace := app.paramsKeeper.Subspace(crisis.DefaultParamspace) + auctionSubspace := app.paramsKeeper.Subspace(auction.DefaultParamspace) + cdpSubspace := app.paramsKeeper.Subspace(cdp.DefaultParamspace) + liquidatorSubspace := app.paramsKeeper.Subspace(liquidator.DefaultParamspace) + pricefeedSubspace := app.paramsKeeper.Subspace(pricefeed.DefaultParamspace).WithKeyTable(pricefeed.ParamKeyTable()) // TODO why do other modules not need this? // add keepers app.accountKeeper = auth.NewAccountKeeper( @@ -207,6 +224,29 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, app.bankKeeper, app.supplyKeeper, &stakingKeeper) + app.pricefeedKeeper = pricefeed.NewKeeper( + app.cdc, + keys[pricefeed.StoreKey], + pricefeedSubspace, + pricefeed.DefaultCodespace) + app.cdpKeeper = cdp.NewKeeper( + app.cdc, + keys[cdp.StoreKey], + cdpSubspace, + app.pricefeedKeeper, + app.bankKeeper) + app.auctionKeeper = auction.NewKeeper( + app.cdc, + app.cdpKeeper, // CDP keeper standing in for bank + keys[auction.StoreKey], + auctionSubspace) + app.liquidatorKeeper = liquidator.NewKeeper( + app.cdc, + keys[liquidator.StoreKey], + liquidatorSubspace, + app.cdpKeeper, + app.auctionKeeper, + app.cdpKeeper) // CDP keeper standing in for bank // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks @@ -227,6 +267,10 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, slashing.NewAppModule(app.slashingKeeper, app.stakingKeeper), staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper), validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper), + auction.NewAppModule(app.auctionKeeper), + cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper), + liquidator.NewAppModule(app.liquidatorKeeper), + pricefeed.NewAppModule(app.pricefeedKeeper), ) // During begin block slashing happens after distr.BeginBlocker so that @@ -234,7 +278,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, // CanWithdrawInvariant invariant. app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName) - app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName) + app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName, auction.ModuleName) // TODO is this correct order? // Note: genutils must occur after staking so that pools are properly // initialized with tokens from genesis accounts. @@ -245,6 +289,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, auth.ModuleName, validatorvesting.ModuleName, distr.ModuleName, staking.ModuleName, bank.ModuleName, slashing.ModuleName, gov.ModuleName, mint.ModuleName, supply.ModuleName, crisis.ModuleName, genutil.ModuleName, + pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, liquidator.ModuleName, // TODO is this order ok? ) app.mm.RegisterInvariants(&app.crisisKeeper) diff --git a/x/pricefeed/genesis.go b/x/pricefeed/genesis.go index 2dafc2b3..ed19d3e6 100644 --- a/x/pricefeed/genesis.go +++ b/x/pricefeed/genesis.go @@ -4,13 +4,12 @@ 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) + keeper.SetOracleParams(ctx, data.OracleParams) // Iterate through the posted prices and set them in the store for _, pp := range data.PostedPrices { From e83127a15e38981a1d7e6100223f4630275d20e9 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 3 Dec 2019 14:32:01 +0000 Subject: [PATCH 06/99] standardise pricefeed NewKeeper --- x/liquidator/keeper/test_common.go | 2 +- x/pricefeed/keeper.go | 2 +- x/pricefeed/test_common.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x/liquidator/keeper/test_common.go b/x/liquidator/keeper/test_common.go index 62751c9f..1f5550d4 100644 --- a/x/liquidator/keeper/test_common.go +++ b/x/liquidator/keeper/test_common.go @@ -75,7 +75,7 @@ func setupTestKeepers() (sdk.Context, keepers) { bank.DefaultCodespace, blacklistedAddrs, ) - pricefeedKeeper := pricefeed.NewKeeper(keyPriceFeed, cdc, paramsKeeper.Subspace(pricefeed.DefaultParamspace).WithKeyTable(pricefeed.ParamKeyTable()), pricefeed.DefaultCodespace) + pricefeedKeeper := pricefeed.NewKeeper(cdc, keyPriceFeed, paramsKeeper.Subspace(pricefeed.DefaultParamspace).WithKeyTable(pricefeed.ParamKeyTable()), pricefeed.DefaultCodespace) cdpKeeper := cdp.NewKeeper( cdc, keyCDP, diff --git a/x/pricefeed/keeper.go b/x/pricefeed/keeper.go index 40473812..092cb168 100644 --- a/x/pricefeed/keeper.go +++ b/x/pricefeed/keeper.go @@ -44,7 +44,7 @@ type Keeper struct { // - adding oracles // - adding/removing assets from the pricefeed func NewKeeper( - storeKey sdk.StoreKey, cdc *codec.Codec, paramSpace ParamSubspace, codespace sdk.CodespaceType, + cdc *codec.Codec, storeKey sdk.StoreKey, paramSpace ParamSubspace, codespace sdk.CodespaceType, ) Keeper { return Keeper{ paramSpace: paramSpace, diff --git a/x/pricefeed/test_common.go b/x/pricefeed/test_common.go index 5fea3df1..7808d01b 100644 --- a/x/pricefeed/test_common.go +++ b/x/pricefeed/test_common.go @@ -24,7 +24,7 @@ func getMockApp(t *testing.T, numGenAccs int, genState GenesisState, genAccs []a RegisterCodec(mApp.Cdc) keyPricefeed := sdk.NewKVStoreKey(StoreKey) pk := mApp.ParamsKeeper - keeper := NewKeeper(keyPricefeed, mApp.Cdc, pk.Subspace(DefaultParamspace).WithKeyTable(ParamKeyTable()), DefaultCodespace) + keeper := NewKeeper(mApp.Cdc, keyPricefeed, pk.Subspace(DefaultParamspace).WithKeyTable(ParamKeyTable()), DefaultCodespace) // Register routes mApp.Router().AddRoute(RouterKey, NewHandler(keeper)) From 6f3cb815a29edecc69de3fffbf29ef7385d7ed44 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 3 Dec 2019 14:33:20 +0000 Subject: [PATCH 07/99] add TestApp --- app/test_common.go | 129 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 app/test_common.go diff --git a/app/test_common.go b/app/test_common.go new file mode 100644 index 00000000..75c163b9 --- /dev/null +++ b/app/test_common.go @@ -0,0 +1,129 @@ +package app + +import ( + "math/rand" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/secp256k1" + "github.com/tendermint/tendermint/libs/log" + tmdb "github.com/tendermint/tm-db" + + "github.com/cosmos/cosmos-sdk/codec" + 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/bank" + "github.com/cosmos/cosmos-sdk/x/crisis" + "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/mint" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/cosmos/cosmos-sdk/x/slashing" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/supply" + + "github.com/kava-labs/kava/x/auction" + "github.com/kava-labs/kava/x/cdp" + "github.com/kava-labs/kava/x/liquidator" + "github.com/kava-labs/kava/x/pricefeed" + validatorvesting "github.com/kava-labs/kava/x/validator-vesting" +) + +// TestApp is a simple wrapper around an App. It exposes internal keepers for use in integration tests. +// This file also contains test helpers. Ideally they would be in separate package. +// Basic Usage: +// Create a test app with NewTestApp, then all keepers and their methods can be accessed for test setup and execution. +// Advanced Usage: +// Some tests call for an app to be initialized with some state. This can be achieved through keeper method calls (ie keeper.SetParams(...)). +// However this leads to a lot of duplicated logic similar to InitGenesis methods. +// So TestApp.InitializeFromGenesisStates() will call InitGenesis with the default genesis state. +//and TestApp.InitializeFromGenesisStates(authState, cdpState) will do the same but overwrite the auth and cdp sections of the default genesis state +// Creating the genesis states can be combersome, but helper methods can make it easier such as NewAuthGenStateFromAccounts below. +type TestApp struct { + App +} + +func NewTestApp() TestApp { + db := tmdb.NewMemDB() + app := NewApp(log.NewNopLogger(), db, nil, true, 0) + return TestApp{App: *app} +} + +func (tApp TestApp) GetAccountKeeper() auth.AccountKeeper { return tApp.accountKeeper } +func (tApp TestApp) GetBankKeeper() bank.Keeper { return tApp.bankKeeper } +func (tApp TestApp) GetSupplyKeeper() supply.Keeper { return tApp.supplyKeeper } +func (tApp TestApp) GetStakingKeeper() staking.Keeper { return tApp.stakingKeeper } +func (tApp TestApp) GetSlashingKeeper() slashing.Keeper { return tApp.slashingKeeper } +func (tApp TestApp) GetMintKeeper() mint.Keeper { return tApp.mintKeeper } +func (tApp TestApp) GetDistrKeeper() distribution.Keeper { return tApp.distrKeeper } +func (tApp TestApp) GetGovKeeper() gov.Keeper { return tApp.govKeeper } +func (tApp TestApp) GetCrisisKeeper() crisis.Keeper { return tApp.crisisKeeper } +func (tApp TestApp) GetParamsKeeper() params.Keeper { return tApp.paramsKeeper } +func (tApp TestApp) GetVVKeeper() validatorvesting.Keeper { return tApp.vvKeeper } +func (tApp TestApp) GetAuctionKeeper() auction.Keeper { return tApp.auctionKeeper } +func (tApp TestApp) GetCDPKeeper() cdp.Keeper { return tApp.cdpKeeper } +func (tApp TestApp) GetLiquidatorKeeper() liquidator.Keeper { return tApp.liquidatorKeeper } +func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefeedKeeper } + +// Create a new auth genesis state from some addresses and coins. The state is returned marshalled into a map. +// TODO this is only a TestApp method because it needs access to the codec, could be another way to structure it +func (tApp TestApp) NewAuthGenStateFromAccounts(addresses []sdk.AccAddress, coins []sdk.Coins) GenesisState { + // Create GenAccounts + accounts := authexported.GenesisAccounts{} + for i := range addresses { + accounts = append(accounts, auth.NewBaseAccount(addresses[i], coins[i], nil, 0, 0)) + } + // Create the auth genesis state + authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts) + return GenesisState{auth.ModuleName: tApp.cdc.MustMarshalJSON(authGenesis)} +} + +// This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states +func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp { + // Create a default genesis state and overwrite with provided values + genesisState := NewDefaultGenesisState() + for _, state := range genesisStates { + for k, v := range state { + genesisState[k] = v + } + } + + // Initialize the chain + stateBytes, err := codec.MarshalJSONIndent(tApp.cdc, genesisState) + if err != nil { + panic(err) + } + tApp.InitChain( + abci.RequestInitChain{ + Validators: []abci.ValidatorUpdate{}, + AppStateBytes: stateBytes, + }, + ) + tApp.Commit() + tApp.BeginBlock(abci.RequestBeginBlock{Header: abci.Header{Height: tApp.LastBlockHeight() + 1}}) + return tApp +} + +// GeneratePrivKeyAddressPairsFromRand generates (deterministically) a total of n private keys and addresses. +// TODO only generate secp256 keys? +func GeneratePrivKeyAddressPairs(n int) (keys []crypto.PrivKey, addrs []sdk.AccAddress) { + r := rand.New(rand.NewSource(12345)) // make the generation deterministic + keys = make([]crypto.PrivKey, n) + addrs = make([]sdk.AccAddress, n) + for i := 0; i < n; i++ { + secret := make([]byte, 32) + _, err := r.Read(secret) + if err != nil { + panic("Could not read randomness") + } + if r.Int63()%2 == 0 { + keys[i] = secp256k1.GenPrivKeySecp256k1(secret) + } else { + keys[i] = ed25519.GenPrivKeyFromSecret(secret) + } + addrs[i] = sdk.AccAddress(keys[i].PubKey().Address()) + } + return +} From ce2b2e22130be8bef8fd307f30d252a220dd7214 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 3 Dec 2019 14:35:27 +0000 Subject: [PATCH 08/99] update cdp keeper tests --- app/test_common.go | 13 ++-- x/cdp/keeper/bank_test.go | 40 ++++------ x/cdp/keeper/integration_test.go | 46 ++++++++++++ x/cdp/keeper/keeper.go | 18 ++--- x/cdp/keeper/keeper_test.go | 122 +++++++++++++++---------------- x/cdp/keeper/test_common.go | 88 ---------------------- 6 files changed, 136 insertions(+), 191 deletions(-) create mode 100644 x/cdp/keeper/integration_test.go delete mode 100644 x/cdp/keeper/test_common.go diff --git a/app/test_common.go b/app/test_common.go index 75c163b9..6980260b 100644 --- a/app/test_common.go +++ b/app/test_common.go @@ -34,13 +34,14 @@ import ( // TestApp is a simple wrapper around an App. It exposes internal keepers for use in integration tests. // This file also contains test helpers. Ideally they would be in separate package. // Basic Usage: -// Create a test app with NewTestApp, then all keepers and their methods can be accessed for test setup and execution. +// Create a test app with NewTestApp, then all keepers and their methods can be accessed for test setup and execution. // Advanced Usage: -// Some tests call for an app to be initialized with some state. This can be achieved through keeper method calls (ie keeper.SetParams(...)). -// However this leads to a lot of duplicated logic similar to InitGenesis methods. -// So TestApp.InitializeFromGenesisStates() will call InitGenesis with the default genesis state. -//and TestApp.InitializeFromGenesisStates(authState, cdpState) will do the same but overwrite the auth and cdp sections of the default genesis state -// Creating the genesis states can be combersome, but helper methods can make it easier such as NewAuthGenStateFromAccounts below. +// Some tests call for an app to be initialized with some state. This can be achieved through keeper method calls (ie keeper.SetParams(...)). +// However this leads to a lot of duplicated logic similar to InitGenesis methods. +// So TestApp.InitializeFromGenesisStates() will call InitGenesis with the default genesis state. +// and TestApp.InitializeFromGenesisStates(authState, cdpState) will do the same but overwrite the auth and cdp sections of the default genesis state +// Creating the genesis states can be combersome, but helper methods can make it easier such as NewAuthGenStateFromAccounts below. +// type TestApp struct { App } diff --git a/x/cdp/keeper/bank_test.go b/x/cdp/keeper/bank_test.go index ddac5479..4ddde8e2 100644 --- a/x/cdp/keeper/bank_test.go +++ b/x/cdp/keeper/bank_test.go @@ -1,19 +1,19 @@ -package keeper +package keeper_test 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" + + "github.com/kava-labs/kava/app" + keep "github.com/kava-labs/kava/x/cdp/keeper" ) // Test the bank functionality of the CDP keeper func TestKeeper_AddSubtractGetCoins(t *testing.T) { - _, addrs := mock.GeneratePrivKeyAddressPairs(1) + _, addrs := app.GeneratePrivKeyAddressPairs(1) normalAddr := addrs[0] tests := []struct { @@ -25,28 +25,23 @@ func TestKeeper_AddSubtractGetCoins(t *testing.T) { }{ {"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 + {"addLiquidatorStable", keep.LiquidatorAccountAddress, true, cs(c("usdx", 53)), cs(c("usdx", 153))}, + {"subLiquidatorStable", keep.LiquidatorAccountAddress, false, cs(c("usdx", 53)), cs(c("usdx", 47))}, + {"addLiquidatorGov", keep.LiquidatorAccountAddress, true, cs(c("kava", 53)), cs(c("usdx", 100))}, // no change to balance + {"subLiquidatorGov", keep.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}) + // setup app with an account + tApp := app.NewTestApp() + authGenState := tApp.NewAuthGenStateFromAccounts([]sdk.AccAddress{normalAddr}, []sdk.Coins{cs(c("usdx", 100), c("kava", 100))}) + tApp.InitializeFromGenesisStates(authGenState) // 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 + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetCDPKeeper() + keeper.SetLiquidatorModuleAccount(ctx, keep.LiquidatorModuleAccount{cs(c("usdx", 100))}) // set gov coin "balance" to zero // perform the test action var err sdk.Error @@ -56,9 +51,6 @@ func TestKeeper_AddSubtractGetCoins(t *testing.T) { _, 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)) diff --git a/x/cdp/keeper/integration_test.go b/x/cdp/keeper/integration_test.go new file mode 100644 index 00000000..0a1d6fc6 --- /dev/null +++ b/x/cdp/keeper/integration_test.go @@ -0,0 +1,46 @@ +package keeper_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/cdp/types" +) + +// 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"}, + } +} diff --git a/x/cdp/keeper/keeper.go b/x/cdp/keeper/keeper.go index ee9f2818..83774e52 100644 --- a/x/cdp/keeper/keeper.go +++ b/x/cdp/keeper/keeper.go @@ -126,13 +126,13 @@ func (k Keeper) ModifyCDP(ctx sdk.Context, owner sdk.AccAddress, collateralDenom } // Set CDP if cdp.CollateralAmount.IsZero() && cdp.Debt.IsZero() { // TODO maybe abstract this logic into SetCDP - k.deleteCDP(ctx, cdp) + k.DeleteCDP(ctx, cdp) } else { k.SetCDP(ctx, cdp) } // set total debts k.SetGlobalDebt(ctx, gDebt) - k.setCollateralState(ctx, collateralState) + k.SetCollateralState(ctx, collateralState) return nil } @@ -195,11 +195,11 @@ func (k Keeper) PartialSeizeCDP(ctx sdk.Context, owner sdk.AccAddress, collatera // Store updated state if cdp.CollateralAmount.IsZero() && cdp.Debt.IsZero() { // TODO maybe abstract this logic into SetCDP - k.deleteCDP(ctx, cdp) + k.DeleteCDP(ctx, cdp) } else { k.SetCDP(ctx, cdp) } - k.setCollateralState(ctx, collateralState) + k.SetCollateralState(ctx, collateralState) return nil } @@ -264,7 +264,7 @@ func (k Keeper) SetCDP(ctx sdk.Context, cdp types.CDP) { 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? +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 @@ -354,7 +354,7 @@ func (k Keeper) GetCollateralState(ctx sdk.Context, collateralDenom string) (typ k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &collateralState) return collateralState, true } -func (k Keeper) setCollateralState(ctx sdk.Context, collateralstate types.CollateralState) { +func (k Keeper) SetCollateralState(ctx sdk.Context, collateralstate types.CollateralState) { // get store store := ctx.KVStore(k.key) // marshal and set @@ -422,7 +422,7 @@ func (k Keeper) AddCoins(ctx sdk.Context, address sdk.AccAddress, amount sdk.Coi return amount, sdk.ErrInsufficientCoins(fmt.Sprintf("insufficient account funds; %s < %s", lma.Coins, amount)) } lma.Coins = updatedCoins - k.setLiquidatorModuleAccount(ctx, lma) + k.SetLiquidatorModuleAccount(ctx, lma) return updatedCoins, nil } else { return k.bankKeeper.AddCoins(ctx, address, amount) @@ -445,7 +445,7 @@ func (k Keeper) SubtractCoins(ctx sdk.Context, address sdk.AccAddress, amount sd return amount, sdk.ErrInsufficientCoins(fmt.Sprintf("insufficient account funds; %s < %s", lma.Coins, amount)) } lma.Coins = updatedCoins - k.setLiquidatorModuleAccount(ctx, lma) + k.SetLiquidatorModuleAccount(ctx, lma) return updatedCoins, nil } else { return k.bankKeeper.SubtractCoins(ctx, address, amount) @@ -483,7 +483,7 @@ func (k Keeper) getLiquidatorModuleAccount(ctx sdk.Context) LiquidatorModuleAcco k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &lma) return lma } -func (k Keeper) setLiquidatorModuleAccount(ctx sdk.Context, lma LiquidatorModuleAccount) { +func (k Keeper) SetLiquidatorModuleAccount(ctx sdk.Context, lma LiquidatorModuleAccount) { store := ctx.KVStore(k.key) bz := k.cdc.MustMarshalBinaryLengthPrefixed(lma) store.Set(liquidatorAccountKey, bz) diff --git a/x/cdp/keeper/keeper_test.go b/x/cdp/keeper/keeper_test.go index 17f03000..184623cd 100644 --- a/x/cdp/keeper/keeper_test.go +++ b/x/cdp/keeper/keeper_test.go @@ -1,27 +1,26 @@ -package keeper +package keeper_test 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" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/cdp/types" + "github.com/kava-labs/kava/x/pricefeed" ) // 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) + _, addrs := app.GeneratePrivKeyAddressPairs(1) ownerAddr := addrs[0] - type state struct { // TODO this allows invalid state to be set up, should it? + type state struct { CDP types.CDP OwnerCoins sdk.Coins GlobalDebt sdk.Int @@ -95,26 +94,24 @@ func TestKeeper_ModifyCDP(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // setup keeper - mapp, keeper, _, _ := setUpMockAppWithoutGenesis() + tApp := app.NewTestApp() // 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()) + authGen := tApp.NewAuthGenStateFromAccounts([]sdk.AccAddress{ownerAddr}, []sdk.Coins{tc.priorState.OwnerCoins}) + tApp.InitializeFromGenesisStates(authGen) + // create a context for db access + ctx := tApp.NewContext(false, abci.Header{}) + // setup store state + keeper := tApp.GetCDPKeeper() + keeper.SetParams(ctx, defaultParamsSingle()) + pricefeedKeeper := tApp.GetPriceFeedKeeper() ap := pricefeed.AssetParams{ Assets: []pricefeed.Asset{ pricefeed.Asset{AssetCode: "xrp", Description: ""}, }, } - keeper.pricefeedKeeper.SetAssetParams(ctx, ap) - _, err := keeper.pricefeedKeeper.SetPrice( + pricefeedKeeper.SetAssetParams(ctx, ap) + _, err := pricefeedKeeper.SetPrice( ctx, ownerAddr, "xrp", sdk.MustNewDecFromStr(tc.price), sdk.NewInt(ctx.BlockHeight()+10)) @@ -123,7 +120,7 @@ func TestKeeper_ModifyCDP(t *testing.T) { t.Log(err) t.Log(tc.name) } - err = keeper.pricefeedKeeper.SetCurrentPrices(ctx) + err = pricefeedKeeper.SetCurrentPrices(ctx) if err != nil { t.Log("test context height", ctx.BlockHeight()) t.Log(err) @@ -134,20 +131,17 @@ func TestKeeper_ModifyCDP(t *testing.T) { } keeper.SetGlobalDebt(ctx, tc.priorState.GlobalDebt) if tc.priorState.CollateralState.Denom != "" { - keeper.setCollateralState(ctx, tc.priorState.CollateralState) + 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)) @@ -166,45 +160,48 @@ func TestKeeper_ModifyCDP(t *testing.T) { 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) + require.Equal(t, tc.expectedState.OwnerCoins, tApp.GetAccountKeeper().GetAccount(ctx, ownerAddr).GetCoins()) }) } } -// 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))) + _, addrs := app.GeneratePrivKeyAddressPairs(1) testAddr := addrs[0] - mock.SetGenesis(mapp, genAccs) + + tApp := app.NewTestApp() + authGenState := tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c(collateral, 100))}) + tApp.InitializeFromGenesisStates(authGenState) + + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetCDPKeeper() + // 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()) + pricefeedKeeper := tApp.GetPriceFeedKeeper() ap := pricefeed.AssetParams{ Assets: []pricefeed.Asset{ - pricefeed.Asset{AssetCode: "xrp", Description: ""}, + pricefeed.Asset{AssetCode: collateral, Description: ""}, }, } - keeper.pricefeedKeeper.SetAssetParams(ctx, ap) - keeper.pricefeedKeeper.SetPrice( + pricefeedKeeper.SetAssetParams(ctx, ap) + pricefeedKeeper.SetPrice( ctx, sdk.AccAddress{}, collateral, sdk.MustNewDecFromStr("1.00"), i(10)) - keeper.pricefeedKeeper.SetCurrentPrices(ctx) + require.NoError(t, pricefeedKeeper.SetCurrentPrices(ctx)) + // Create CDP - keeper.SetGlobalDebt(ctx, i(0)) + keeper.SetParams(ctx, defaultParamsSingle()) err := keeper.ModifyCDP(ctx, testAddr, collateral, i(10), i(5)) require.NoError(t, err) // Reduce price - keeper.pricefeedKeeper.SetPrice( + pricefeedKeeper.SetPrice( ctx, sdk.AccAddress{}, collateral, sdk.MustNewDecFromStr("0.90"), i(10)) - keeper.pricefeedKeeper.SetCurrentPrices(ctx) + require.NoError(t, pricefeedKeeper.SetCurrentPrices(ctx)) // Seize entire CDP err = keeper.PartialSeizeCDP(ctx, testAddr, collateral, i(10), i(5)) @@ -220,14 +217,12 @@ func TestKeeper_PartialSeizeCDP(t *testing.T) { 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) + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{}) + keeper := tApp.GetCDPKeeper() keeper.SetParams(ctx, defaultParamsMulti()) // setup CDPs - _, addrs := mock.GeneratePrivKeyAddressPairs(2) + _, addrs := app.GeneratePrivKeyAddressPairs(2) cdps := types.CDPs{ {addrs[0], "xrp", i(4000), i(5)}, {addrs[1], "xrp", i(4000), i(2000)}, @@ -285,7 +280,7 @@ func TestKeeper_GetCDPs(t *testing.T) { _, err = keeper.GetCDPs(ctx, "", d("0.34023")) require.Error(t, err) // Check deleting a CDP removes it - keeper.deleteCDP(ctx, cdps[0]) + keeper.DeleteCDP(ctx, cdps[0]) returnedCdps, err = keeper.GetCDPs(ctx, "", sdk.Dec{}) require.NoError(t, err) require.Equal(t, @@ -295,14 +290,15 @@ func TestKeeper_GetCDPs(t *testing.T) { 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) + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{}) + keeper := tApp.GetCDPKeeper() + keeper.SetParams(ctx, defaultParamsSingle()) - _, addrs := mock.GeneratePrivKeyAddressPairs(1) + _, addrs := app.GeneratePrivKeyAddressPairs(1) cdp := types.CDP{addrs[0], "xrp", i(412), i(56)} // write and read from store @@ -314,7 +310,7 @@ func TestKeeper_GetSetDeleteCDP(t *testing.T) { require.Equal(t, cdp, readCDP) // delete auction - keeper.deleteCDP(ctx, cdp) + keeper.DeleteCDP(ctx, cdp) // check auction does not exist _, found = keeper.GetCDP(ctx, cdp.Owner, cdp.CollateralDenom) @@ -322,10 +318,9 @@ func TestKeeper_GetSetDeleteCDP(t *testing.T) { } 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) + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{}) + keeper := tApp.GetCDPKeeper() keeper.SetParams(ctx, defaultParamsSingle()) gDebt := i(4120000) @@ -339,15 +334,14 @@ func TestKeeper_GetSetGDebt(t *testing.T) { 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) + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{}) + keeper := tApp.GetCDPKeeper() keeper.SetParams(ctx, defaultParamsSingle()) collateralState := types.CollateralState{"xrp", i(15400)} // write and read from store - keeper.setCollateralState(ctx, collateralState) + keeper.SetCollateralState(ctx, collateralState) readCState, found := keeper.GetCollateralState(ctx, collateralState.Denom) // check before and after match diff --git a/x/cdp/keeper/test_common.go b/x/cdp/keeper/test_common.go deleted file mode 100644 index 474b069e..00000000 --- a/x/cdp/keeper/test_common.go +++ /dev/null @@ -1,88 +0,0 @@ -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"}, - } -} From 2d73e627731320b07f348c72473755d7f52b7e2f Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 3 Dec 2019 14:35:40 +0000 Subject: [PATCH 09/99] update cdp tests --- x/cdp/app_test.go | 61 ++++++++++++++++++++++++--------------- x/cdp/integration_test.go | 11 +++++++ x/cdp/test_common.go | 52 --------------------------------- 3 files changed, 49 insertions(+), 75 deletions(-) create mode 100644 x/cdp/integration_test.go delete mode 100644 x/cdp/test_common.go diff --git a/x/cdp/app_test.go b/x/cdp/app_test.go index 39737d43..684bb00c 100644 --- a/x/cdp/app_test.go +++ b/x/cdp/app_test.go @@ -1,30 +1,37 @@ -package cdp +package cdp_test import ( "testing" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/simapp" 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/app" + "github.com/kava-labs/kava/x/cdp" "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))) + tApp := app.NewTestApp() + privKeys, addrs := app.GeneratePrivKeyAddressPairs(1) 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{ + tApp.InitializeFromGenesisStates( + tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c("xrp", 100))}), + ) + // check balance + ctx := tApp.NewContext(false, abci.Header{}) + require.Equal(t, cs(c("xrp", 100)), tApp.GetAccountKeeper().GetAccount(ctx, testAddr).GetCoins()) + + // setup cdp keeper + keeper := tApp.GetCDPKeeper() + params := cdp.CdpParams{ GlobalDebtLimit: sdk.NewInt(100000), - CollateralParams: []CollateralParams{ + CollateralParams: []cdp.CollateralParams{ { Denom: "xrp", LiquidationRatio: sdk.MustNewDecFromStr("1.5"), @@ -35,6 +42,8 @@ func TestApp_CreateModifyDeleteCDP(t *testing.T) { } keeper.SetParams(ctx, params) keeper.SetGlobalDebt(ctx, sdk.NewInt(0)) + // setup pricefeed + pfKeeper := tApp.GetPriceFeedKeeper() ap := pricefeed.AssetParams{ Assets: []pricefeed.Asset{pricefeed.Asset{AssetCode: "xrp", Description: ""}}, } @@ -44,24 +53,30 @@ func TestApp_CreateModifyDeleteCDP(t *testing.T) { sdk.MustNewDecFromStr("1.00"), sdk.NewInt(10)) pfKeeper.SetCurrentPrices(ctx) - mapp.EndBlock(abci.RequestEndBlock{}) - mapp.Commit() + tApp.EndBlock(abci.RequestEndBlock{}) + tApp.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) + msgs := []sdk.Msg{cdp.NewMsgCreateOrModifyCDP(testAddr, "xrp", i(10), i(5))} + simapp.SignCheckDeliver(t, tApp.Codec(), tApp.BaseApp, abci.Header{Height: tApp.LastBlockHeight() + 1}, msgs, []uint64{0}, []uint64{0}, true, true, testPrivKey) - mock.CheckBalance(t, mapp, testAddr, cs(c("usdx", 5), c("xrp", 90))) + // check balance + ctx = tApp.NewContext(true, abci.Header{}) + require.Equal(t, cs(c("usdx", 5), c("xrp", 90)), tApp.GetAccountKeeper().GetAccount(ctx, testAddr).GetCoins()) // 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) + msgs = []sdk.Msg{cdp.NewMsgCreateOrModifyCDP(testAddr, "xrp", i(40), i(5))} + simapp.SignCheckDeliver(t, tApp.Codec(), tApp.BaseApp, abci.Header{Height: tApp.LastBlockHeight() + 1}, msgs, []uint64{0}, []uint64{1}, true, true, testPrivKey) - mock.CheckBalance(t, mapp, testAddr, cs(c("usdx", 10), c("xrp", 50))) + // check balance + ctx = tApp.NewContext(true, abci.Header{}) + require.Equal(t, cs(c("usdx", 10), c("xrp", 50)), tApp.GetAccountKeeper().GetAccount(ctx, testAddr).GetCoins()) // 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) + msgs = []sdk.Msg{cdp.NewMsgCreateOrModifyCDP(testAddr, "xrp", i(-50), i(-10))} + simapp.SignCheckDeliver(t, tApp.Codec(), tApp.BaseApp, abci.Header{Height: tApp.LastBlockHeight() + 1}, msgs, []uint64{0}, []uint64{2}, true, true, testPrivKey) - mock.CheckBalance(t, mapp, testAddr, cs(c("xrp", 100))) + // check balance + ctx = tApp.NewContext(true, abci.Header{}) + require.Equal(t, cs(c("xrp", 100)), tApp.GetAccountKeeper().GetAccount(ctx, testAddr).GetCoins()) } diff --git a/x/cdp/integration_test.go b/x/cdp/integration_test.go new file mode 100644 index 00000000..4ba15395 --- /dev/null +++ b/x/cdp/integration_test.go @@ -0,0 +1,11 @@ +package cdp_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// 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...) } diff --git a/x/cdp/test_common.go b/x/cdp/test_common.go deleted file mode 100644 index 54066f56..00000000 --- a/x/cdp/test_common.go +++ /dev/null @@ -1,52 +0,0 @@ -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...) } From bf83a9bf8f4c224cbae0bb26b9516124a5af7c4b Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Wed, 4 Dec 2019 11:32:08 -0500 Subject: [PATCH 10/99] address review comments for pricefeed --- x/pricefeed/abci.go | 14 ++++- x/pricefeed/alias.go | 58 ++++++++++--------- x/pricefeed/genesis.go | 10 ++-- x/pricefeed/keeper/keeper.go | 96 +++++++++++++++++-------------- x/pricefeed/keeper/keeper_test.go | 56 +++++++++--------- x/pricefeed/keeper/params.go | 44 +++++++------- x/pricefeed/keeper/querier.go | 14 ++--- x/pricefeed/types/asset.go | 42 +++++++------- x/pricefeed/types/events.go | 8 +++ x/pricefeed/types/params.go | 22 +++---- 10 files changed, 195 insertions(+), 169 deletions(-) create mode 100644 x/pricefeed/types/events.go diff --git a/x/pricefeed/abci.go b/x/pricefeed/abci.go index 67a0cb0b..ac0ca068 100644 --- a/x/pricefeed/abci.go +++ b/x/pricefeed/abci.go @@ -1,17 +1,25 @@ package pricefeed import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" ) // EndBlocker updates the current pricefeed func EndBlocker(ctx sdk.Context, k Keeper) { // Update the current price of each asset. - for _, a := range k.GetAssetParams(ctx) { + for _, a := range k.GetMarketParams(ctx) { if a.Active { - err := k.SetCurrentPrices(ctx, a.AssetCode) + err := k.SetCurrentPrices(ctx, a.MarketID) if err != nil { - // TODO emit an event that price failed to update + // In the event of failure, emit an event. + ctx.EventManager().EmitEvent( + sdk.NewEvent( + EventTypeNoValidPrices, + sdk.NewAttribute(AttributeKeyPriceUpdateFailed, fmt.Sprintf("%s", a.MarketID)), + ), + ) continue } } diff --git a/x/pricefeed/alias.go b/x/pricefeed/alias.go index bbe6e654..01a51ea7 100644 --- a/x/pricefeed/alias.go +++ b/x/pricefeed/alias.go @@ -11,25 +11,27 @@ import ( ) 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 - QuerierRoute = types.QuerierRoute - DefaultParamspace = types.DefaultParamspace - RawPriceFeedPrefix = types.RawPriceFeedPrefix - CurrentPricePrefix = types.CurrentPricePrefix - AssetPrefix = types.AssetPrefix - OraclePrefix = types.OraclePrefix - TypeMsgPostPrice = types.TypeMsgPostPrice - QueryCurrentPrice = types.QueryCurrentPrice - QueryRawPrices = types.QueryRawPrices - QueryAssets = types.QueryAssets + DefaultCodespace = types.DefaultCodespace + CodeEmptyInput = types.CodeEmptyInput + CodeExpired = types.CodeExpired + CodeInvalidPrice = types.CodeInvalidPrice + CodeInvalidAsset = types.CodeInvalidAsset + CodeInvalidOracle = types.CodeInvalidOracle + EventTypeNoValidPrices = types.EventTypeNoValidPrices + AttributeKeyPriceUpdateFailed = types.AttributeKeyPriceUpdateFailed + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey + QuerierRoute = types.QuerierRoute + DefaultParamspace = types.DefaultParamspace + RawPriceFeedPrefix = types.RawPriceFeedPrefix + CurrentPricePrefix = types.CurrentPricePrefix + AssetPrefix = types.AssetPrefix + OraclePrefix = types.OraclePrefix + TypeMsgPostPrice = types.TypeMsgPostPrice + QueryCurrentPrice = types.QueryCurrentPrice + QueryRawPrices = types.QueryRawPrices + QueryAssets = types.QueryAssets ) var ( @@ -51,23 +53,23 @@ var ( NewQuerier = keeper.NewQuerier // variable aliases - ModuleCdc = types.ModuleCdc - KeyAssets = types.KeyAssets + ModuleCdc = types.ModuleCdc + KeyMarkets = types.KeyMarkets ) type ( + Market = types.Market + Markets = types.Markets + Oracle = types.Oracle + Oracles = types.Oracles + CurrentPrice = types.CurrentPrice + PostedPrice = types.PostedPrice + SortDecs = types.SortDecs GenesisState = types.GenesisState MsgPostPrice = types.MsgPostPrice Params = types.Params ParamSubspace = types.ParamSubspace QueryRawPricesResp = types.QueryRawPricesResp QueryAssetsResp = types.QueryAssetsResp - Asset = types.Asset - Assets = types.Assets - Oracle = types.Oracle - Oracles = types.Oracles - CurrentPrice = types.CurrentPrice - PostedPrice = types.PostedPrice - SortDecs = types.SortDecs Keeper = keeper.Keeper ) diff --git a/x/pricefeed/genesis.go b/x/pricefeed/genesis.go index 87cb45fc..3bf16251 100644 --- a/x/pricefeed/genesis.go +++ b/x/pricefeed/genesis.go @@ -12,16 +12,16 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { // Iterate through the posted prices and set them in the store for _, pp := range data.PostedPrices { - _, err := keeper.SetPrice(ctx, pp.OracleAddress, pp.AssetCode, pp.Price, pp.Expiry) + _, err := keeper.SetPrice(ctx, pp.OracleAddress, pp.MarketID, pp.Price, pp.Expiry) if err != nil { panic(err) } } // Set the current price (if any) based on what's now in the store - for _, a := range data.Params.Assets { + for _, a := range data.Params.Markets { if a.Active { - _ = keeper.SetCurrentPrices(ctx, a.AssetCode) + _ = keeper.SetCurrentPrices(ctx, a.MarketID) } } } @@ -33,8 +33,8 @@ func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { params := keeper.GetParams(ctx) var postedPrices []PostedPrice - for _, asset := range keeper.GetAssetParams(ctx) { - pp := keeper.GetRawPrices(ctx, asset.AssetCode) + for _, asset := range keeper.GetMarketParams(ctx) { + pp := keeper.GetRawPrices(ctx, asset.MarketID) postedPrices = append(postedPrices, pp...) } diff --git a/x/pricefeed/keeper/keeper.go b/x/pricefeed/keeper/keeper.go index a5fcfa0d..f50291d7 100644 --- a/x/pricefeed/keeper/keeper.go +++ b/x/pricefeed/keeper/keeper.go @@ -41,13 +41,13 @@ func NewKeeper( func (k Keeper) SetPrice( ctx sdk.Context, oracle sdk.AccAddress, - assetCode string, + marketID string, price sdk.Dec, expiry time.Time) (types.PostedPrice, sdk.Error) { // If the expiry is less than or equal to the current blockheight, we consider the price valid if expiry.After(ctx.BlockTime()) { store := ctx.KVStore(k.storeKey) - prices := k.GetRawPrices(ctx, assetCode) + prices := k.GetRawPrices(ctx, marketID) var index int found := false for i := range prices { @@ -60,17 +60,17 @@ func (k Keeper) SetPrice( // set the price for that particular oracle if found { prices[index] = types.PostedPrice{ - AssetCode: assetCode, OracleAddress: oracle, + MarketID: marketID, OracleAddress: oracle, Price: price, Expiry: expiry} } else { prices = append(prices, types.PostedPrice{ - AssetCode: assetCode, OracleAddress: oracle, + MarketID: marketID, OracleAddress: oracle, Price: price, Expiry: expiry}) index = len(prices) - 1 } store.Set( - []byte(types.RawPriceFeedPrefix+assetCode), k.cdc.MustMarshalBinaryBare(prices), + []byte(types.RawPriceFeedPrefix+marketID), k.cdc.MustMarshalBinaryBare(prices), ) return prices[index], nil } @@ -79,67 +79,74 @@ func (k Keeper) SetPrice( } // SetCurrentPrices updates the price of an asset to the meadian of all valid oracle inputs -func (k Keeper) SetCurrentPrices(ctx sdk.Context, assetCode string) sdk.Error { - _, ok := k.GetAsset(ctx, assetCode) +func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) sdk.Error { + _, ok := k.GetMarket(ctx, marketID) if !ok { return types.ErrInvalidAsset(k.codespace) } - prices := k.GetRawPrices(ctx, assetCode) + prices := k.GetRawPrices(ctx, marketID) var notExpiredPrices []types.CurrentPrice // filter out expired prices for _, v := range prices { if v.Expiry.After(ctx.BlockTime()) { notExpiredPrices = append(notExpiredPrices, types.CurrentPrice{ - AssetCode: v.AssetCode, - Price: v.Price, + MarketID: v.MarketID, + Price: v.Price, }) } } - l := len(notExpiredPrices) - var medianPrice sdk.Dec - // 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 types.ErrNoValidPrice(k.codespace) - } else if l == 1 { - // Return immediately if there's only one price - medianPrice = notExpiredPrices[0].Price - } 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) - } else { - // integer division, so we'll get an integer back, rounded down - medianPrice = notExpiredPrices[l/2].Price - } + medianPrice, err := k.CalculateMedianPrice(ctx, notExpiredPrices) + if err != nil { + return err } store := ctx.KVStore(k.storeKey) currentPrice := types.CurrentPrice{ - AssetCode: assetCode, - Price: medianPrice, + MarketID: marketID, + Price: medianPrice, } store.Set( - []byte(types.CurrentPricePrefix+assetCode), k.cdc.MustMarshalBinaryBare(currentPrice), + []byte(types.CurrentPricePrefix+marketID), k.cdc.MustMarshalBinaryBare(currentPrice), ) return nil } +// CalculateMedianPrice calculates the median prices for the input prices. +func (k Keeper) CalculateMedianPrice(ctx sdk.Context, prices []types.CurrentPrice) (sdk.Dec, sdk.Error) { + l := len(prices) + + if l == 0 { + // Error if there are no valid prices in the raw pricefeed + return sdk.Dec{}, types.ErrNoValidPrice(k.codespace) + } else if l == 1 { + // Return immediately if there's only one price + return prices[0].Price, nil + } else { + // sort the prices + sort.Slice(prices, func(i, j int) bool { + return prices[i].Price.LT(prices[j].Price) + }) + // for even numbers of prices, the median is calculated as the mean of the two middle prices + if l%2 == 0 { + median := k.calculateMeanPrice(ctx, prices[l/2-1:l/2+1]) + return median, nil + } + // for odd numbers of prices, return the middle element + return prices[l/2].Price, nil + } +} + +func (k Keeper) calculateMeanPrice(ctx sdk.Context, prices []types.CurrentPrice) sdk.Dec { + sum := prices[0].Price.Add(prices[1].Price) + mean := sum.Quo(sdk.NewDec(2)) + return mean +} + // GetCurrentPrice fetches the current median price of all oracles for a specific asset -func (k Keeper) GetCurrentPrice(ctx sdk.Context, assetCode string) types.CurrentPrice { +func (k Keeper) GetCurrentPrice(ctx sdk.Context, marketID string) types.CurrentPrice { store := ctx.KVStore(k.storeKey) - bz := store.Get([]byte(types.CurrentPricePrefix + assetCode)) + bz := store.Get([]byte(types.CurrentPricePrefix + marketID)) // TODO panic or return error if not found var price types.CurrentPrice k.cdc.MustUnmarshalBinaryBare(bz, &price) @@ -147,14 +154,15 @@ func (k Keeper) GetCurrentPrice(ctx sdk.Context, assetCode string) types.Current } // GetRawPrices fetches the set of all prices posted by oracles for an asset -func (k Keeper) GetRawPrices(ctx sdk.Context, assetCode string) []types.PostedPrice { +func (k Keeper) GetRawPrices(ctx sdk.Context, marketID string) []types.PostedPrice { store := ctx.KVStore(k.storeKey) - bz := store.Get([]byte(types.RawPriceFeedPrefix + assetCode)) + bz := store.Get([]byte(types.RawPriceFeedPrefix + marketID)) var prices []types.PostedPrice k.cdc.MustUnmarshalBinaryBare(bz, &prices) return prices } +// Codespace return the codespace for the keeper func (k Keeper) Codespace() sdk.CodespaceType { return k.codespace } diff --git a/x/pricefeed/keeper/keeper_test.go b/x/pricefeed/keeper/keeper_test.go index 1c93cb9d..105062bb 100644 --- a/x/pricefeed/keeper/keeper_test.go +++ b/x/pricefeed/keeper/keeper_test.go @@ -12,8 +12,8 @@ import ( "github.com/kava-labs/kava/x/pricefeed/types" ) -// TestKeeper_SetGetAsset tests adding assets to the pricefeed, getting assets from the store -func TestKeeper_SetGetAsset(t *testing.T) { +// TestKeeper_SetGetMarket tests adding markets to the pricefeed, getting markets from the store +func TestKeeper_SetGetMarket(t *testing.T) { helper := getMockApp(t, 0, types.GenesisState{}, nil) header := abci.Header{ Height: helper.mApp.LastBlockHeight() + 1, @@ -21,32 +21,32 @@ func TestKeeper_SetGetAsset(t *testing.T) { helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header}) ctx := helper.mApp.BaseApp.NewContext(false, header) - ap := types.Params{ - Assets: []types.Asset{ - types.Asset{AssetCode: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + mp := types.Params{ + Markets: types.Markets{ + types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, }, } - helper.keeper.SetParams(ctx, ap) - assets := helper.keeper.GetAssetParams(ctx) - require.Equal(t, len(assets), 1) - require.Equal(t, assets[0].AssetCode, "tstusd") + helper.keeper.SetParams(ctx, mp) + markets := helper.keeper.GetMarketParams(ctx) + require.Equal(t, len(markets), 1) + require.Equal(t, markets[0].MarketID, "tstusd") - _, found := helper.keeper.GetAsset(ctx, "tstusd") + _, found := helper.keeper.GetMarket(ctx, "tstusd") require.Equal(t, found, true) - ap = types.Params{ - Assets: []types.Asset{ - types.Asset{AssetCode: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, - types.Asset{AssetCode: "tst2usd", BaseAsset: "tst2", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + mp = types.Params{ + Markets: types.Markets{ + types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + types.Market{MarketID: "tst2usd", BaseAsset: "tst2", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, }, } - helper.keeper.SetParams(ctx, ap) - assets = helper.keeper.GetAssetParams(ctx) - require.Equal(t, len(assets), 2) - require.Equal(t, assets[0].AssetCode, "tstusd") - require.Equal(t, assets[1].AssetCode, "tst2usd") + helper.keeper.SetParams(ctx, mp) + markets = helper.keeper.GetMarketParams(ctx) + require.Equal(t, len(markets), 2) + require.Equal(t, markets[0].MarketID, "tstusd") + require.Equal(t, markets[1].MarketID, "tst2usd") - _, found = helper.keeper.GetAsset(ctx, "nan") + _, found = helper.keeper.GetMarket(ctx, "nan") require.Equal(t, found, false) } @@ -58,12 +58,12 @@ func TestKeeper_GetSetPrice(t *testing.T) { Time: tmtime.Now()} helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header}) ctx := helper.mApp.BaseApp.NewContext(false, header) - ap := types.Params{ - Assets: []types.Asset{ - types.Asset{AssetCode: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + mp := types.Params{ + Markets: types.Markets{ + types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, }, } - helper.keeper.SetParams(ctx, ap) + helper.keeper.SetParams(ctx, mp) // Set price by oracle 1 _, err := helper.keeper.SetPrice( ctx, helper.addrs[0], "tstusd", @@ -103,12 +103,12 @@ func TestKeeper_GetSetCurrentPrice(t *testing.T) { Time: tmtime.Now()} helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header}) ctx := helper.mApp.BaseApp.NewContext(false, header) - ap := types.Params{ - Assets: []types.Asset{ - types.Asset{AssetCode: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + mp := types.Params{ + Markets: types.Markets{ + types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, }, } - helper.keeper.SetParams(ctx, ap) + helper.keeper.SetParams(ctx, mp) helper.keeper.SetPrice( ctx, helper.addrs[0], "tstusd", sdk.MustNewDecFromStr("0.33"), diff --git a/x/pricefeed/keeper/params.go b/x/pricefeed/keeper/params.go index 576c9398..aa64cca1 100644 --- a/x/pricefeed/keeper/params.go +++ b/x/pricefeed/keeper/params.go @@ -10,7 +10,7 @@ import ( // GetParams gets params from the store func (k Keeper) GetParams(ctx sdk.Context) types.Params { - return types.NewParams(k.GetAssetParams(ctx)) + return types.NewParams(k.GetMarketParams(ctx)) } // SetParams updates params in the store @@ -18,47 +18,47 @@ func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { k.paramstore.SetParamSet(ctx, ¶ms) } -// GetAssetParams get asset params from store -func (k Keeper) GetAssetParams(ctx sdk.Context) types.Assets { - var assets types.Assets - k.paramstore.Get(ctx, types.KeyAssets, &assets) - return assets +// GetMarketParams get asset params from store +func (k Keeper) GetMarketParams(ctx sdk.Context) types.Markets { + var markets types.Markets + k.paramstore.Get(ctx, types.KeyMarkets, &markets) + return markets } // GetOracles returns the oracles in the pricefeed store -func (k Keeper) GetOracles(ctx sdk.Context, assetCode string) (types.Oracles, error) { +func (k Keeper) GetOracles(ctx sdk.Context, marketID string) (types.Oracles, error) { - for _, a := range k.GetAssetParams(ctx) { - if assetCode == a.AssetCode { - return a.Oracles, nil + for _, m := range k.GetMarketParams(ctx) { + if marketID == m.MarketID { + return m.Oracles, nil } } - return types.Oracles{}, fmt.Errorf("asset %s not found", assetCode) + return types.Oracles{}, fmt.Errorf("asset %s not found", marketID) } // GetOracle returns the oracle from the store or an error if not found -func (k Keeper) GetOracle(ctx sdk.Context, assetCode string, address sdk.AccAddress) (types.Oracle, error) { - oracles, err := k.GetOracles(ctx, assetCode) +func (k Keeper) GetOracle(ctx sdk.Context, marketID string, address sdk.AccAddress) (types.Oracle, error) { + oracles, err := k.GetOracles(ctx, marketID) if err != nil { - return types.Oracle{}, fmt.Errorf("asset %s not found", assetCode) + return types.Oracle{}, fmt.Errorf("asset %s not found", marketID) } for _, o := range oracles { if address.Equals(o.Address) { return o, nil } } - return types.Oracle{}, fmt.Errorf("oracle %s not found for asset %s", address, assetCode) + return types.Oracle{}, fmt.Errorf("oracle %s not found for asset %s", address, marketID) } -// GetAsset returns the asset if it is in the pricefeed system -func (k Keeper) GetAsset(ctx sdk.Context, assetCode string) (types.Asset, bool) { - assets := k.GetAssetParams(ctx) +// GetMarket returns the market if it is in the pricefeed system +func (k Keeper) GetMarket(ctx sdk.Context, marketID string) (types.Market, bool) { + markets := k.GetMarketParams(ctx) - for i := range assets { - if assets[i].AssetCode == assetCode { - return assets[i], true + for i := range markets { + if markets[i].MarketID == marketID { + return markets[i], true } } - return types.Asset{}, false + return types.Market{}, false } diff --git a/x/pricefeed/keeper/querier.go b/x/pricefeed/keeper/querier.go index 245fa754..a862b776 100644 --- a/x/pricefeed/keeper/querier.go +++ b/x/pricefeed/keeper/querier.go @@ -27,12 +27,12 @@ func NewQuerier(keeper Keeper) sdk.Querier { } 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) + marketID := path[0] + _, found := keeper.GetMarket(ctx, marketID) if !found { return []byte{}, sdk.ErrUnknownRequest("asset not found") } - currentPrice := keeper.GetCurrentPrice(ctx, assetCode) + currentPrice := keeper.GetCurrentPrice(ctx, marketID) bz, err2 := codec.MarshalJSONIndent(keeper.cdc, currentPrice) if err2 != nil { @@ -44,12 +44,12 @@ func queryCurrentPrice(ctx sdk.Context, path []string, req abci.RequestQuery, ke func queryRawPrices(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { var priceList types.QueryRawPricesResp - assetCode := path[0] - _, found := keeper.GetAsset(ctx, assetCode) + marketID := path[0] + _, found := keeper.GetMarket(ctx, marketID) if !found { return []byte{}, sdk.ErrUnknownRequest("asset not found") } - rawPrices := keeper.GetRawPrices(ctx, assetCode) + rawPrices := keeper.GetRawPrices(ctx, marketID) for _, price := range rawPrices { priceList = append(priceList, price.String()) } @@ -63,7 +63,7 @@ func queryRawPrices(ctx sdk.Context, path []string, req abci.RequestQuery, keepe func queryAssets(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { var assetList types.QueryAssetsResp - assets := keeper.GetAssetParams(ctx) + assets := keeper.GetMarketParams(ctx) for _, asset := range assets { assetList = append(assetList, asset.String()) } diff --git a/x/pricefeed/types/asset.go b/x/pricefeed/types/asset.go index 83ba1c0b..bc19b532 100644 --- a/x/pricefeed/types/asset.go +++ b/x/pricefeed/types/asset.go @@ -8,34 +8,34 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// Asset struct that represents an asset in the pricefeed -type Asset struct { - AssetCode string `json:"asset_code" yaml:"asset_code"` +// Market struct that represents an asset in the pricefeed +type Market struct { + MarketID string `json:"market_id" yaml:"market_id"` BaseAsset string `json:"base_asset" yaml:"base_asset"` QuoteAsset string `json:"quote_asset" yaml:"quote_asset"` Oracles Oracles `json:"oracles" yaml:"oracles"` Active bool `json:"active" yaml:"active"` } -// implement fmt.Stringer -func (a Asset) String() string { +// String implement fmt.Stringer +func (a Market) String() string { return fmt.Sprintf(`Asset: - Asset Code: %s + Market ID: %s Base Asset: %s Quote Asset: %s Oracles: %s Active: %t`, - a.AssetCode, a.BaseAsset, a.QuoteAsset, a.Oracles, a.Active) + a.MarketID, a.BaseAsset, a.QuoteAsset, a.Oracles, a.Active) } -// Assets array type for oracle -type Assets []Asset +// Markets array type for oracle +type Markets []Market // String implements fmt.Stringer -func (as Assets) String() string { - out := "Assets:\n" - for _, a := range as { - out += fmt.Sprintf("%s\n", a.String()) +func (ms Markets) String() string { + out := "Markets:\n" + for _, m := range ms { + out += fmt.Sprintf("%s\n", m.String()) } return strings.TrimSpace(out) } @@ -64,13 +64,13 @@ func (os Oracles) String() string { // 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" yaml:"asset_code"` - Price sdk.Dec `json:"price" yaml:"price"` + MarketID string `json:"market_id" yaml:"market_id"` + Price sdk.Dec `json:"price" yaml:"price"` } // PostedPrice struct represented a price for an asset posted by a specific oracle type PostedPrice struct { - AssetCode string `json:"asset_code" yaml:"asset_code"` + MarketID string `json:"market_id" yaml:"market_id"` OracleAddress sdk.AccAddress `json:"oracle_address" yaml:"oracle_address"` Price sdk.Dec `json:"price" yaml:"price"` Expiry time.Time `json:"expiry" yaml:"expiry"` @@ -78,16 +78,16 @@ type PostedPrice struct { // implement fmt.Stringer func (cp CurrentPrice) String() string { - return strings.TrimSpace(fmt.Sprintf(`AssetCode: %s -Price: %s`, cp.AssetCode, cp.Price)) + return strings.TrimSpace(fmt.Sprintf(`Market ID: %s +Price: %s`, cp.MarketID, cp.Price)) } // implement fmt.Stringer func (pp PostedPrice) String() string { - return strings.TrimSpace(fmt.Sprintf(`AssetCode: %s -OracleAddress: %s + return strings.TrimSpace(fmt.Sprintf(`Market ID: %s +Oracle Address: %s Price: %s -Expiry: %s`, pp.AssetCode, pp.OracleAddress, pp.Price, pp.Expiry)) +Expiry: %s`, pp.MarketID, pp.OracleAddress, pp.Price, pp.Expiry)) } // SortDecs provides the interface needed to sort sdk.Dec slices diff --git a/x/pricefeed/types/events.go b/x/pricefeed/types/events.go new file mode 100644 index 00000000..0bf5fa51 --- /dev/null +++ b/x/pricefeed/types/events.go @@ -0,0 +1,8 @@ +package types + +// Pricefeed module event types +const ( + EventTypeNoValidPrices = "no_valid_prices" + + AttributeKeyPriceUpdateFailed = "price_update_failed" +) diff --git a/x/pricefeed/types/params.go b/x/pricefeed/types/params.go index 49d68716..fdef14d3 100644 --- a/x/pricefeed/types/params.go +++ b/x/pricefeed/types/params.go @@ -9,8 +9,8 @@ import ( ) var ( - // KeyAssets store key for assets - KeyAssets = []byte("assets") + // KeyMarkets store key for markets + KeyMarkets = []byte("Markets") ) // ParamKeyTable Key declaration for parameters @@ -20,33 +20,33 @@ func ParamKeyTable() params.KeyTable { // Params params for pricefeed. Can be altered via governance type Params struct { - Assets []Asset `json:"assets" yaml:"assets"` // Array containing the assets supported by the pricefeed + Markets Markets `json:"markets" yaml:"markets"` // Array containing the markets supported by the pricefeed } // ParamSetPairs implements the ParamSet interface and returns all the key/value pairs // pairs of pricefeed module's parameters. func (p Params) ParamSetPairs() params.ParamSetPairs { return params.ParamSetPairs{ - {Key: KeyAssets, Value: &p.Assets}, + {Key: KeyMarkets, Value: &p.Markets}, } } // NewParams creates a new AssetParams object -func NewParams(assets []Asset) Params { +func NewParams(markets Markets) Params { return Params{ - Assets: assets, + Markets: markets, } } // DefaultParams default params for pricefeed func DefaultParams() Params { - return NewParams(Assets{}) + return NewParams(Markets{}) } // String implements fmt.stringer func (p Params) String() string { out := "Params:\n" - for _, a := range p.Assets { + for _, a := range p.Markets { out += a.String() } return strings.TrimSpace(out) @@ -61,9 +61,9 @@ type ParamSubspace interface { // Validate ensure that params have valid values func (p Params) Validate() error { // iterate over assets and verify them - for _, asset := range p.Assets { - if asset.AssetCode == "" { - return fmt.Errorf("invalid asset: %s. missing asset code", asset.String()) + for _, asset := range p.Markets { + if asset.MarketID == "" { + return fmt.Errorf("invalid market: %s. missing market ID", asset.String()) } } return nil From 05a99be97b0cf486975836ee1542612e1cc200ab Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Wed, 4 Dec 2019 11:54:53 -0500 Subject: [PATCH 11/99] apply pricefeed changes to other modules --- x/cdp/app_test.go | 6 +- x/cdp/genesis.go | 4 +- x/cdp/keeper/keeper_test.go | 12 +- x/cdp/types/genesis.go | 7 +- x/cdp/types/params.go | 241 +++++++++++++---------------- x/cdp/types/types.go | 64 +++----- x/liquidator/keeper/keeper_test.go | 18 ++- x/liquidator/keeper/test_common.go | 21 +-- 8 files changed, 165 insertions(+), 208 deletions(-) diff --git a/x/cdp/app_test.go b/x/cdp/app_test.go index fed5e0f0..7eac72ee 100644 --- a/x/cdp/app_test.go +++ b/x/cdp/app_test.go @@ -38,9 +38,9 @@ func TestApp_CreateModifyDeleteCDP(t *testing.T) { keeper.SetParams(ctx, params) keeper.SetGlobalDebt(ctx, sdk.NewInt(0)) ap := pricefeed.Params{ - Assets: []pricefeed.Asset{ - pricefeed.Asset{ - AssetCode: "xrp", BaseAsset: "xrp", + Markets: []pricefeed.Market{ + pricefeed.Market{ + MarketID: "xrp", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, }, } diff --git a/x/cdp/genesis.go b/x/cdp/genesis.go index d50be231..afabd6b9 100644 --- a/x/cdp/genesis.go +++ b/x/cdp/genesis.go @@ -10,8 +10,8 @@ func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, data GenesisStat // 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.GetParams(ctx) - for _, a := range ap.Assets { - collateralMap[a.AssetCode] = 1 + for _, m := range ap.Markets { + collateralMap[m.MarketID] = 1 } for _, col := range data.Params.CollateralParams { diff --git a/x/cdp/keeper/keeper_test.go b/x/cdp/keeper/keeper_test.go index df2b3c0b..ebdd855f 100644 --- a/x/cdp/keeper/keeper_test.go +++ b/x/cdp/keeper/keeper_test.go @@ -111,9 +111,9 @@ func TestKeeper_ModifyCDP(t *testing.T) { keeper.SetParams(ctx, defaultParamsSingle()) // setup store state ap := pricefeed.Params{ - Assets: []pricefeed.Asset{ - pricefeed.Asset{ - AssetCode: "xrp", BaseAsset: "xrp", + Markets: []pricefeed.Market{ + pricefeed.Market{ + MarketID: "xrp", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, }, } @@ -186,9 +186,9 @@ func TestKeeper_PartialSeizeCDP(t *testing.T) { ctx := mapp.BaseApp.NewContext(false, header) keeper.SetParams(ctx, defaultParamsSingle()) ap := pricefeed.Params{ - Assets: []pricefeed.Asset{ - pricefeed.Asset{ - AssetCode: "xrp", BaseAsset: "xrp", + Markets: []pricefeed.Market{ + pricefeed.Market{ + MarketID: "xrp", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, }, } diff --git a/x/cdp/types/genesis.go b/x/cdp/types/genesis.go index 390f9696..04ac4654 100644 --- a/x/cdp/types/genesis.go +++ b/x/cdp/types/genesis.go @@ -1,11 +1,14 @@ 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 Params `json:"params" yaml:"params"` - CDPs CDPs `json:"cdps" yaml:"cdps"` + 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 } diff --git a/x/cdp/types/params.go b/x/cdp/types/params.go index beed781f..d4afa0dd 100644 --- a/x/cdp/types/params.go +++ b/x/cdp/types/params.go @@ -4,168 +4,135 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/params" + "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") - KeyDebtParams = []byte("DebtParams") - KeyCircuitBreaker = []byte("CircuitBreaker") - DefaultGlobalDebt = sdk.Coins{} - DefaultCircuitBreaker = false - DefaultCollateralParams = CollateralParams{} - DefaultDebtParams = DebtParams{} + KeyGlobalDebtLimit = []byte("GlobalDebtLimit") + KeyCollateralParams = []byte("CollateralParams") + KeyStableDenoms = []byte("StableDenoms") ) -// Params governance parameters for cdp module -type Params struct { - CollateralParams CollateralParams `json:"collateral_params" yaml:"collateral_params"` - DebtParams DebtParams `json:"debt_params" yaml:"debt_params"` - GlobalDebtLimit sdk.Coins `json:"global_debt_limit" yaml:"global_debt_limit"` - CircuitBreaker bool `json:"circuit_breaker" yaml:"circuit_breaker"` -} - -// String implements fmt.Stringer -func (p Params) String() string { - return fmt.Sprintf(`Params: - Global Debt Limit: %s - Collateral Params: %s - Debt Params: %s - Circuit Breaker: %t`, - p.GlobalDebtLimit, p.CollateralParams, p.DebtParams, p.CircuitBreaker, - ) -} - -// NewParams returns a new params object -func NewParams(debtLimit sdk.Coins, collateralParams CollateralParams, debtParams DebtParams, breaker bool) Params { - return Params{ - GlobalDebtLimit: debtLimit, - CollateralParams: collateralParams, - DebtParams: debtParams, - CircuitBreaker: breaker, - } -} - -// DefaultParams returns default params for cdp module -func DefaultParams() Params { - return NewParams(DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParams, DefaultCircuitBreaker) -} - -// CollateralParam governance parameters for each collateral type within the cdp module -type CollateralParam struct { - Denom string `json:"denom" yaml:"denom"` // Coin name of collateral type - LiquidationRatio sdk.Dec `json:"liquidation_ratio" yaml:"liquidation_ratio"` // The ratio (Collateral (priced in stable coin) / Debt) under which a CDP will be liquidated - DebtLimit sdk.Coins `json:"debt_limit" yaml:"debt_limit"` // Maximum amount of debt allowed to be drawn from this collateral type - //DebtFloor sdk.Int // used to prevent dust -} - -// String implements fmt.Stringer -func (cp CollateralParam) String() string { - return fmt.Sprintf(`Collateral: - Denom: %s - LiquidationRatio: %s - DebtLimit: %s`, cp.Denom, cp.LiquidationRatio, cp.DebtLimit) -} - -// CollateralParams array of CollateralParam -type CollateralParams []CollateralParam - -// String implements fmt.Stringer -func (cps CollateralParams) String() string { - out := "Collateral Params\n" - for _, cp := range cps { - out += fmt.Sprintf("%s\n", cp) - } - return out -} - -// DebtParam governance params for debt assets -type DebtParam struct { - Denom string `json:"denom" yaml:"denom"` - ReferenceAsset string `json:"reference_asset" yaml:"reference_asset"` - DebtLimit sdk.Coins `json:"debt_limit" yaml:"debt_limit"` -} - -func (dp DebtParam) String() string { - return fmt.Sprintf(`Debt: - Denom: %s - ReferenceAsset: %s - DebtLimit: %s`, dp.Denom, dp.ReferenceAsset, dp.DebtLimit) -} - -// DebtParams array of DebtParam -type DebtParams []DebtParam - -// String implements fmt.Stringer -func (dps DebtParams) String() string { - out := "Debt Params\n" - for _, dp := range dps { - out += fmt.Sprintf("%s\n", dp) - } - return out -} - // ParamKeyTable Key declaration for parameters -func ParamKeyTable() params.KeyTable { - return params.NewKeyTable().RegisterParamSet(&Params{}) +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 *Params) ParamSetPairs() params.ParamSetPairs { - return params.ParamSetPairs{ - {Key: KeyGlobalDebtLimit, Value: &p.GlobalDebtLimit}, - {Key: KeyCollateralParams, Value: &p.CollateralParams}, - {Key: KeyDebtParams, Value: &p.DebtParams}, - {Key: KeyCircuitBreaker, Value: &p.CircuitBreaker}, +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 Params) Validate() error { - debtDenoms := make(map[string]int) - debtParamsDebtLimit := sdk.Coins{} - for _, dp := range p.DebtParams { - _, found := debtDenoms[dp.Denom] - if found { - return fmt.Errorf("duplicate debt denom: %s", dp.Denom) - } - debtDenoms[dp.Denom] = 1 - if dp.DebtLimit.IsAnyNegative() { - return fmt.Errorf("debt limit for all debt tokens should be positive, is %s for %s", dp.DebtLimit, dp.Denom) - } - debtParamsDebtLimit = debtParamsDebtLimit.Add(dp.DebtLimit) - } - if debtParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) { - return fmt.Errorf("debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tdebt limits: %s", - p.GlobalDebtLimit, debtParamsDebtLimit) - } - +func (p CdpParams) Validate() error { collateralDupMap := make(map[string]int) - collateralParamsDebtLimit := sdk.Coins{} - for _, cp := range p.CollateralParams { - _, found := collateralDupMap[cp.Denom] + denomDupMap := make(map[string]int) + for _, collateral := range p.CollateralParams { + _, found := collateralDupMap[collateral.Denom] if found { - return fmt.Errorf("duplicate collateral denom: %s", cp.Denom) + return fmt.Errorf("duplicate denom: %s", collateral.Denom) } - collateralDupMap[cp.Denom] = 1 + collateralDupMap[collateral.Denom] = 1 - if cp.DebtLimit.IsAnyNegative() { - return fmt.Errorf("debt limit for all collaterals should be positive, is %s for %s", cp.DebtLimit, cp.Denom) + if collateral.DebtLimit.IsNegative() { + return fmt.Errorf("debt limit should be positive, is %s for %s", collateral.DebtLimit, collateral.Denom) } - collateralParamsDebtLimit = collateralParamsDebtLimit.Add(cp.DebtLimit) + + // TODO do we want to enforce overcollateralization at this level? -- probably not, as it's technically a governance thing (kevin) } - if collateralParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) { - return fmt.Errorf("collateral debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tcollateral debt limits: %s", - p.GlobalDebtLimit, collateralParamsDebtLimit) + if p.GlobalDebtLimit.IsNegative() { + return fmt.Errorf("global debt limit should be positive, is %s", p.GlobalDebtLimit) } - if p.GlobalDebtLimit.IsAnyNegative() { - return fmt.Errorf("global debt limit should be positive for all debt tokens, 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"}, + } +} diff --git a/x/cdp/types/types.go b/x/cdp/types/types.go index c4b96cbc..02051ed4 100644 --- a/x/cdp/types/types.go +++ b/x/cdp/types/types.go @@ -3,7 +3,6 @@ package types import ( "fmt" "strings" - "time" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -16,41 +15,27 @@ 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.Coins `json:"collateral_amount" yaml:"collateral_amount"` // Amount of collateral stored in this CDP - Debt sdk.Coins `json:"debt" yaml:"debt"` - AccumulatedFees sdk.Coins `json:"accumulated_fees" yaml:"accumulated_fees"` - FeesUpdated time.Time `json:"fees_updated" yaml:"fees_updated"` // Amount of stable coin drawn from 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 } -// IsUnderCollateralized checks if cdp is below the liquidation ratio func (cdp CDP) IsUnderCollateralized(price sdk.Dec, liquidationRatio sdk.Dec) bool { - collateralValue := sdk.NewDecFromInt(cdp.CollateralAmount.AmountOf(cdp.CollateralDenom)).Mul(price) - minCollateralValue := sdk.NewDec(0) - for _, c := range cdp.Debt { - minCollateralValue = minCollateralValue.Add(liquidationRatio.Mul(c.Amount.ToDec())) - } + collateralValue := sdk.NewDecFromInt(cdp.CollateralAmount).Mul(price) + minCollateralValue := liquidationRatio.Mul(sdk.NewDecFromInt(cdp.Debt)) return collateralValue.LT(minCollateralValue) // TODO LT or LTE? } -// String implements fmt.stringer func (cdp CDP) String() string { return strings.TrimSpace(fmt.Sprintf(`CDP: Owner: %s - Collateral Type: %s - Collateral: %s - Debt: %s - Fees: %s - Fees Last Updated: %s`, + Collateral: %s + Debt: %s`, cdp.Owner, - cdp.CollateralDenom, - cdp.CollateralAmount, - cdp.Debt, - cdp.AccumulatedFees, - cdp.FeesUpdated, + sdk.NewCoin(cdp.CollateralDenom, cdp.CollateralAmount), + sdk.NewCoin("usdx", cdp.Debt), )) } -// CDPs array of CDP type CDPs []CDP // String implements stringer @@ -67,23 +52,22 @@ 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) -// } +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 { diff --git a/x/liquidator/keeper/keeper_test.go b/x/liquidator/keeper/keeper_test.go index d69b5547..23fdd974 100644 --- a/x/liquidator/keeper/keeper_test.go +++ b/x/liquidator/keeper/keeper_test.go @@ -2,10 +2,12 @@ package keeper import ( "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/mock" "github.com/stretchr/testify/require" + tmtime "github.com/tendermint/tendermint/types/time" "github.com/kava-labs/kava/x/cdp" "github.com/kava-labs/kava/x/liquidator/types" @@ -19,8 +21,8 @@ func TestKeeper_SeizeAndStartCollateralAuction(t *testing.T) { _, 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.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("8000.00"), tmtime.Now().Add(time.Hour*1)) + k.pricefeedKeeper.SetCurrentPrices(ctx, "btc") cdp.InitGenesis(ctx, k.cdpKeeper, k.pricefeedKeeper, cdpDefaultGenesis()) dp := defaultParams() k.liquidatorKeeper.SetParams(ctx, dp) @@ -28,8 +30,8 @@ func TestKeeper_SeizeAndStartCollateralAuction(t *testing.T) { 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) + k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("7999.99"), tmtime.Now().Add(time.Hour*1)) + k.pricefeedKeeper.SetCurrentPrices(ctx, "btc") // Run test function auctionID, err := k.liquidatorKeeper.SeizeAndStartCollateralAuction(ctx, addrs[0], "btc") @@ -100,16 +102,16 @@ func TestKeeper_partialSeizeCDP(t *testing.T) { pricefeed.InitGenesis(ctx, k.pricefeedKeeper, pricefeedGenesis()) - k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("8000.00"), i(999999999)) - k.pricefeedKeeper.SetCurrentPrices(ctx) + k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("8000.00"), tmtime.Now().Add(time.Hour*1)) + k.pricefeedKeeper.SetCurrentPrices(ctx, "btc") 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) + k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("7999.99"), tmtime.Now().Add(time.Hour*1)) + k.pricefeedKeeper.SetCurrentPrices(ctx, "btc") // Run test function err := k.liquidatorKeeper.partialSeizeCDP(ctx, addrs[0], "btc", i(2), i(10000)) diff --git a/x/liquidator/keeper/test_common.go b/x/liquidator/keeper/test_common.go index 62751c9f..4ec42cb1 100644 --- a/x/liquidator/keeper/test_common.go +++ b/x/liquidator/keeper/test_common.go @@ -1,6 +1,8 @@ package keeper import ( + "time" + "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" @@ -9,6 +11,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/params" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" + tmtime "github.com/tendermint/tendermint/types/time" dbm "github.com/tendermint/tm-db" "github.com/kava-labs/kava/x/auction" @@ -75,7 +78,7 @@ func setupTestKeepers() (sdk.Context, keepers) { bank.DefaultCodespace, blacklistedAddrs, ) - pricefeedKeeper := pricefeed.NewKeeper(keyPriceFeed, cdc, paramsKeeper.Subspace(pricefeed.DefaultParamspace).WithKeyTable(pricefeed.ParamKeyTable()), pricefeed.DefaultCodespace) + pricefeedKeeper := pricefeed.NewKeeper(keyPriceFeed, cdc, paramsKeeper.Subspace(pricefeed.DefaultParamspace), pricefeed.DefaultCodespace) cdpKeeper := cdp.NewKeeper( cdc, keyCDP, @@ -150,20 +153,18 @@ func cdpDefaultGenesis() cdp.GenesisState { } func pricefeedGenesis() pricefeed.GenesisState { - ap := pricefeed.AssetParams{ - Assets: []pricefeed.Asset{ - pricefeed.Asset{AssetCode: "btc", Description: "a description"}, - }, + ap := pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{MarketID: "btc", BaseAsset: "btc", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}}, } return pricefeed.GenesisState{ - AssetParams: ap, - OracleParams: pricefeed.DefaultOracleParams(), + Params: ap, PostedPrices: []pricefeed.PostedPrice{ pricefeed.PostedPrice{ - AssetCode: "btc", - OracleAddress: "", + MarketID: "btc", + OracleAddress: sdk.AccAddress{}, Price: sdk.MustNewDecFromStr("8000.00"), - Expiry: sdk.NewInt(999999999), + Expiry: tmtime.Now().Add(1 * time.Hour), }, }, } From 4662123105bde568e79dc75ccda21a81525f9b25 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 5 Dec 2019 13:51:46 +0000 Subject: [PATCH 12/99] add check balance method to TestApp --- app/test_common.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/test_common.go b/app/test_common.go index 6980260b..af345f28 100644 --- a/app/test_common.go +++ b/app/test_common.go @@ -2,6 +2,9 @@ package app import ( "math/rand" + "testing" + + "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto" @@ -107,6 +110,11 @@ func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) T return tApp } +func (tApp TestApp) CheckBalance(t *testing.T, ctx sdk.Context, owner sdk.AccAddress, expectedCoins sdk.Coins) { + actualCoins := tApp.GetAccountKeeper().GetAccount(ctx, owner).GetCoins() + require.Equal(t, expectedCoins, actualCoins) +} + // GeneratePrivKeyAddressPairsFromRand generates (deterministically) a total of n private keys and addresses. // TODO only generate secp256 keys? func GeneratePrivKeyAddressPairs(n int) (keys []crypto.PrivKey, addrs []sdk.AccAddress) { From ab8331f90a99e31f6d33214b2142d24858271bf9 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 5 Dec 2019 13:53:10 +0000 Subject: [PATCH 13/99] update auction keeper tests --- x/auction/app_test.go | 171 -------------------------------- x/auction/keeper/keeper.go | 12 +-- x/auction/keeper/keeper_test.go | 142 ++++++++++++++++++++++---- x/auction/keeper/test_common.go | 35 ------- 4 files changed, 130 insertions(+), 230 deletions(-) delete mode 100644 x/auction/app_test.go delete mode 100644 x/auction/keeper/test_common.go diff --git a/x/auction/app_test.go b/x/auction/app_test.go deleted file mode 100644 index 20106efb..00000000 --- a/x/auction/app_test.go +++ /dev/null @@ -1,171 +0,0 @@ -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 -} diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index 1ffc561b..f43f81cc 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -144,8 +144,8 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID types.ID) sdk.Error { return err } - // delete auction from store (and queue) - k.deleteAuction(ctx, auctionID) + // Delete auction from store (and queue) + k.DeleteAuction(ctx, auctionID) return nil } @@ -204,7 +204,7 @@ func (k Keeper) SetAuction(ctx sdk.Context, auction types.Auction) { store.Set(k.getAuctionKey(auction.GetID()), bz) // add to the queue - k.insertIntoQueue(ctx, auction.GetEndTime(), auction.GetID()) + k.InsertIntoQueue(ctx, auction.GetEndTime(), auction.GetID()) } // getAuction gets an auction from the store by auctionID @@ -221,8 +221,8 @@ func (k Keeper) GetAuction(ctx sdk.Context, auctionID types.ID) (types.Auction, return auction, true } -// deleteAuction removes an auction from the store without any validation -func (k Keeper) deleteAuction(ctx sdk.Context, auctionID types.ID) { +// 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 { @@ -245,7 +245,7 @@ func (k Keeper) getAuctionKey(auctionID types.ID) []byte { } // Inserts a AuctionID into the queue at endTime -func (k Keeper) insertIntoQueue(ctx sdk.Context, endTime types.EndTime, auctionID types.ID) { +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 diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go index 6c77b32e..79aa9807 100644 --- a/x/auction/keeper/keeper_test.go +++ b/x/auction/keeper/keeper_test.go @@ -1,21 +1,126 @@ -package keeper +package keeper_test 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" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/auction/keeper" + "github.com/kava-labs/kava/x/auction/types" ) +func TestKeeper_ForwardAuction(t *testing.T) { + // Setup + _, addrs := app.GeneratePrivKeyAddressPairs(2) + seller := addrs[0] + buyer := addrs[1] + + tApp := app.NewTestApp() + authGenState := tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}) + tApp.InitializeFromGenesisStates(authGenState) + + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Create an auction (lot: 20 t1, initialBid: 0 t2) + auctionID, err := keeper.StartForwardAuction(ctx, seller, c("token1", 20), c("token2", 0)) // lot, initialBid + require.NoError(t, err) + // Check seller's coins have decreased + tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) + + // PlaceBid (bid: 10 t2, lot: same as starting) + require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 10), c("token1", 20))) // bid, lot + // Check buyer's coins have decreased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) + // Check seller's coins have increased + tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) + + // Close auction at just after auction expiry + ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration)) + require.NoError(t, keeper.CloseAuction(ctx, auctionID)) + // Check buyer's coins increased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90))) +} + +func TestKeeper_ReverseAuction(t *testing.T) { + // Setup + _, addrs := app.GeneratePrivKeyAddressPairs(2) + seller := addrs[0] + buyer := addrs[1] + + tApp := app.NewTestApp() + authGenState := tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}) + tApp.InitializeFromGenesisStates(authGenState) + + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Start auction + auctionID, err := keeper.StartReverseAuction(ctx, buyer, c("token1", 20), c("token2", 99)) // buyer, bid, initialLot + require.NoError(t, err) + // Check buyer's coins have decreased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 1))) + + // Place a bid + require.NoError(t, keeper.PlaceBid(ctx, 0, seller, c("token1", 20), c("token2", 10))) // bid, lot + // Check seller's coins have decreased + tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) + // Check buyer's coins have increased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90))) + + // Close auction at just after auction expiry + ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration)) + require.NoError(t, keeper.CloseAuction(ctx, auctionID)) + // Check seller's coins increased + tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) +} + +func TestKeeper_ForwardReverseAuction(t *testing.T) { + // Setup + _, addrs := app.GeneratePrivKeyAddressPairs(3) + seller := addrs[0] + buyer := addrs[1] + recipient := addrs[2] + + tApp := app.NewTestApp() + authGenState := tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}) + tApp.InitializeFromGenesisStates(authGenState) + + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Start auction + auctionID, err := keeper.StartForwardReverseAuction(ctx, seller, c("token1", 20), c("token2", 50), recipient) // seller, lot, maxBid, otherPerson + require.NoError(t, err) + // Check seller's coins have decreased + tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) + + // Place a bid + require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50), c("token1", 15))) // bid, lot + // Check bidder's coins have decreased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50))) + // Check seller's coins have increased + tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 150))) + // Check "recipient" has received coins + tApp.CheckBalance(t, ctx, recipient, cs(c("token1", 105), c("token2", 100))) + + // Close auction at just after auction expiry + ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration)) + require.NoError(t, keeper.CloseAuction(ctx, auctionID)) + // Check buyer's coins increased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 115), c("token2", 50))) +} + 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)) + _, addrs := app.GeneratePrivKeyAddressPairs(1) + tApp := app.NewTestApp() + keeper := tApp.GetAuctionKeeper() + ctx := tApp.NewContext(true, abci.Header{}) + auction, _ := types.NewForwardAuction(addrs[0], c("usdx", 100), c("kava", 0), types.EndTime(1000)) id := types.ID(5) auction.SetID(id) @@ -26,15 +131,13 @@ func TestKeeper_SetGetDeleteAuction(t *testing.T) { // 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) + keeper.DeleteAuction(ctx, id) // check auction does not exist _, found = keeper.GetAuction(ctx, id) @@ -49,10 +152,10 @@ func TestKeeper_SetGetDeleteAuction(t *testing.T) { // 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) + tApp := app.NewTestApp() + keeper := tApp.GetAuctionKeeper() + ctx := tApp.NewContext(true, abci.Header{}) + // create an example queue type queue []struct { endTime types.EndTime @@ -62,7 +165,7 @@ func TestKeeper_ExpiredAuctionQueue(t *testing.T) { // write and read queue for _, v := range q { - keeper.insertIntoQueue(ctx, v.endTime, v.auctionID) + keeper.InsertIntoQueue(ctx, v.endTime, v.auctionID) } iter := keeper.GetQueueIterator(ctx, 1000) @@ -70,19 +173,22 @@ func TestKeeper_ExpiredAuctionQueue(t *testing.T) { i := 0 for ; iter.Valid(); iter.Next() { var auctionID types.ID - keeper.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &auctionID) + tApp.Codec().MustUnmarshalBinaryLengthPrefixed(iter.Value(), &auctionID) require.Equal(t, q[i].auctionID, auctionID) i++ } } -func convertIteratorToSlice(keeper Keeper, iterator sdk.Iterator) []types.ID { +func convertIteratorToSlice(keeper 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) + types.ModuleCdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &auctionID) queue = append(queue, auctionID) } return queue } + +func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } +func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } diff --git a/x/auction/keeper/test_common.go b/x/auction/keeper/test_common.go deleted file mode 100644 index b5702438..00000000 --- a/x/auction/keeper/test_common.go +++ /dev/null @@ -1,35 +0,0 @@ -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 -} From 215241edd9a2e09418bbf80fcd59d19d747b9fc1 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 5 Dec 2019 13:53:28 +0000 Subject: [PATCH 14/99] update auction module tests --- x/auction/abci_test.go | 45 ++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/x/auction/abci_test.go b/x/auction/abci_test.go index 2fb36bda..7d485335 100644 --- a/x/auction/abci_test.go +++ b/x/auction/abci_test.go @@ -1,4 +1,4 @@ -package auction +package auction_test import ( "testing" @@ -6,23 +6,42 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/auction" ) 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) + // Setup + _, addrs := app.GeneratePrivKeyAddressPairs(1) + seller := addrs[0] - seller := addresses[0] - keeper.StartForwardAuction(ctx, seller, sdk.NewInt64Coin("token1", 20), sdk.NewInt64Coin("token2", 0)) + tApp := app.NewTestApp() + authGenState := tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100))}) + tApp.InitializeFromGenesisStates(authGenState) - // run the endblocker, simulating a block height after auction expiry - expiryBlock := ctx.BlockHeight() + int64(DefaultMaxAuctionDuration) - EndBlocker(ctx.WithBlockHeight(expiryBlock), keeper) + ctx := tApp.NewContext(true, abci.Header{}) + keeper := tApp.GetAuctionKeeper() - // check auction has been closed - _, found := keeper.GetAuction(ctx, 0) + auctionID, err := keeper.StartForwardAuction(ctx, seller, c("token1", 20), c("token2", 0)) + require.NoError(t, err) + + // Run the endblocker, simulating a block height just before auction expiry + preExpiryHeight := ctx.BlockHeight() + int64(auction.DefaultMaxAuctionDuration) - 1 + auction.EndBlocker(ctx.WithBlockHeight(preExpiryHeight), keeper) + + // Check auction has not been closed yet + _, found := keeper.GetAuction(ctx, auctionID) + require.True(t, found) + + // Run the endblocker, simulating a block height just after auction expiry + expiryHeight := preExpiryHeight + 1 + auction.EndBlocker(ctx.WithBlockHeight(expiryHeight), keeper) + + // Check auction has been closed + _, found = keeper.GetAuction(ctx, auctionID) require.False(t, found) } + +func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } +func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } From e3aca06236f4358609b9e2a8c89719bb8144781a Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 5 Dec 2019 13:59:20 +0000 Subject: [PATCH 15/99] update cdp tests to use CheckBalance method --- x/cdp/app_test.go | 10 ++++------ x/cdp/keeper/keeper_test.go | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/x/cdp/app_test.go b/x/cdp/app_test.go index 684bb00c..8dabeb3b 100644 --- a/x/cdp/app_test.go +++ b/x/cdp/app_test.go @@ -3,8 +3,6 @@ package cdp_test import ( "testing" - "github.com/stretchr/testify/require" - "github.com/cosmos/cosmos-sdk/simapp" sdk "github.com/cosmos/cosmos-sdk/types" abci "github.com/tendermint/tendermint/abci/types" @@ -25,7 +23,7 @@ func TestApp_CreateModifyDeleteCDP(t *testing.T) { ) // check balance ctx := tApp.NewContext(false, abci.Header{}) - require.Equal(t, cs(c("xrp", 100)), tApp.GetAccountKeeper().GetAccount(ctx, testAddr).GetCoins()) + tApp.CheckBalance(t, ctx, testAddr, cs(c("xrp", 100))) // setup cdp keeper keeper := tApp.GetCDPKeeper() @@ -62,7 +60,7 @@ func TestApp_CreateModifyDeleteCDP(t *testing.T) { // check balance ctx = tApp.NewContext(true, abci.Header{}) - require.Equal(t, cs(c("usdx", 5), c("xrp", 90)), tApp.GetAccountKeeper().GetAccount(ctx, testAddr).GetCoins()) + tApp.CheckBalance(t, ctx, testAddr, cs(c("usdx", 5), c("xrp", 90))) // Modify CDP msgs = []sdk.Msg{cdp.NewMsgCreateOrModifyCDP(testAddr, "xrp", i(40), i(5))} @@ -70,7 +68,7 @@ func TestApp_CreateModifyDeleteCDP(t *testing.T) { // check balance ctx = tApp.NewContext(true, abci.Header{}) - require.Equal(t, cs(c("usdx", 10), c("xrp", 50)), tApp.GetAccountKeeper().GetAccount(ctx, testAddr).GetCoins()) + tApp.CheckBalance(t, ctx, testAddr, cs(c("usdx", 10), c("xrp", 50))) // Delete CDP msgs = []sdk.Msg{cdp.NewMsgCreateOrModifyCDP(testAddr, "xrp", i(-50), i(-10))} @@ -78,5 +76,5 @@ func TestApp_CreateModifyDeleteCDP(t *testing.T) { // check balance ctx = tApp.NewContext(true, abci.Header{}) - require.Equal(t, cs(c("xrp", 100)), tApp.GetAccountKeeper().GetAccount(ctx, testAddr).GetCoins()) + tApp.CheckBalance(t, ctx, testAddr, cs(c("xrp", 100))) } diff --git a/x/cdp/keeper/keeper_test.go b/x/cdp/keeper/keeper_test.go index 184623cd..aeed1a6a 100644 --- a/x/cdp/keeper/keeper_test.go +++ b/x/cdp/keeper/keeper_test.go @@ -160,7 +160,7 @@ func TestKeeper_ModifyCDP(t *testing.T) { require.Equal(t, tc.expectedState.GlobalDebt, actualGDebt) require.Equal(t, tc.expectedState.CollateralState, actualCstate) // check owner balance - require.Equal(t, tc.expectedState.OwnerCoins, tApp.GetAccountKeeper().GetAccount(ctx, ownerAddr).GetCoins()) + tApp.CheckBalance(t, ctx, ownerAddr, tc.expectedState.OwnerCoins) }) } } From 1295dfbffcab58cd8fcac934b28ddc89a43a4ab5 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 6 Dec 2019 23:49:47 +0000 Subject: [PATCH 16/99] update liquidator tests --- app/test_common.go | 4 +- x/liquidator/keeper/integration_test.go | 73 ++++++++++ x/liquidator/keeper/keeper.go | 12 +- x/liquidator/keeper/keeper_test.go | 118 +++++++--------- x/liquidator/keeper/test_common.go | 171 ------------------------ 5 files changed, 130 insertions(+), 248 deletions(-) create mode 100644 x/liquidator/keeper/integration_test.go delete mode 100644 x/liquidator/keeper/test_common.go diff --git a/app/test_common.go b/app/test_common.go index af345f28..18004b69 100644 --- a/app/test_common.go +++ b/app/test_common.go @@ -72,7 +72,7 @@ func (tApp TestApp) GetLiquidatorKeeper() liquidator.Keeper { return tApp.liquid func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefeedKeeper } // Create a new auth genesis state from some addresses and coins. The state is returned marshalled into a map. -// TODO this is only a TestApp method because it needs access to the codec, could be another way to structure it +// TODO make this not a TestApp method. func (tApp TestApp) NewAuthGenStateFromAccounts(addresses []sdk.AccAddress, coins []sdk.Coins) GenesisState { // Create GenAccounts accounts := authexported.GenesisAccounts{} @@ -81,7 +81,7 @@ func (tApp TestApp) NewAuthGenStateFromAccounts(addresses []sdk.AccAddress, coin } // Create the auth genesis state authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts) - return GenesisState{auth.ModuleName: tApp.cdc.MustMarshalJSON(authGenesis)} + return GenesisState{auth.ModuleName: auth.ModuleCdc.MustMarshalJSON(authGenesis)} } // This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states diff --git a/x/liquidator/keeper/integration_test.go b/x/liquidator/keeper/integration_test.go new file mode 100644 index 00000000..53151269 --- /dev/null +++ b/x/liquidator/keeper/integration_test.go @@ -0,0 +1,73 @@ +package keeper_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/cdp" + "github.com/kava-labs/kava/x/liquidator" + "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...) } + +// Default genesis states to initialize test apps + +func NewPFGenState(asset string, price sdk.Dec) app.GenesisState { + quote := "usd" + ap := pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{MarketID: asset, BaseAsset: asset, QuoteAsset: quote, Oracles: pricefeed.Oracles{}, Active: true}}, + } + pfGenesis := pricefeed.GenesisState{ + Params: ap, + PostedPrices: []pricefeed.PostedPrice{ + pricefeed.PostedPrice{ + MarketID: asset, + OracleAddress: sdk.AccAddress{}, + Price: price, + Expiry: time.Unix(9999999999, 0), // some deterministic future date, + }, + }, + } + return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)} +} + +func NewCDPGenState() app.GenesisState { + cdpGenesis := cdp.GenesisState{ + Params: cdp.CdpParams{ + GlobalDebtLimit: sdk.NewInt(1000000), + CollateralParams: []cdp.CollateralParams{ + { + Denom: "btc", + LiquidationRatio: sdk.MustNewDecFromStr("1.5"), + DebtLimit: sdk.NewInt(500000), + }, + }, + }, + GlobalDebt: sdk.ZeroInt(), + CDPs: cdp.CDPs{}, + } + return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} +} + +func NewLiquidatorGenState() app.GenesisState { + liquidatorGenesis := types.GenesisState{ + Params: types.LiquidatorParams{ + DebtAuctionSize: sdk.NewInt(1000), + CollateralParams: []types.CollateralParams{ + { + Denom: "btc", + AuctionSize: sdk.NewInt(1), + }, + }, + }, + } + return app.GenesisState{liquidator.ModuleName: liquidator.ModuleCdc.MustMarshalJSON(liquidatorGenesis)} +} diff --git a/x/liquidator/keeper/keeper.go b/x/liquidator/keeper/keeper.go index e5a15014..59354270 100644 --- a/x/liquidator/keeper/keeper.go +++ b/x/liquidator/keeper/keeper.go @@ -55,7 +55,7 @@ func (k Keeper) SeizeAndStartCollateralAuction(ctx sdk.Context, owner sdk.AccAdd 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) + err := k.PartialSeizeCDP(ctx, owner, collateralDenom, collateralToSell, stableToRaise) if err != nil { return 0, err } @@ -99,7 +99,7 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context) (auction.ID, sdk.Error) { } // 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) + k.SetSeizedDebt(ctx, seizedDebt) return auctionID, nil } @@ -131,7 +131,7 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context) (auction.ID, sdk.Error) { // } // 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 +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 { @@ -141,7 +141,7 @@ func (k Keeper) partialSeizeCDP(ctx sdk.Context, owner sdk.AccAddress, collatera // increment the total seized debt (Awe) by cdp.debt seizedDebt := k.GetSeizedDebt(ctx) seizedDebt.Total = seizedDebt.Total.Add(debtToSeize) - k.setSeizedDebt(ctx, seizedDebt) + 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)) @@ -172,7 +172,7 @@ func (k Keeper) SettleDebt(ctx sdk.Context) sdk.Error { if err != nil { return err // this should not error in this context } - k.setSeizedDebt(ctx, updatedDebt) + k.SetSeizedDebt(ctx, updatedDebt) // Subtract stable coin from moduleAccout k.bankKeeper.SubtractCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress(), sdk.Coins{sdk.NewCoin(k.cdpKeeper.GetStableDenom(), settleAmount)}) @@ -197,7 +197,7 @@ func (k Keeper) GetSeizedDebt(ctx sdk.Context) types.SeizedDebt { k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &seizedDebt) return seizedDebt } -func (k Keeper) setSeizedDebt(ctx sdk.Context, debt types.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) diff --git a/x/liquidator/keeper/keeper_test.go b/x/liquidator/keeper/keeper_test.go index 23fdd974..c952f11c 100644 --- a/x/liquidator/keeper/keeper_test.go +++ b/x/liquidator/keeper/keeper_test.go @@ -1,124 +1,103 @@ -package keeper +package keeper_test import ( "testing" "time" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/mock" "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" tmtime "github.com/tendermint/tendermint/types/time" - "github.com/kava-labs/kava/x/cdp" + "github.com/kava-labs/kava/app" "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 := app.GeneratePrivKeyAddressPairs(1) - _, addrs := mock.GeneratePrivKeyAddressPairs(1) + tApp := app.NewTestApp() + tApp.InitializeFromGenesisStates( + tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c("btc", 100))}), + NewPFGenState("btc", sdk.MustNewDecFromStr("8000.00")), + NewCDPGenState(), + NewLiquidatorGenState(), + ) + ctx := tApp.NewContext(false, abci.Header{}) - pricefeed.InitGenesis(ctx, k.pricefeedKeeper, pricefeedGenesis()) - k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("8000.00"), tmtime.Now().Add(time.Hour*1)) - k.pricefeedKeeper.SetCurrentPrices(ctx, "btc") - 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))) + require.NoError(t, tApp.GetCDPKeeper().ModifyCDP(ctx, addrs[0], "btc", i(3), i(16000))) - k.cdpKeeper.ModifyCDP(ctx, addrs[0], "btc", i(3), i(16000)) - - k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("7999.99"), tmtime.Now().Add(time.Hour*1)) - k.pricefeedKeeper.SetCurrentPrices(ctx, "btc") + _, err := tApp.GetPriceFeedKeeper().SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("7999.99"), time.Unix(9999999999, 0)) + require.NoError(t, err) + require.NoError(t, tApp.GetPriceFeedKeeper().SetCurrentPrices(ctx, "btc")) // Run test function - auctionID, err := k.liquidatorKeeper.SeizeAndStartCollateralAuction(ctx, addrs[0], "btc") + auctionID, err := tApp.GetLiquidatorKeeper().SeizeAndStartCollateralAuction(ctx, addrs[0], "btc") // Check CDP require.NoError(t, err) - cdp, found := k.cdpKeeper.GetCDP(ctx, addrs[0], "btc") + cdp, found := tApp.GetCDPKeeper().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) + _, found = tApp.GetAuctionKeeper().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()) + tApp := app.NewTestApp() + tApp.InitializeFromGenesisStates( + NewLiquidatorGenState(), + ) + keeper := tApp.GetLiquidatorKeeper() + ctx := tApp.NewContext(false, abci.Header{}) + initSDebt := types.SeizedDebt{i(2000), i(0)} - k.liquidatorKeeper.setSeizedDebt(ctx, initSDebt) + keeper.SetSeizedDebt(ctx, initSDebt) // Execute - auctionID, err := k.liquidatorKeeper.StartDebtAuction(ctx) + auctionID, err := keeper.StartDebtAuction(ctx) // Check require.NoError(t, err) require.Equal(t, types.SeizedDebt{ initSDebt.Total, - initSDebt.SentToAuction.Add(k.liquidatorKeeper.GetParams(ctx).DebtAuctionSize), + initSDebt.SentToAuction.Add(keeper.GetParams(ctx).DebtAuctionSize), }, - k.liquidatorKeeper.GetSeizedDebt(ctx), + keeper.GetSeizedDebt(ctx), ) - _, found := k.auctionKeeper.GetAuction(ctx, auctionID) + _, found := tApp.GetAuctionKeeper().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 := app.GeneratePrivKeyAddressPairs(1) - _, addrs := mock.GeneratePrivKeyAddressPairs(1) + tApp := app.NewTestApp() + tApp.InitializeFromGenesisStates( + tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c("btc", 100))}), + NewPFGenState("btc", sdk.MustNewDecFromStr("8000.00")), + NewCDPGenState(), + NewLiquidatorGenState(), + ) + ctx := tApp.NewContext(false, abci.Header{}) - pricefeed.InitGenesis(ctx, k.pricefeedKeeper, pricefeedGenesis()) + tApp.GetCDPKeeper().ModifyCDP(ctx, addrs[0], "btc", i(3), i(16000)) - k.pricefeedKeeper.SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("8000.00"), tmtime.Now().Add(time.Hour*1)) - k.pricefeedKeeper.SetCurrentPrices(ctx, "btc") - 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"), tmtime.Now().Add(time.Hour*1)) - k.pricefeedKeeper.SetCurrentPrices(ctx, "btc") + tApp.GetPriceFeedKeeper().SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("7999.99"), tmtime.Now().Add(time.Hour*1)) + tApp.GetPriceFeedKeeper().SetCurrentPrices(ctx, "btc") // Run test function - err := k.liquidatorKeeper.partialSeizeCDP(ctx, addrs[0], "btc", i(2), i(10000)) + err := tApp.GetLiquidatorKeeper().PartialSeizeCDP(ctx, addrs[0], "btc", i(2), i(10000)) // Check require.NoError(t, err) - cdp, found := k.cdpKeeper.GetCDP(ctx, addrs[0], "btc") + cdp, found := tApp.GetCDPKeeper().GetCDP(ctx, addrs[0], "btc") require.True(t, found) require.Equal(t, i(1), cdp.CollateralAmount) require.Equal(t, i(6000), cdp.Debt) @@ -126,12 +105,13 @@ func TestKeeper_partialSeizeCDP(t *testing.T) { func TestKeeper_GetSetSeizedDebt(t *testing.T) { // Setup - ctx, k := setupTestKeepers() + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{}) debt := types.SeizedDebt{i(234247645), i(2343)} // Run test function - k.liquidatorKeeper.setSeizedDebt(ctx, debt) - readDebt := k.liquidatorKeeper.GetSeizedDebt(ctx) + tApp.GetLiquidatorKeeper().SetSeizedDebt(ctx, debt) + readDebt := tApp.GetLiquidatorKeeper().GetSeizedDebt(ctx) // Check require.Equal(t, debt, readDebt) diff --git a/x/liquidator/keeper/test_common.go b/x/liquidator/keeper/test_common.go deleted file mode 100644 index ab2d0a6b..00000000 --- a/x/liquidator/keeper/test_common.go +++ /dev/null @@ -1,171 +0,0 @@ -package keeper - -import ( - "time" - - "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" - tmtime "github.com/tendermint/tendermint/types/time" - 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(cdc, keyPriceFeed, paramsKeeper.Subspace(pricefeed.DefaultParamspace), 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.Params{ - Markets: []pricefeed.Market{ - pricefeed.Market{MarketID: "btc", BaseAsset: "btc", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}}, - } - return pricefeed.GenesisState{ - Params: ap, - PostedPrices: []pricefeed.PostedPrice{ - pricefeed.PostedPrice{ - MarketID: "btc", - OracleAddress: sdk.AccAddress{}, - Price: sdk.MustNewDecFromStr("8000.00"), - Expiry: tmtime.Now().Add(1 * time.Hour), - }, - }, - } -} From 83969f3702c3ee161e650d73a038e04cc0abc7ad Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 6 Dec 2019 23:53:20 +0000 Subject: [PATCH 17/99] update pricefeed tests --- x/pricefeed/genesis.go | 5 +- x/pricefeed/keeper/keeper_test.go | 108 ++++++++++++++---------------- x/pricefeed/keeper/test_common.go | 46 ------------- 3 files changed, 55 insertions(+), 104 deletions(-) delete mode 100644 x/pricefeed/keeper/test_common.go diff --git a/x/pricefeed/genesis.go b/x/pricefeed/genesis.go index 3bf16251..b80fe9a5 100644 --- a/x/pricefeed/genesis.go +++ b/x/pricefeed/genesis.go @@ -21,7 +21,10 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { // Set the current price (if any) based on what's now in the store for _, a := range data.Params.Markets { if a.Active { - _ = keeper.SetCurrentPrices(ctx, a.MarketID) + err := keeper.SetCurrentPrices(ctx, a.MarketID) + if err != nil { + panic(err) + } } } } diff --git a/x/pricefeed/keeper/keeper_test.go b/x/pricefeed/keeper/keeper_test.go index 105062bb..7c06172b 100644 --- a/x/pricefeed/keeper/keeper_test.go +++ b/x/pricefeed/keeper/keeper_test.go @@ -1,4 +1,4 @@ -package keeper +package keeper_test import ( "testing" @@ -7,31 +7,28 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" - tmtime "github.com/tendermint/tendermint/types/time" + "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/pricefeed/types" ) // TestKeeper_SetGetMarket tests adding markets to the pricefeed, getting markets from the store func TestKeeper_SetGetMarket(t *testing.T) { - helper := getMockApp(t, 0, types.GenesisState{}, nil) - header := abci.Header{ - Height: helper.mApp.LastBlockHeight() + 1, - Time: tmtime.Now()} - helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header}) - ctx := helper.mApp.BaseApp.NewContext(false, header) + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{}) + keeper := tApp.GetPriceFeedKeeper() mp := types.Params{ Markets: types.Markets{ types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, }, } - helper.keeper.SetParams(ctx, mp) - markets := helper.keeper.GetMarketParams(ctx) + keeper.SetParams(ctx, mp) + markets := keeper.GetMarketParams(ctx) require.Equal(t, len(markets), 1) require.Equal(t, markets[0].MarketID, "tstusd") - _, found := helper.keeper.GetMarket(ctx, "tstusd") + _, found := keeper.GetMarket(ctx, "tstusd") require.Equal(t, found, true) mp = types.Params{ @@ -40,102 +37,99 @@ func TestKeeper_SetGetMarket(t *testing.T) { types.Market{MarketID: "tst2usd", BaseAsset: "tst2", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, }, } - helper.keeper.SetParams(ctx, mp) - markets = helper.keeper.GetMarketParams(ctx) + keeper.SetParams(ctx, mp) + markets = keeper.GetMarketParams(ctx) require.Equal(t, len(markets), 2) require.Equal(t, markets[0].MarketID, "tstusd") require.Equal(t, markets[1].MarketID, "tst2usd") - _, found = helper.keeper.GetMarket(ctx, "nan") + _, found = keeper.GetMarket(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, types.GenesisState{}, nil) - header := abci.Header{ - Height: helper.mApp.LastBlockHeight() + 1, - Time: tmtime.Now()} - helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header}) - ctx := helper.mApp.BaseApp.NewContext(false, header) + _, addrs := app.GeneratePrivKeyAddressPairs(2) + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{}) + keeper := tApp.GetPriceFeedKeeper() + mp := types.Params{ Markets: types.Markets{ types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, }, } - helper.keeper.SetParams(ctx, mp) + keeper.SetParams(ctx, mp) // Set price by oracle 1 - _, err := helper.keeper.SetPrice( - ctx, helper.addrs[0], "tstusd", + _, err := keeper.SetPrice( + ctx, addrs[0], "tstusd", sdk.MustNewDecFromStr("0.33"), - header.Time.Add(1*time.Hour)) + time.Now().Add(1*time.Hour)) require.NoError(t, err) // Get raw prices - rawPrices := helper.keeper.GetRawPrices(ctx, "tstusd") + rawPrices := keeper.GetRawPrices(ctx, "tstusd") 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], "tstusd", + _, err = keeper.SetPrice( + ctx, addrs[1], "tstusd", sdk.MustNewDecFromStr("0.35"), - header.Time.Add(time.Hour*1)) + time.Now().Add(time.Hour*1)) require.NoError(t, err) - rawPrices = helper.keeper.GetRawPrices(ctx, "tstusd") + rawPrices = keeper.GetRawPrices(ctx, "tstusd") 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], "tstusd", + _, err = keeper.SetPrice( + ctx, addrs[0], "tstusd", sdk.MustNewDecFromStr("0.37"), - header.Time.Add(time.Hour*1)) + time.Now().Add(time.Hour*1)) require.NoError(t, err) - rawPrices = helper.keeper.GetRawPrices(ctx, "tstusd") + rawPrices = keeper.GetRawPrices(ctx, "tstusd") 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, types.GenesisState{}, nil) - header := abci.Header{ - Height: helper.mApp.LastBlockHeight() + 1, - Time: tmtime.Now()} - helper.mApp.BeginBlock(abci.RequestBeginBlock{Header: header}) - ctx := helper.mApp.BaseApp.NewContext(false, header) + _, addrs := app.GeneratePrivKeyAddressPairs(4) + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{}) + keeper := tApp.GetPriceFeedKeeper() + mp := types.Params{ Markets: types.Markets{ types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, }, } - helper.keeper.SetParams(ctx, mp) - helper.keeper.SetPrice( - ctx, helper.addrs[0], "tstusd", + keeper.SetParams(ctx, mp) + keeper.SetPrice( + ctx, addrs[0], "tstusd", sdk.MustNewDecFromStr("0.33"), - header.Time.Add(time.Hour*1)) - helper.keeper.SetPrice( - ctx, helper.addrs[1], "tstusd", + time.Now().Add(time.Hour*1)) + keeper.SetPrice( + ctx, addrs[1], "tstusd", sdk.MustNewDecFromStr("0.35"), - header.Time.Add(time.Hour*1)) - helper.keeper.SetPrice( - ctx, helper.addrs[2], "tstusd", + time.Now().Add(time.Hour*1)) + keeper.SetPrice( + ctx, addrs[2], "tstusd", sdk.MustNewDecFromStr("0.34"), - header.Time.Add(time.Hour*1)) + time.Now().Add(time.Hour*1)) // Set current price - err := helper.keeper.SetCurrentPrices(ctx, "tstusd") + err := keeper.SetCurrentPrices(ctx, "tstusd") require.NoError(t, err) // Get Current price - price := helper.keeper.GetCurrentPrice(ctx, "tstusd") + price := keeper.GetCurrentPrice(ctx, "tstusd") require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.34")), true) // Even number of oracles - helper.keeper.SetPrice( - ctx, helper.addrs[3], "tstusd", + keeper.SetPrice( + ctx, addrs[3], "tstusd", sdk.MustNewDecFromStr("0.36"), - header.Time.Add(time.Hour*1)) - err = helper.keeper.SetCurrentPrices(ctx, "tstusd") + time.Now().Add(time.Hour*1)) + err = keeper.SetCurrentPrices(ctx, "tstusd") require.NoError(t, err) - price = helper.keeper.GetCurrentPrice(ctx, "tstusd") + price = keeper.GetCurrentPrice(ctx, "tstusd") require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.345")), true) - } diff --git a/x/pricefeed/keeper/test_common.go b/x/pricefeed/keeper/test_common.go deleted file mode 100644 index 1f42c5ba..00000000 --- a/x/pricefeed/keeper/test_common.go +++ /dev/null @@ -1,46 +0,0 @@ -package keeper - -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" - "github.com/tendermint/tendermint/crypto" - - "github.com/kava-labs/kava/x/pricefeed/types" -) - -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 types.GenesisState, genAccs []authexported.Account) testHelper { - mApp := mock.NewApp() - types.RegisterCodec(mApp.Cdc) - keyPricefeed := sdk.NewKVStoreKey(types.StoreKey) - pk := mApp.ParamsKeeper - keeper := NewKeeper(mApp.Cdc, keyPricefeed, pk.Subspace(types.DefaultParamspace), types.DefaultCodespace) - - 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} -} From 2152bb4ed2775b0d5e9f23fa41487f324a87d9c5 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 7 Dec 2019 00:12:07 +0000 Subject: [PATCH 18/99] rename test helper func --- app/test_common.go | 26 ++++++++++++-------------- x/auction/abci_test.go | 2 +- x/auction/keeper/keeper_test.go | 6 +++--- x/cdp/app_test.go | 2 +- x/cdp/keeper/bank_test.go | 2 +- x/cdp/keeper/keeper_test.go | 4 ++-- x/liquidator/keeper/keeper_test.go | 4 ++-- 7 files changed, 22 insertions(+), 24 deletions(-) diff --git a/app/test_common.go b/app/test_common.go index 18004b69..f6a99036 100644 --- a/app/test_common.go +++ b/app/test_common.go @@ -44,7 +44,6 @@ import ( // So TestApp.InitializeFromGenesisStates() will call InitGenesis with the default genesis state. // and TestApp.InitializeFromGenesisStates(authState, cdpState) will do the same but overwrite the auth and cdp sections of the default genesis state // Creating the genesis states can be combersome, but helper methods can make it easier such as NewAuthGenStateFromAccounts below. -// type TestApp struct { App } @@ -71,19 +70,6 @@ func (tApp TestApp) GetCDPKeeper() cdp.Keeper { return tApp.cdpKee func (tApp TestApp) GetLiquidatorKeeper() liquidator.Keeper { return tApp.liquidatorKeeper } func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefeedKeeper } -// Create a new auth genesis state from some addresses and coins. The state is returned marshalled into a map. -// TODO make this not a TestApp method. -func (tApp TestApp) NewAuthGenStateFromAccounts(addresses []sdk.AccAddress, coins []sdk.Coins) GenesisState { - // Create GenAccounts - accounts := authexported.GenesisAccounts{} - for i := range addresses { - accounts = append(accounts, auth.NewBaseAccount(addresses[i], coins[i], nil, 0, 0)) - } - // Create the auth genesis state - authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts) - return GenesisState{auth.ModuleName: auth.ModuleCdc.MustMarshalJSON(authGenesis)} -} - // This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp { // Create a default genesis state and overwrite with provided values @@ -115,6 +101,18 @@ func (tApp TestApp) CheckBalance(t *testing.T, ctx sdk.Context, owner sdk.AccAdd require.Equal(t, expectedCoins, actualCoins) } +// Create a new auth genesis state from some addresses and coins. The state is returned marshalled into a map. +func NewAuthGenState(addresses []sdk.AccAddress, coins []sdk.Coins) GenesisState { + // Create GenAccounts + accounts := authexported.GenesisAccounts{} + for i := range addresses { + accounts = append(accounts, auth.NewBaseAccount(addresses[i], coins[i], nil, 0, 0)) + } + // Create the auth genesis state + authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts) + return GenesisState{auth.ModuleName: auth.ModuleCdc.MustMarshalJSON(authGenesis)} +} + // GeneratePrivKeyAddressPairsFromRand generates (deterministically) a total of n private keys and addresses. // TODO only generate secp256 keys? func GeneratePrivKeyAddressPairs(n int) (keys []crypto.PrivKey, addrs []sdk.AccAddress) { diff --git a/x/auction/abci_test.go b/x/auction/abci_test.go index 7d485335..73040006 100644 --- a/x/auction/abci_test.go +++ b/x/auction/abci_test.go @@ -17,7 +17,7 @@ func TestKeeper_EndBlocker(t *testing.T) { seller := addrs[0] tApp := app.NewTestApp() - authGenState := tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100))}) + authGenState := app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100))}) tApp.InitializeFromGenesisStates(authGenState) ctx := tApp.NewContext(true, abci.Header{}) diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go index 79aa9807..f6b8b613 100644 --- a/x/auction/keeper/keeper_test.go +++ b/x/auction/keeper/keeper_test.go @@ -19,7 +19,7 @@ func TestKeeper_ForwardAuction(t *testing.T) { buyer := addrs[1] tApp := app.NewTestApp() - authGenState := tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}) + authGenState := app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}) tApp.InitializeFromGenesisStates(authGenState) ctx := tApp.NewContext(false, abci.Header{}) @@ -52,7 +52,7 @@ func TestKeeper_ReverseAuction(t *testing.T) { buyer := addrs[1] tApp := app.NewTestApp() - authGenState := tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}) + authGenState := app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}) tApp.InitializeFromGenesisStates(authGenState) ctx := tApp.NewContext(false, abci.Header{}) @@ -86,7 +86,7 @@ func TestKeeper_ForwardReverseAuction(t *testing.T) { recipient := addrs[2] tApp := app.NewTestApp() - authGenState := tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}) + authGenState := app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}) tApp.InitializeFromGenesisStates(authGenState) ctx := tApp.NewContext(false, abci.Header{}) diff --git a/x/cdp/app_test.go b/x/cdp/app_test.go index db4883b8..09471330 100644 --- a/x/cdp/app_test.go +++ b/x/cdp/app_test.go @@ -20,7 +20,7 @@ func TestApp_CreateModifyDeleteCDP(t *testing.T) { testAddr := addrs[0] testPrivKey := privKeys[0] tApp.InitializeFromGenesisStates( - tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c("xrp", 100))}), + app.NewAuthGenState(addrs, []sdk.Coins{cs(c("xrp", 100))}), ) ctx := tApp.NewContext(false, abci.Header{}) // check balance diff --git a/x/cdp/keeper/bank_test.go b/x/cdp/keeper/bank_test.go index 4ddde8e2..4cf194db 100644 --- a/x/cdp/keeper/bank_test.go +++ b/x/cdp/keeper/bank_test.go @@ -35,7 +35,7 @@ func TestKeeper_AddSubtractGetCoins(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // setup app with an account tApp := app.NewTestApp() - authGenState := tApp.NewAuthGenStateFromAccounts([]sdk.AccAddress{normalAddr}, []sdk.Coins{cs(c("usdx", 100), c("kava", 100))}) + authGenState := app.NewAuthGenState([]sdk.AccAddress{normalAddr}, []sdk.Coins{cs(c("usdx", 100), c("kava", 100))}) tApp.InitializeFromGenesisStates(authGenState) // create a new context and setup the liquidator account diff --git a/x/cdp/keeper/keeper_test.go b/x/cdp/keeper/keeper_test.go index 7fd13c2c..d9381f2d 100644 --- a/x/cdp/keeper/keeper_test.go +++ b/x/cdp/keeper/keeper_test.go @@ -97,7 +97,7 @@ func TestKeeper_ModifyCDP(t *testing.T) { // setup keeper tApp := app.NewTestApp() // initialize cdp owner account with coins - authGen := tApp.NewAuthGenStateFromAccounts([]sdk.AccAddress{ownerAddr}, []sdk.Coins{tc.priorState.OwnerCoins}) + authGen := app.NewAuthGenState([]sdk.AccAddress{ownerAddr}, []sdk.Coins{tc.priorState.OwnerCoins}) tApp.InitializeFromGenesisStates(authGen) // create a context for db access ctx := tApp.NewContext(false, abci.Header{}) @@ -172,7 +172,7 @@ func TestKeeper_PartialSeizeCDP(t *testing.T) { testAddr := addrs[0] tApp := app.NewTestApp() - authGenState := tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c(collateral, 100))}) + authGenState := app.NewAuthGenState(addrs, []sdk.Coins{cs(c(collateral, 100))}) tApp.InitializeFromGenesisStates(authGenState) ctx := tApp.NewContext(false, abci.Header{}) diff --git a/x/liquidator/keeper/keeper_test.go b/x/liquidator/keeper/keeper_test.go index c952f11c..813ac6aa 100644 --- a/x/liquidator/keeper/keeper_test.go +++ b/x/liquidator/keeper/keeper_test.go @@ -19,7 +19,7 @@ func TestKeeper_SeizeAndStartCollateralAuction(t *testing.T) { tApp := app.NewTestApp() tApp.InitializeFromGenesisStates( - tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c("btc", 100))}), + app.NewAuthGenState(addrs, []sdk.Coins{cs(c("btc", 100))}), NewPFGenState("btc", sdk.MustNewDecFromStr("8000.00")), NewCDPGenState(), NewLiquidatorGenState(), @@ -80,7 +80,7 @@ func TestKeeper_partialSeizeCDP(t *testing.T) { tApp := app.NewTestApp() tApp.InitializeFromGenesisStates( - tApp.NewAuthGenStateFromAccounts(addrs, []sdk.Coins{cs(c("btc", 100))}), + app.NewAuthGenState(addrs, []sdk.Coins{cs(c("btc", 100))}), NewPFGenState("btc", sdk.MustNewDecFromStr("8000.00")), NewCDPGenState(), NewLiquidatorGenState(), From 81302ce29f303ad90f3af3781095e320e81a8e4b Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 7 Dec 2019 01:25:25 +0000 Subject: [PATCH 19/99] tidy up cdp tests --- x/cdp/app_test.go | 35 +--------- x/cdp/integration_test.go | 45 +++++++++++++ x/cdp/keeper/bank_test.go | 5 +- x/cdp/keeper/integration_test.go | 107 ++++++++++++++++++++++++------- x/cdp/keeper/keeper_test.go | 84 +++++++----------------- 5 files changed, 159 insertions(+), 117 deletions(-) diff --git a/x/cdp/app_test.go b/x/cdp/app_test.go index 09471330..a11758ee 100644 --- a/x/cdp/app_test.go +++ b/x/cdp/app_test.go @@ -2,7 +2,6 @@ package cdp_test import ( "testing" - "time" "github.com/cosmos/cosmos-sdk/simapp" sdk "github.com/cosmos/cosmos-sdk/types" @@ -10,7 +9,6 @@ import ( "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/cdp" - "github.com/kava-labs/kava/x/pricefeed" ) func TestApp_CreateModifyDeleteCDP(t *testing.T) { @@ -21,40 +19,13 @@ func TestApp_CreateModifyDeleteCDP(t *testing.T) { testPrivKey := privKeys[0] tApp.InitializeFromGenesisStates( app.NewAuthGenState(addrs, []sdk.Coins{cs(c("xrp", 100))}), + NewPFGenState("xrp", d("1.00")), + NewCDPGenState("xrp", d("1.5")), ) - ctx := tApp.NewContext(false, abci.Header{}) // check balance + ctx := tApp.NewContext(false, abci.Header{}) tApp.CheckBalance(t, ctx, testAddr, cs(c("xrp", 100))) - // setup cdp keeper - keeper := tApp.GetCDPKeeper() - params := cdp.CdpParams{ - GlobalDebtLimit: sdk.NewInt(100000), - CollateralParams: []cdp.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)) - pricefeedKeeper := tApp.GetPriceFeedKeeper() - ap := pricefeed.Params{ - Markets: []pricefeed.Market{ - pricefeed.Market{ - MarketID: "xrp", BaseAsset: "xrp", - QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, - }, - } - pricefeedKeeper.SetParams(ctx, ap) - pricefeedKeeper.SetPrice( - ctx, sdk.AccAddress{}, "xrp", - sdk.MustNewDecFromStr("1.00"), - time.Unix(9999999999, 0)) // some deterministic future date - pricefeedKeeper.SetCurrentPrices(ctx, "xrp") tApp.EndBlock(abci.RequestEndBlock{}) tApp.Commit() diff --git a/x/cdp/integration_test.go b/x/cdp/integration_test.go index 4ba15395..f3a301da 100644 --- a/x/cdp/integration_test.go +++ b/x/cdp/integration_test.go @@ -1,7 +1,13 @@ package cdp_test import ( + "time" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/cdp" + "github.com/kava-labs/kava/x/pricefeed" + "github.com/kava-labs/kava/app" ) // Avoid cluttering test cases with long function name @@ -9,3 +15,42 @@ 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 NewPFGenState(asset string, price sdk.Dec) app.GenesisState { + quote := "usd" + ap := pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{MarketID: asset, BaseAsset: asset, QuoteAsset: quote, Oracles: pricefeed.Oracles{}, Active: true}, + }, + } + pfGenesis := pricefeed.GenesisState{ + Params: ap, + PostedPrices: []pricefeed.PostedPrice{ + pricefeed.PostedPrice{ + MarketID: asset, + OracleAddress: sdk.AccAddress{}, + Price: price, + Expiry: time.Unix(9999999999, 0), // some deterministic future date, + }, + }, + } + return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)} +} + +func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { + cdpGenesis := cdp.GenesisState{ + Params: cdp.CdpParams{ + GlobalDebtLimit: sdk.NewInt(1000000), + CollateralParams: []cdp.CollateralParams{ + { + Denom: asset, + LiquidationRatio: liquidationRatio, + DebtLimit: sdk.NewInt(500000), + }, + }, + }, + GlobalDebt: sdk.ZeroInt(), + CDPs: cdp.CDPs{}, + } + return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} +} \ No newline at end of file diff --git a/x/cdp/keeper/bank_test.go b/x/cdp/keeper/bank_test.go index 4cf194db..3e13b4b4 100644 --- a/x/cdp/keeper/bank_test.go +++ b/x/cdp/keeper/bank_test.go @@ -35,8 +35,9 @@ func TestKeeper_AddSubtractGetCoins(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // setup app with an account tApp := app.NewTestApp() - authGenState := app.NewAuthGenState([]sdk.AccAddress{normalAddr}, []sdk.Coins{cs(c("usdx", 100), c("kava", 100))}) - tApp.InitializeFromGenesisStates(authGenState) + tApp.InitializeFromGenesisStates( + app.NewAuthGenState([]sdk.AccAddress{normalAddr}, []sdk.Coins{cs(c("usdx", 100), c("kava", 100))}), + ) // create a new context and setup the liquidator account ctx := tApp.NewContext(false, abci.Header{}) diff --git a/x/cdp/keeper/integration_test.go b/x/cdp/keeper/integration_test.go index 0a1d6fc6..0e465b85 100644 --- a/x/cdp/keeper/integration_test.go +++ b/x/cdp/keeper/integration_test.go @@ -1,9 +1,14 @@ package keeper_test import ( + "time" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/cdp" "github.com/kava-labs/kava/x/cdp/types" + "github.com/kava-labs/kava/x/pricefeed" ) // Avoid cluttering test cases with long function name @@ -12,35 +17,91 @@ 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), +func NewPFGenState(asset string, price sdk.Dec) app.GenesisState { + quote := "usd" + ap := pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{MarketID: asset, BaseAsset: asset, QuoteAsset: quote, Oracles: pricefeed.Oracles{}, Active: true}, + }, + } + pfGenesis := pricefeed.GenesisState{ + Params: ap, + PostedPrices: []pricefeed.PostedPrice{ + pricefeed.PostedPrice{ + MarketID: asset, + OracleAddress: sdk.AccAddress{}, + Price: price, + Expiry: time.Unix(9999999999, 0), // some deterministic future date, }, }, - StableDenoms: []string{"usdx"}, } + return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)} } -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), +func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { + cdpGenesis := cdp.GenesisState{ + Params: cdp.CdpParams{ + GlobalDebtLimit: sdk.NewInt(1000000), + CollateralParams: []cdp.CollateralParams{ + { + Denom: asset, + LiquidationRatio: liquidationRatio, + DebtLimit: sdk.NewInt(500000), + }, }, }, - StableDenoms: []string{"usdx"}, + GlobalDebt: sdk.ZeroInt(), + CDPs: cdp.CDPs{}, } + return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} +} + +func NewPFGenStateMulti() app.GenesisState { + quote := "usd" + ap := pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{MarketID: "btc", BaseAsset: "btc", QuoteAsset: quote, Oracles: pricefeed.Oracles{}, Active: true}, + pricefeed.Market{MarketID: "xrp", BaseAsset: "xrp", QuoteAsset: quote, Oracles: pricefeed.Oracles{}, Active: true}, + }, + } + pfGenesis := pricefeed.GenesisState{ + Params: ap, + PostedPrices: []pricefeed.PostedPrice{ + pricefeed.PostedPrice{ + MarketID: "btc", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("8000.00"), + Expiry: time.Unix(9999999999, 0), // some deterministic future date, + }, + pricefeed.PostedPrice{ + MarketID: "xrp", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("0.25"), + Expiry: time.Unix(9999999999, 0), // some deterministic future date, + }, + }, + } + return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)} +} +func NewCDPGenStateMulti() app.GenesisState { + cdpGenesis := cdp.GenesisState{ + Params: cdp.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), + }, + }, + }, + GlobalDebt: sdk.ZeroInt(), + CDPs: cdp.CDPs{}, + } + return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} } diff --git a/x/cdp/keeper/keeper_test.go b/x/cdp/keeper/keeper_test.go index d9381f2d..bded6ff8 100644 --- a/x/cdp/keeper/keeper_test.go +++ b/x/cdp/keeper/keeper_test.go @@ -11,7 +11,6 @@ import ( "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/cdp/types" - "github.com/kava-labs/kava/x/pricefeed" ) // 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? @@ -94,41 +93,19 @@ func TestKeeper_ModifyCDP(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - // setup keeper + // setup test app tApp := app.NewTestApp() - // initialize cdp owner account with coins - authGen := app.NewAuthGenState([]sdk.AccAddress{ownerAddr}, []sdk.Coins{tc.priorState.OwnerCoins}) - tApp.InitializeFromGenesisStates(authGen) + // initialize cdp owner account with coins, and collateral with price and params + tApp.InitializeFromGenesisStates( + app.NewAuthGenState([]sdk.AccAddress{ownerAddr}, []sdk.Coins{tc.priorState.OwnerCoins}), + NewPFGenState("xrp", d(tc.price)), + NewCDPGenState("xrp", d("2.0")), + ) // create a context for db access ctx := tApp.NewContext(false, abci.Header{}) // setup store state keeper := tApp.GetCDPKeeper() - keeper.SetParams(ctx, defaultParamsSingle()) - pricefeedKeeper := tApp.GetPriceFeedKeeper() - ap := pricefeed.Params{ - Markets: []pricefeed.Market{ - pricefeed.Market{ - MarketID: "xrp", BaseAsset: "xrp", - QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, - }, - } - pricefeedKeeper.SetParams(ctx, ap) - _, err := pricefeedKeeper.SetPrice( - ctx, ownerAddr, "xrp", - sdk.MustNewDecFromStr(tc.price), - time.Unix(9999999999, 0)) // some deterministic future date - if err != nil { - t.Log("test context height", ctx.BlockHeight()) - t.Log(err) - t.Log(tc.name) - } - err = pricefeedKeeper.SetCurrentPrices(ctx, "xrp") - 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) } @@ -138,7 +115,7 @@ func TestKeeper_ModifyCDP(t *testing.T) { } // call func under test - err = keeper.ModifyCDP(ctx, tc.args.owner, tc.args.collateralDenom, tc.args.changeInCollateral, tc.args.changeInDebt) + err := keeper.ModifyCDP(ctx, tc.args.owner, tc.args.collateralDenom, tc.args.changeInCollateral, tc.args.changeInDebt) // get new state for verification actualCDP, found := keeper.GetCDP(ctx, tc.args.owner, tc.args.collateralDenom) @@ -172,38 +149,23 @@ func TestKeeper_PartialSeizeCDP(t *testing.T) { testAddr := addrs[0] tApp := app.NewTestApp() - authGenState := app.NewAuthGenState(addrs, []sdk.Coins{cs(c(collateral, 100))}) - tApp.InitializeFromGenesisStates(authGenState) + tApp.InitializeFromGenesisStates( + app.NewAuthGenState(addrs, []sdk.Coins{cs(c(collateral, 100))}), + NewPFGenState(collateral, d("1.00")), + NewCDPGenState(collateral, d("2.00")), + ) ctx := tApp.NewContext(false, abci.Header{}) keeper := tApp.GetCDPKeeper() - // setup pricefeed - pricefeedKeeper := tApp.GetPriceFeedKeeper() - ap := pricefeed.Params{ - Markets: []pricefeed.Market{ - pricefeed.Market{ - MarketID: "xrp", BaseAsset: collateral, - QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, - }, - } - pricefeedKeeper.SetParams(ctx, ap) - pricefeedKeeper.SetPrice( - ctx, sdk.AccAddress{}, collateral, - sdk.MustNewDecFromStr("1.00"), - time.Unix(9999999999, 0)) // some deterministic future date - pricefeedKeeper.SetCurrentPrices(ctx, collateral) - // Create CDP - keeper.SetParams(ctx, defaultParamsSingle()) err := keeper.ModifyCDP(ctx, testAddr, collateral, i(10), i(5)) require.NoError(t, err) // Reduce price - pricefeedKeeper.SetPrice( + tApp.GetPriceFeedKeeper().SetPrice( ctx, sdk.AccAddress{}, collateral, - sdk.MustNewDecFromStr("0.90"), - time.Unix(9999999999, 0)) // some deterministic future date - pricefeedKeeper.SetCurrentPrices(ctx, collateral) + d("0.90"), time.Unix(9999999999, 0)) // some deterministic future date + tApp.GetPriceFeedKeeper().SetCurrentPrices(ctx, collateral) // Seize entire CDP err = keeper.PartialSeizeCDP(ctx, testAddr, collateral, i(10), i(5)) @@ -218,11 +180,14 @@ func TestKeeper_PartialSeizeCDP(t *testing.T) { } func TestKeeper_GetCDPs(t *testing.T) { - // setup keeper - tApp := app.NewTestApp() + // setup test app + tApp := app.NewTestApp().InitializeFromGenesisStates( + NewPFGenStateMulti(), // collateral needs to be in pricefeed for cdp InitGenesis to validate + NewCDPGenStateMulti(), + ) ctx := tApp.NewContext(true, abci.Header{}) keeper := tApp.GetCDPKeeper() - keeper.SetParams(ctx, defaultParamsMulti()) + // setup CDPs _, addrs := app.GeneratePrivKeyAddressPairs(2) cdps := types.CDPs{ @@ -299,7 +264,6 @@ func TestKeeper_GetSetDeleteCDP(t *testing.T) { ctx := tApp.NewContext(true, abci.Header{}) keeper := tApp.GetCDPKeeper() - keeper.SetParams(ctx, defaultParamsSingle()) _, addrs := app.GeneratePrivKeyAddressPairs(1) cdp := types.CDP{addrs[0], "xrp", i(412), i(56)} @@ -323,7 +287,7 @@ func TestKeeper_GetSetGDebt(t *testing.T) { tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{}) keeper := tApp.GetCDPKeeper() - keeper.SetParams(ctx, defaultParamsSingle()) + gDebt := i(4120000) // write and read from store @@ -339,7 +303,7 @@ func TestKeeper_GetSetCollateralState(t *testing.T) { tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{}) keeper := tApp.GetCDPKeeper() - keeper.SetParams(ctx, defaultParamsSingle()) + collateralState := types.CollateralState{"xrp", i(15400)} // write and read from store From 11fd42685b6944488ffe8f474076fb7a00b4667c Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 7 Dec 2019 01:25:45 +0000 Subject: [PATCH 20/99] minor tidying --- x/auction/abci_test.go | 5 +++-- x/auction/keeper/keeper_test.go | 15 +++++++++------ x/liquidator/keeper/integration_test.go | 1 + x/liquidator/keeper/keeper_test.go | 8 ++++---- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/x/auction/abci_test.go b/x/auction/abci_test.go index 73040006..38941aeb 100644 --- a/x/auction/abci_test.go +++ b/x/auction/abci_test.go @@ -17,8 +17,9 @@ func TestKeeper_EndBlocker(t *testing.T) { seller := addrs[0] tApp := app.NewTestApp() - authGenState := app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100))}) - tApp.InitializeFromGenesisStates(authGenState) + tApp.InitializeFromGenesisStates( + app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100))}), + ) ctx := tApp.NewContext(true, abci.Header{}) keeper := tApp.GetAuctionKeeper() diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go index f6b8b613..873dcb81 100644 --- a/x/auction/keeper/keeper_test.go +++ b/x/auction/keeper/keeper_test.go @@ -19,8 +19,9 @@ func TestKeeper_ForwardAuction(t *testing.T) { buyer := addrs[1] tApp := app.NewTestApp() - authGenState := app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}) - tApp.InitializeFromGenesisStates(authGenState) + tApp.InitializeFromGenesisStates( + app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}), + ) ctx := tApp.NewContext(false, abci.Header{}) keeper := tApp.GetAuctionKeeper() @@ -52,8 +53,9 @@ func TestKeeper_ReverseAuction(t *testing.T) { buyer := addrs[1] tApp := app.NewTestApp() - authGenState := app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}) - tApp.InitializeFromGenesisStates(authGenState) + tApp.InitializeFromGenesisStates( + app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}), + ) ctx := tApp.NewContext(false, abci.Header{}) keeper := tApp.GetAuctionKeeper() @@ -86,8 +88,9 @@ func TestKeeper_ForwardReverseAuction(t *testing.T) { recipient := addrs[2] tApp := app.NewTestApp() - authGenState := app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}) - tApp.InitializeFromGenesisStates(authGenState) + tApp.InitializeFromGenesisStates( + app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}), + ) ctx := tApp.NewContext(false, abci.Header{}) keeper := tApp.GetAuctionKeeper() diff --git a/x/liquidator/keeper/integration_test.go b/x/liquidator/keeper/integration_test.go index 53151269..01c825f8 100644 --- a/x/liquidator/keeper/integration_test.go +++ b/x/liquidator/keeper/integration_test.go @@ -14,6 +14,7 @@ import ( // 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...) } diff --git a/x/liquidator/keeper/keeper_test.go b/x/liquidator/keeper/keeper_test.go index 813ac6aa..3dae54fe 100644 --- a/x/liquidator/keeper/keeper_test.go +++ b/x/liquidator/keeper/keeper_test.go @@ -20,7 +20,7 @@ func TestKeeper_SeizeAndStartCollateralAuction(t *testing.T) { tApp := app.NewTestApp() tApp.InitializeFromGenesisStates( app.NewAuthGenState(addrs, []sdk.Coins{cs(c("btc", 100))}), - NewPFGenState("btc", sdk.MustNewDecFromStr("8000.00")), + NewPFGenState("btc", d("8000.00")), NewCDPGenState(), NewLiquidatorGenState(), ) @@ -28,7 +28,7 @@ func TestKeeper_SeizeAndStartCollateralAuction(t *testing.T) { require.NoError(t, tApp.GetCDPKeeper().ModifyCDP(ctx, addrs[0], "btc", i(3), i(16000))) - _, err := tApp.GetPriceFeedKeeper().SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("7999.99"), time.Unix(9999999999, 0)) + _, err := tApp.GetPriceFeedKeeper().SetPrice(ctx, addrs[0], "btc", d("7999.99"), time.Unix(9999999999, 0)) require.NoError(t, err) require.NoError(t, tApp.GetPriceFeedKeeper().SetCurrentPrices(ctx, "btc")) @@ -81,7 +81,7 @@ func TestKeeper_partialSeizeCDP(t *testing.T) { tApp := app.NewTestApp() tApp.InitializeFromGenesisStates( app.NewAuthGenState(addrs, []sdk.Coins{cs(c("btc", 100))}), - NewPFGenState("btc", sdk.MustNewDecFromStr("8000.00")), + NewPFGenState("btc", d("8000.00")), NewCDPGenState(), NewLiquidatorGenState(), ) @@ -89,7 +89,7 @@ func TestKeeper_partialSeizeCDP(t *testing.T) { tApp.GetCDPKeeper().ModifyCDP(ctx, addrs[0], "btc", i(3), i(16000)) - tApp.GetPriceFeedKeeper().SetPrice(ctx, addrs[0], "btc", sdk.MustNewDecFromStr("7999.99"), tmtime.Now().Add(time.Hour*1)) + tApp.GetPriceFeedKeeper().SetPrice(ctx, addrs[0], "btc", d("7999.99"), tmtime.Now().Add(time.Hour*1)) tApp.GetPriceFeedKeeper().SetCurrentPrices(ctx, "btc") // Run test function From d8347fd49c241a2f6388980d1f85748d7486d27b Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 7 Dec 2019 14:56:40 +0000 Subject: [PATCH 21/99] tidy test genesis states --- x/cdp/keeper/integration_test.go | 30 +++++++++++-------------- x/cdp/keeper/keeper_test.go | 6 ++--- x/liquidator/keeper/integration_test.go | 14 +++++------- x/liquidator/keeper/keeper_test.go | 4 ++-- 4 files changed, 24 insertions(+), 30 deletions(-) diff --git a/x/cdp/keeper/integration_test.go b/x/cdp/keeper/integration_test.go index 0e465b85..af8ad4c6 100644 --- a/x/cdp/keeper/integration_test.go +++ b/x/cdp/keeper/integration_test.go @@ -17,15 +17,13 @@ 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 NewPFGenState(asset string, price sdk.Dec) app.GenesisState { - quote := "usd" - ap := pricefeed.Params{ - Markets: []pricefeed.Market{ - pricefeed.Market{MarketID: asset, BaseAsset: asset, QuoteAsset: quote, Oracles: pricefeed.Oracles{}, Active: true}, - }, - } +func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { pfGenesis := pricefeed.GenesisState{ - Params: ap, + Params: pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{MarketID: asset, BaseAsset: asset, QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, + }, + }, PostedPrices: []pricefeed.PostedPrice{ pricefeed.PostedPrice{ MarketID: asset, @@ -56,16 +54,14 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} } -func NewPFGenStateMulti() app.GenesisState { - quote := "usd" - ap := pricefeed.Params{ - Markets: []pricefeed.Market{ - pricefeed.Market{MarketID: "btc", BaseAsset: "btc", QuoteAsset: quote, Oracles: pricefeed.Oracles{}, Active: true}, - pricefeed.Market{MarketID: "xrp", BaseAsset: "xrp", QuoteAsset: quote, Oracles: pricefeed.Oracles{}, Active: true}, - }, - } +func NewPricefeedGenStateMulti() app.GenesisState { pfGenesis := pricefeed.GenesisState{ - Params: ap, + Params: pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{MarketID: "btc", BaseAsset: "btc", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, + pricefeed.Market{MarketID: "xrp", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, + }, + }, PostedPrices: []pricefeed.PostedPrice{ pricefeed.PostedPrice{ MarketID: "btc", diff --git a/x/cdp/keeper/keeper_test.go b/x/cdp/keeper/keeper_test.go index bded6ff8..33983c85 100644 --- a/x/cdp/keeper/keeper_test.go +++ b/x/cdp/keeper/keeper_test.go @@ -98,7 +98,7 @@ func TestKeeper_ModifyCDP(t *testing.T) { // initialize cdp owner account with coins, and collateral with price and params tApp.InitializeFromGenesisStates( app.NewAuthGenState([]sdk.AccAddress{ownerAddr}, []sdk.Coins{tc.priorState.OwnerCoins}), - NewPFGenState("xrp", d(tc.price)), + NewPricefeedGenState("xrp", d(tc.price)), NewCDPGenState("xrp", d("2.0")), ) // create a context for db access @@ -151,7 +151,7 @@ func TestKeeper_PartialSeizeCDP(t *testing.T) { tApp := app.NewTestApp() tApp.InitializeFromGenesisStates( app.NewAuthGenState(addrs, []sdk.Coins{cs(c(collateral, 100))}), - NewPFGenState(collateral, d("1.00")), + NewPricefeedGenState(collateral, d("1.00")), NewCDPGenState(collateral, d("2.00")), ) @@ -182,7 +182,7 @@ func TestKeeper_PartialSeizeCDP(t *testing.T) { func TestKeeper_GetCDPs(t *testing.T) { // setup test app tApp := app.NewTestApp().InitializeFromGenesisStates( - NewPFGenStateMulti(), // collateral needs to be in pricefeed for cdp InitGenesis to validate + NewPricefeedGenStateMulti(), // collateral needs to be in pricefeed for cdp InitGenesis to validate NewCDPGenStateMulti(), ) ctx := tApp.NewContext(true, abci.Header{}) diff --git a/x/liquidator/keeper/integration_test.go b/x/liquidator/keeper/integration_test.go index 01c825f8..32f57bbf 100644 --- a/x/liquidator/keeper/integration_test.go +++ b/x/liquidator/keeper/integration_test.go @@ -18,16 +18,14 @@ 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...) } -// Default genesis states to initialize test apps +// Genesis states to initialize test apps -func NewPFGenState(asset string, price sdk.Dec) app.GenesisState { - quote := "usd" - ap := pricefeed.Params{ - Markets: []pricefeed.Market{ - pricefeed.Market{MarketID: asset, BaseAsset: asset, QuoteAsset: quote, Oracles: pricefeed.Oracles{}, Active: true}}, - } +func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { pfGenesis := pricefeed.GenesisState{ - Params: ap, + Params: pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{MarketID: asset, BaseAsset: asset, QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}}, + }, PostedPrices: []pricefeed.PostedPrice{ pricefeed.PostedPrice{ MarketID: asset, diff --git a/x/liquidator/keeper/keeper_test.go b/x/liquidator/keeper/keeper_test.go index 3dae54fe..c6598f06 100644 --- a/x/liquidator/keeper/keeper_test.go +++ b/x/liquidator/keeper/keeper_test.go @@ -20,7 +20,7 @@ func TestKeeper_SeizeAndStartCollateralAuction(t *testing.T) { tApp := app.NewTestApp() tApp.InitializeFromGenesisStates( app.NewAuthGenState(addrs, []sdk.Coins{cs(c("btc", 100))}), - NewPFGenState("btc", d("8000.00")), + NewPricefeedGenState("btc", d("8000.00")), NewCDPGenState(), NewLiquidatorGenState(), ) @@ -81,7 +81,7 @@ func TestKeeper_partialSeizeCDP(t *testing.T) { tApp := app.NewTestApp() tApp.InitializeFromGenesisStates( app.NewAuthGenState(addrs, []sdk.Coins{cs(c("btc", 100))}), - NewPFGenState("btc", d("8000.00")), + NewPricefeedGenState("btc", d("8000.00")), NewCDPGenState(), NewLiquidatorGenState(), ) From f794ba1bf9e3ce56164b14d4edf8fcb36518ccf7 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 7 Dec 2019 15:08:07 +0000 Subject: [PATCH 22/99] use time consistently in tests --- x/cdp/integration_test.go | 6 +++--- x/cdp/keeper/integration_test.go | 6 +++--- x/cdp/keeper/keeper_test.go | 2 +- x/liquidator/keeper/integration_test.go | 2 +- x/liquidator/keeper/keeper_test.go | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/x/cdp/integration_test.go b/x/cdp/integration_test.go index f3a301da..abf674fb 100644 --- a/x/cdp/integration_test.go +++ b/x/cdp/integration_test.go @@ -5,9 +5,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/cdp" "github.com/kava-labs/kava/x/pricefeed" - "github.com/kava-labs/kava/app" ) // Avoid cluttering test cases with long function name @@ -30,7 +30,7 @@ func NewPFGenState(asset string, price sdk.Dec) app.GenesisState { MarketID: asset, OracleAddress: sdk.AccAddress{}, Price: price, - Expiry: time.Unix(9999999999, 0), // some deterministic future date, + Expiry: time.Now().Add(1 * time.Hour), }, }, } @@ -53,4 +53,4 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { CDPs: cdp.CDPs{}, } return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} -} \ No newline at end of file +} diff --git a/x/cdp/keeper/integration_test.go b/x/cdp/keeper/integration_test.go index af8ad4c6..23fd6722 100644 --- a/x/cdp/keeper/integration_test.go +++ b/x/cdp/keeper/integration_test.go @@ -29,7 +29,7 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { MarketID: asset, OracleAddress: sdk.AccAddress{}, Price: price, - Expiry: time.Unix(9999999999, 0), // some deterministic future date, + Expiry: time.Now().Add(1 * time.Hour), }, }, } @@ -67,13 +67,13 @@ func NewPricefeedGenStateMulti() app.GenesisState { MarketID: "btc", OracleAddress: sdk.AccAddress{}, Price: sdk.MustNewDecFromStr("8000.00"), - Expiry: time.Unix(9999999999, 0), // some deterministic future date, + Expiry: time.Now().Add(1 * time.Hour), }, pricefeed.PostedPrice{ MarketID: "xrp", OracleAddress: sdk.AccAddress{}, Price: sdk.MustNewDecFromStr("0.25"), - Expiry: time.Unix(9999999999, 0), // some deterministic future date, + Expiry: time.Now().Add(1 * time.Hour), }, }, } diff --git a/x/cdp/keeper/keeper_test.go b/x/cdp/keeper/keeper_test.go index 33983c85..35f2b696 100644 --- a/x/cdp/keeper/keeper_test.go +++ b/x/cdp/keeper/keeper_test.go @@ -164,7 +164,7 @@ func TestKeeper_PartialSeizeCDP(t *testing.T) { // Reduce price tApp.GetPriceFeedKeeper().SetPrice( ctx, sdk.AccAddress{}, collateral, - d("0.90"), time.Unix(9999999999, 0)) // some deterministic future date + d("0.90"), time.Now().Add(1*time.Hour)) tApp.GetPriceFeedKeeper().SetCurrentPrices(ctx, collateral) // Seize entire CDP diff --git a/x/liquidator/keeper/integration_test.go b/x/liquidator/keeper/integration_test.go index 32f57bbf..53f936ac 100644 --- a/x/liquidator/keeper/integration_test.go +++ b/x/liquidator/keeper/integration_test.go @@ -31,7 +31,7 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { MarketID: asset, OracleAddress: sdk.AccAddress{}, Price: price, - Expiry: time.Unix(9999999999, 0), // some deterministic future date, + Expiry: time.Now().Add(1 * time.Hour), }, }, } diff --git a/x/liquidator/keeper/keeper_test.go b/x/liquidator/keeper/keeper_test.go index c6598f06..4541f3da 100644 --- a/x/liquidator/keeper/keeper_test.go +++ b/x/liquidator/keeper/keeper_test.go @@ -28,7 +28,7 @@ func TestKeeper_SeizeAndStartCollateralAuction(t *testing.T) { require.NoError(t, tApp.GetCDPKeeper().ModifyCDP(ctx, addrs[0], "btc", i(3), i(16000))) - _, err := tApp.GetPriceFeedKeeper().SetPrice(ctx, addrs[0], "btc", d("7999.99"), time.Unix(9999999999, 0)) + _, err := tApp.GetPriceFeedKeeper().SetPrice(ctx, addrs[0], "btc", d("7999.99"), time.Now().Add(1*time.Hour)) require.NoError(t, err) require.NoError(t, tApp.GetPriceFeedKeeper().SetCurrentPrices(ctx, "btc")) From d8a428e1d84caff415700a02e6235b5321e8288f Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Wed, 11 Dec 2019 22:59:06 +0000 Subject: [PATCH 23/99] rough auction type refactor --- x/auction/alias.go | 1 - x/auction/keeper/keeper.go | 288 +++++++++++++++++++++++----- x/auction/types/auctions.go | 286 +++++++++++++-------------- x/auction/types/expected_keepers.go | 17 +- 4 files changed, 381 insertions(+), 211 deletions(-) diff --git a/x/auction/alias.go b/x/auction/alias.go index 58040464..9f86f9e7 100644 --- a/x/auction/alias.go +++ b/x/auction/alias.go @@ -24,7 +24,6 @@ const ( var ( // functions aliases NewIDFromString = types.NewIDFromString - NewBaseAuction = types.NewBaseAuction NewForwardAuction = types.NewForwardAuction NewReverseAuction = types.NewReverseAuction NewForwardReverseAuction = types.NewForwardReverseAuction diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index f43f81cc..08d49a2f 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -7,11 +7,13 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/params/subspace" + "github.com/cosmos/cosmos-sdk/x/supply" + "github.com/kava-labs/kava/x/auction/types" ) type Keeper struct { - bankKeeper types.BankKeeper + supplyKeeper types.SupplyKeeper storeKey sdk.StoreKey cdc *codec.Codec paramSubspace subspace.Subspace @@ -19,23 +21,27 @@ type Keeper struct { } // NewKeeper returns a new auction keeper. -func NewKeeper(cdc *codec.Codec, bankKeeper types.BankKeeper, storeKey sdk.StoreKey, paramstore subspace.Subspace) Keeper { +func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, bankKeeper types.BankKeeper, supplyKeeper types.SupplyKeeper, paramstore subspace.Subspace) Keeper { return Keeper{ - bankKeeper: bankKeeper, + supplyKeeper: supplyKeeper, 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) { +func (k Keeper) StartForwardAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (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) + auction := types.NewForwardAuction(seller, lot, bidDenom, types.EndTime(ctx.BlockHeight())+types.DefaultMaxAuctionDuration) + + // take coins from module account + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) + if err != nil { + return 0, err + } + // store the auction + auctionID, err := k.storeNewAuction(ctx, auction) // TODO does this need to be a pointer to satisfy the interface if err != nil { return 0, err } @@ -43,11 +49,17 @@ func (k Keeper) StartForwardAuction(ctx sdk.Context, seller sdk.AccAddress, lot } // 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) { +func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer string, 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) + auction := types.NewReverseAuction(buyer, bid, initialLot, types.EndTime(ctx.BlockHeight())+types.DefaultMaxAuctionDuration) + + // This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker. + macc := k.supplyKeeper.GetModuleAccount(ctx, buyer) + if !macc.HasPermission(supply.Minter) { // TODO ideally don't want to import supply + return 0, sdk.ErrInternal("module does not have minting permissions") + } + // store the auction + auctionID, err := k.storeNewAuction(ctx, &auction) if err != nil { return 0, err } @@ -55,19 +67,25 @@ func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer sdk.AccAddress, bid s } // 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) { +func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller string, 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) + auction := types.NewForwardReverseAuction(seller, lot, types.EndTime(ctx.BlockHeight())+types.DefaultMaxAuctionDuration, maxBid, otherPerson) + + // take coins from module account + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.Coins{lot}) + if err != nil { + return 0, err + } + // store the auction + auctionID, err := k.storeNewAuction(ctx, &auction) 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) { +// set an auction in the store, adding a new ID, and setting indexes +func (k Keeper) storeNewAuction(ctx sdk.Context, auction types.Auction) (types.ID, sdk.Error) { // get ID newAuctionID, err := k.getNextAuctionID(ctx) if err != nil { @@ -76,18 +94,14 @@ func (k Keeper) startAuction(ctx sdk.Context, auction types.Auction, initiatorOu // 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 { @@ -97,35 +111,179 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID types.ID, bidder sdk.AccAddr 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 + // check end time + if ctx.BlockHeight() > auction.GetEndTime() { + return sdk.ErrInternal("auction has closed") } - // 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 + + var err sdk.Error + var a types.Auction + switch auc := auction.(type) { + case types.ForwardAuction: + a, err = k.PlaceBidForward(ctx, auc, bidder, bid) if err != nil { - panic(err) + return err } - } - // add inputs - for _, input := range coinInputs { - _, err = k.bankKeeper.AddCoins(ctx, input.Address, sdk.NewCoins(input.Coin)) // TODO errors + case types.ReverseAuction: + a, err = k.PlaceBidReverse(ctx, auc, bidder, lot) if err != nil { - panic(err) + return err } + case types.ForwardReverseAuction: + a, err = k.PlaceBidForwardReverse(ctx, auc, bidder, bid, lot) + if err != nil { + return err + } + default: + panic("unrecognized auction type") } // store updated auction - k.SetAuction(ctx, auction) + k.SetAuction(ctx, a) // maybe move into above funcs 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) PlaceBidForward(ctx sdk.Context, a types.ForwardAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.ForwardAuction, sdk.Error) { + // Valid New Bid + if bid.Denom != a.Bid.Denom { + return a, sdk.ErrInternal("bid denom doesn't match auction") + } + if !a.Bid.IsLT(bid) { // TODO add minimum bid size + return a, sdk.ErrInternal("bid not greater than last bid") + } + + // Move Coins + increment := bid.Sub(a.Bid) + bidAmtToReturn := a.Bid + if bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance + bidAmtToReturn = sdk.NewInt64Coin(a.Bid.Denom, 0) + } + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(bidAmtToReturn.Add(increment))) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, bidder, sdk.NewCoins(bidAmtToReturn)) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(increment)) // increase in bid size is burned + if err != nil { + return a, err + } + err = k.supplyKeeper.BurnCoins(ctx, a.Initiator, sdk.NewCoins(increment)) + if err != nil { + return a, err + } + + // Update Auction + a.Bidder = bidder + a.Bid = bid + // increment timeout + a.EndTime = EndTime(min(int64(ctx.BlockHeight()+types.DefaultMaxBidDuration), int64(a.MaxEndTime))) + + return a, nil +} +func (k Keeper) PlaceBidForwardReverse(ctx sdk.Context, a types.ForwardReverseAuction, bidder sdk.AccAddress, bid sdk.Coin, lot sdk.Coin) (types.ForwardReverseAuction, sdk.Error) { + // Validate New Bid // TODO min bid increments, make validation code less confusing + if !a.Bid.IsEqual(a.MaxBid) { + // Auction is in forward phase, a bid here can put the auction into forward or reverse phases + if !a.Bid.IsLT(bid) { + return a, sdk.ErrInternal("auction in forward phase, new bid not higher than last bid") + } + if a.MaxBid.IsLT(bid) { + return a, sdk.ErrInternal("bid higher than max bid") + } + if lot.IsNegative() || a.Lot.IsLT(lot) { + return a, sdk.ErrInternal("lot out of bounds") + } + if lot.IsLT(a.Lot) && !bid.IsEqual(a.MaxBid) { + return a, sdk.ErrInternal("auction cannot enter reverse phase without bidding max bid") + } + } else { + // Auction is in reverse phase, it can never leave reverse phase + if !bid.IsEqual(a.MaxBid) { + return a, sdk.ErrInternal("") // not necessary + } + if lot.IsNegative() { + return a, sdk.ErrInternal("can't bid negative amount") + } + if !lot.IsLT(a.Lot) { + return a, sdk.ErrInternal("auction in reverse phase, new bid not less than previous amount") + } + } + + // Move Coins + bidIncrement := bid.Sub(a.Bid) + bidAmtToReturn := a.Bid + lotDecrement := a.Lot.Sub(lot) + if bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance + bidAmtToReturn = sdk.NewInt64Coin(a.Bid.Denom, 0) + } + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(bidAmtToReturn.Add(bidIncrement))) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, bidder, sdk.NewCoins(bidAmtToReturn)) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(bidIncrement)) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.OtherPerson, sdk.NewCoins(lotDecrement)) + if err != nil { + return a, err + } + + // Update Auction + a.Bidder = bidder + a.Lot = lot + a.Bid = bid + // increment timeout + a.EndTime = EndTime(min(int64(currentBlockHeight+DefaultMaxBidDuration), int64(a.MaxEndTime))) + + return types.ForwardReverseAuction{}, nil +} +func (k Keeper) PlaceBidReverse(ctx sdk.Context, a types.ReverseAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.ReverseAuction, sdk.Error) { + // Validate New Bid + if lot.Denom != a.Lot.Denom { + return a, sdk.ErrInternal("lot denom doesn't match auction") + } + if lot.IsNegative() { + return a, sdk.ErrInternal("lot less than 0") + } + if !lot.IsLT(a.Lot) { // TODO add min bid decrements + return a, sdk.ErrInternal("lot not smaller than last lot") + } + + // Move Coins + bidAmtToReturn := a.Bid + if bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance + bidAmtToReturn = sdk.NewInt64Coin(a.Bid.Denom, 0) + } + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(bidAmtToReturn)) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, bidder, sdk.NewCoins(bidAmtToReturn)) + if err != nil { + return a, err + } + + // Update Auction + a.Bidder = bidder + a.Lot = lot + // increment timeout + a.EndTime = EndTime(min(int64(ctx.BlockHeight()+types.DefaultMaxBidDuration), int64(a.MaxEndTime))) + + return a, nil +} + +// ========================================================================================================== + +// CloseAuction closes an auction and distributes funds to the highest bidder. func (k Keeper) CloseAuction(ctx sdk.Context, auctionID types.ID) sdk.Error { // get the auction from the store @@ -134,14 +292,25 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID types.ID) sdk.Error { 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 + if ctx.BlockHeight() < int64(auction.GetEndTime()) { 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 + var err sdk.Error + switch auc := auction.(type) { + case types.ForwardAuction, types.ForwardReverseAuction: + err = k.PayoutAuctionLot(ctx, auc) + if err != nil { + return err + } + case types.ReverseAuction: + err = k.MintAndPayoutAuctionLot(ctx, auc) + if err != nil { + return err + } + default: + panic("unrecognized auction type") } // Delete auction from store (and queue) @@ -149,7 +318,26 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID types.ID) sdk.Error { return nil } +func (k Keeper) MintAndPayoutAuctionLot(ctx sdk.Context, a types.ReverseAuction) sdk.Error { + err := k.supplyKeeper.MintCoins(ctx, a.Initiator, sdk.NewCoins(a.Lot)) + if err != nil { + return err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, a.Initiator, a.Bidder, sdk.NewCoins(a.Lot)) + if err != nil { + return err + } + return nil +} +func (k Keeper) PayoutAuctionLot(ctx sdk.Context, a types.Auction) sdk.Error { + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.GetBid(), sdk.NewCoins(a.GetLot())) + if err != nil { + return err + } + return nil +} +// ===================================================================================================================== // ---------- Store methods ---------- // Use these to add and remove auction from the store. @@ -162,7 +350,7 @@ func (k Keeper) getNextAuctionID(ctx sdk.Context) (types.ID, sdk.Error) { // TOD // 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? : + // TODO Set auction ID in genesis //return 0, ErrInvalidGenesis(keeper.codespace, "InitialProposalID never set") } var auctionID types.ID @@ -177,7 +365,7 @@ func (k Keeper) incrementNextAuctionID(ctx sdk.Context) sdk.Error { 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? + //return 0, ErrInvalidGenesis(keeper.codespace, "InitialProposalID never set") // TODO } var auctionID types.ID k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &auctionID) @@ -214,7 +402,7 @@ func (k Keeper) GetAuction(ctx sdk.Context, auctionID types.ID) (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 + return auction, false } k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &auction) diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 386fb232..4193db85 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -5,26 +5,26 @@ import ( "strconv" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/supply" ) // 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) + // 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 + // GetPayout() BankInput } // 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) + Initiator string // Module who starts the auction. Giving away Lot (aka seller in a forward auction). Restricted to being a module account name rather than any account. 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 + EndTime EndTime // Block height at which the auction closes. It closes at the end of this block // TODO change to time type MaxEndTime EndTime // Maximum closing time. Auctions can close before this but never after. } @@ -56,48 +56,30 @@ type BankOutput struct { } // GetID getter for auction ID -func (a BaseAuction) GetID() ID { return a.ID } +func (a *BaseAuction) GetID() ID { return a.ID } // SetID setter for auction ID func (a *BaseAuction) SetID(id ID) { a.ID = id } +// GetBid getter for auction bid +func (a *BaseAuction) GetBid() sdk.Coin { return a.Bid } + +// GetLot getter for auction lot +func (a *BaseAuction) GetLot() sdk.Coin { return a.Lot } + // GetEndTime getter for auction end time -func (a BaseAuction) GetEndTime() EndTime { return a.EndTime } +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 (a BaseAuction) GetPayout() BankInput { +// return BankInput{a.Bidder, a.Lot} +// } func (e EndTime) String() string { return string(e) } -func (a BaseAuction) String() string { +func (a *BaseAuction) String() string { return fmt.Sprintf(`Auction %d: Initiator: %s Lot: %s @@ -111,118 +93,108 @@ func (a BaseAuction) String() 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 + *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{ +func NewForwardAuction(seller string, lot sdk.Coin, bidDenom string, EndTime EndTime) ForwardAuction { + 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 + Bidder: nil, // TODO on the first place bid, 0 coins will be sent to this address, check if this causes problems or can be avoided + Bid: sdk.NewInt64Coin(bidDenom, 0), EndTime: EndTime, MaxEndTime: EndTime, }} - output := BankOutput{seller, lot} - return auction, output + // output := BankOutput{seller, lot} + return auction } // 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 +// 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? +// // 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 -} +// 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 + *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{ +func NewReverseAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime EndTime) ReverseAuction { + // Bidder set here receives the proceeds from the first bid placed. This is set to the address of the module account. + // When this happens it uses supply.SendCoinsFromModuleToAccount, rather than SendCoinsFromModuleToModule. + // Currently not a problem but if extra checks are added to module accounts this will skip them. + // TODO description + auction := ReverseAuction{&BaseAuction{ // no ID - Initiator: buyer, + Initiator: buyerModAccName, 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 + Bidder: supply.NewModuleAddress(buyerModAccName), // 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 + //output := BankOutput{buyer, initialLot} + return auction } // PlaceBid implements Auction -func (a *ReverseAuction) PlaceBid(currentBlockHeight EndTime, bidder sdk.AccAddress, lot sdk.Coin, bid sdk.Coin) ([]BankOutput, []BankInput, sdk.Error) { +// 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 +// // 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? +// // 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 -} +// return outputs, inputs, nil +// } // ForwardReverseAuction type for forward reverse auction type ForwardReverseAuction struct { - BaseAuction + *BaseAuction MaxBid sdk.Coin - OtherPerson sdk.AccAddress // TODO rename, this is normally the original CDP owner + OtherPerson sdk.AccAddress // TODO rename, this is normally the original CDP owner, will have to be updated to account for deposits } -func (a ForwardReverseAuction) String() string { +func (a *ForwardReverseAuction) String() string { return fmt.Sprintf(`Auction %d: Initiator: %s Lot: %s @@ -239,69 +211,69 @@ func (a ForwardReverseAuction) String() string { } // 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) { +func NewForwardReverseAuction(seller string, lot sdk.Coin, EndTime EndTime, maxBid sdk.Coin, otherPerson sdk.AccAddress) ForwardReverseAuction { auction := ForwardReverseAuction{ - BaseAuction: BaseAuction{ + 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 + Bidder: nil, // TODO on the first place bid, 0 coins will be sent to this address, check if this causes problems or can be avoided + Bid: sdk.NewInt64Coin(maxBid.Denom, 0), EndTime: EndTime, MaxEndTime: EndTime}, MaxBid: maxBid, OtherPerson: otherPerson, } - output := BankOutput{seller, lot} - return auction, output + //output := BankOutput{seller, lot} + return auction } // 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") - } +// 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 - } +// // 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 - } +// 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? +// // 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 -} +// return outputs, inputs, nil +// } diff --git a/x/auction/types/expected_keepers.go b/x/auction/types/expected_keepers.go index 5a99550d..85956576 100644 --- a/x/auction/types/expected_keepers.go +++ b/x/auction/types/expected_keepers.go @@ -2,9 +2,20 @@ package types import ( sdk "github.com/cosmos/cosmos-sdk/types" + supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported" ) -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) +// SupplyKeeper defines the expected supply Keeper +type SupplyKeeper interface { + //GetSupply(ctx sdk.Context) supplyexported.SupplyI + + //GetModuleAddress(name string) sdk.AccAddress + GetModuleAccount(ctx sdk.Context, moduleName string) supplyexported.ModuleAccountI + + SendCoinsFromModuleToModule(ctx sdk.Context, sender, recipient string, amt sdk.Coins) sdk.Error + SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) sdk.Error + SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) sdk.Error + + BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error + MintCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error } From 231aa75774fd512340d9ac5b794a7b4e7f8f3427 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 12 Dec 2019 00:02:06 +0000 Subject: [PATCH 24/99] replace endTime type --- x/auction/keeper/keeper.go | 44 +++++------ x/auction/types/auctions.go | 154 ++++-------------------------------- x/auction/types/params.go | 27 +++---- 3 files changed, 46 insertions(+), 179 deletions(-) diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index 08d49a2f..acf65cb1 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -3,6 +3,7 @@ package keeper import ( "bytes" "fmt" + "time" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -21,7 +22,7 @@ type Keeper struct { } // NewKeeper returns a new auction keeper. -func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, bankKeeper types.BankKeeper, supplyKeeper types.SupplyKeeper, paramstore subspace.Subspace) Keeper { +func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, supplyKeeper types.SupplyKeeper, paramstore subspace.Subspace) Keeper { return Keeper{ supplyKeeper: supplyKeeper, storeKey: storeKey, @@ -33,7 +34,7 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, bankKeeper types.BankKee // StartForwardAuction starts a normal auction. Known as flap in maker. func (k Keeper) StartForwardAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (types.ID, sdk.Error) { // create auction - auction := types.NewForwardAuction(seller, lot, bidDenom, types.EndTime(ctx.BlockHeight())+types.DefaultMaxAuctionDuration) + auction := types.NewForwardAuction(seller, lot, bidDenom, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) // take coins from module account err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) @@ -41,7 +42,7 @@ func (k Keeper) StartForwardAuction(ctx sdk.Context, seller string, lot sdk.Coin return 0, err } // store the auction - auctionID, err := k.storeNewAuction(ctx, auction) // TODO does this need to be a pointer to satisfy the interface + auctionID, err := k.storeNewAuction(ctx, auction) // TODO does this need to be a pointer to satisfy the interface? if err != nil { return 0, err } @@ -51,7 +52,7 @@ func (k Keeper) StartForwardAuction(ctx sdk.Context, seller string, lot sdk.Coin // StartReverseAuction starts an auction where sellers compete by offering decreasing prices. Known as flop in maker. func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin) (types.ID, sdk.Error) { // create auction - auction := types.NewReverseAuction(buyer, bid, initialLot, types.EndTime(ctx.BlockHeight())+types.DefaultMaxAuctionDuration) + auction := types.NewReverseAuction(buyer, bid, initialLot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) // This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker. macc := k.supplyKeeper.GetModuleAccount(ctx, buyer) @@ -69,7 +70,7 @@ func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer string, bid sdk.Coin, // 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 string, lot sdk.Coin, maxBid sdk.Coin, otherPerson sdk.AccAddress) (types.ID, sdk.Error) { // create auction - auction := types.NewForwardReverseAuction(seller, lot, types.EndTime(ctx.BlockHeight())+types.DefaultMaxAuctionDuration, maxBid, otherPerson) + auction := types.NewForwardReverseAuction(seller, lot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), maxBid, otherPerson) // take coins from module account err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.Coins{lot}) @@ -100,8 +101,6 @@ func (k Keeper) storeNewAuction(ctx sdk.Context, auction types.Auction) (types.I 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 { @@ -112,7 +111,7 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID types.ID, bidder sdk.AccAddr } // check end time - if ctx.BlockHeight() > auction.GetEndTime() { + if ctx.BlockTime().After(auction.GetEndTime()) { return sdk.ErrInternal("auction has closed") } @@ -139,7 +138,7 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID types.ID, bidder sdk.AccAddr } // store updated auction - k.SetAuction(ctx, a) // maybe move into above funcs + k.SetAuction(ctx, a) // TODO maybe move into above funcs return nil } @@ -180,7 +179,7 @@ func (k Keeper) PlaceBidForward(ctx sdk.Context, a types.ForwardAuction, bidder a.Bidder = bidder a.Bid = bid // increment timeout - a.EndTime = EndTime(min(int64(ctx.BlockHeight()+types.DefaultMaxBidDuration), int64(a.MaxEndTime))) + a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration), a.MaxEndTime) // TODO write a min func for time types return a, nil } @@ -242,7 +241,7 @@ func (k Keeper) PlaceBidForwardReverse(ctx sdk.Context, a types.ForwardReverseAu a.Lot = lot a.Bid = bid // increment timeout - a.EndTime = EndTime(min(int64(currentBlockHeight+DefaultMaxBidDuration), int64(a.MaxEndTime))) + a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration), a.MaxEndTime) return types.ForwardReverseAuction{}, nil } @@ -276,13 +275,11 @@ func (k Keeper) PlaceBidReverse(ctx sdk.Context, a types.ReverseAuction, bidder a.Bidder = bidder a.Lot = lot // increment timeout - a.EndTime = EndTime(min(int64(ctx.BlockHeight()+types.DefaultMaxBidDuration), int64(a.MaxEndTime))) + a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration), a.MaxEndTime) return a, nil } -// ========================================================================================================== - // CloseAuction closes an auction and distributes funds to the highest bidder. func (k Keeper) CloseAuction(ctx sdk.Context, auctionID types.ID) sdk.Error { @@ -292,8 +289,8 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID types.ID) sdk.Error { return sdk.ErrInternal("auction doesn't exist") } // error if auction has not reached the end time - if ctx.BlockHeight() < int64(auction.GetEndTime()) { - 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())) + if ctx.BlockTime().Before(auction.GetEndTime()) { + return sdk.ErrInternal(fmt.Sprintf("auction can't be closed as curent block time (%v) is under auction end time (%v)", ctx.BlockTime(), auction.GetEndTime())) } // payout to the last bidder @@ -330,19 +327,18 @@ func (k Keeper) MintAndPayoutAuctionLot(ctx sdk.Context, a types.ReverseAuction) return nil } func (k Keeper) PayoutAuctionLot(ctx sdk.Context, a types.Auction) sdk.Error { - err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.GetBid(), sdk.NewCoins(a.GetLot())) + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.GetBidder(), sdk.NewCoins(a.GetLot())) if err != nil { return err } 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 +func (k Keeper) getNextAuctionID(ctx sdk.Context) (types.ID, sdk.Error) { // get next ID from store store := ctx.KVStore(k.storeKey) bz := store.Get(k.getNextAuctionIDKey()) @@ -433,7 +429,7 @@ func (k Keeper) getAuctionKey(auctionID types.ID) []byte { } // Inserts a AuctionID into the queue at endTime -func (k Keeper) InsertIntoQueue(ctx sdk.Context, endTime types.EndTime, auctionID types.ID) { +func (k Keeper) InsertIntoQueue(ctx sdk.Context, endTime time.Time, auctionID types.ID) { // get the store store := ctx.KVStore(k.storeKey) // marshal thing to be inserted @@ -446,13 +442,13 @@ func (k Keeper) InsertIntoQueue(ctx sdk.Context, endTime types.EndTime, auctionI } // removes an auctionID from the queue -func (k Keeper) removeFromQueue(ctx sdk.Context, endTime types.EndTime, auctionID types.ID) { +func (k Keeper) removeFromQueue(ctx sdk.Context, endTime time.Time, 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" ? +func (k Keeper) GetQueueIterator(ctx sdk.Context, endTime time.Time) sdk.Iterator { // TODO rename to "getAuctionsByExpiry" ? // get store store := ctx.KVStore(k.storeKey) // get an interator @@ -472,7 +468,7 @@ 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 { +func getQueueElementKeyPrefix(endTime time.Time) []byte { return bytes.Join([][]byte{ queueKeyPrefix, sdk.Uint64ToBigEndian(uint64(endTime)), // TODO check this gives correct ordering @@ -480,7 +476,7 @@ func getQueueElementKeyPrefix(endTime types.EndTime) []byte { } // Returns the key for an auctionID in the queue -func getQueueElementKey(endTime types.EndTime, auctionID types.ID) []byte { +func getQueueElementKey(endTime time.Time, auctionID types.ID) []byte { return bytes.Join([][]byte{ queueKeyPrefix, sdk.Uint64ToBigEndian(uint64(endTime)), // TODO check this gives correct ordering diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 4193db85..aa1e6e96 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -3,6 +3,7 @@ package types import ( "fmt" "strconv" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/supply" @@ -12,9 +13,9 @@ import ( 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 + GetBidder() sdk.AccAddress + GetLot() sdk.Coin + GetEndTime() time.Time } // BaseAuction type shared by all Auctions @@ -24,8 +25,8 @@ type BaseAuction struct { 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 // TODO change to time type - MaxEndTime EndTime // Maximum closing time. Auctions can close before this but never after. + EndTime time.Time // Auction closing time. Triggers at the end of the block with time ≥ endTime (bids placed in that block are valid) // TODO ensure everything is consistent with this + MaxEndTime time.Time // Maximum closing time. Auctions can close before this but never after. } // ID type for auction IDs @@ -40,21 +41,6 @@ func NewIDFromString(s string) (ID, error) { 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 } @@ -62,22 +48,13 @@ func (a *BaseAuction) GetID() ID { return a.ID } func (a *BaseAuction) SetID(id ID) { a.ID = id } // GetBid getter for auction bid -func (a *BaseAuction) GetBid() sdk.Coin { return a.Bid } +func (a *BaseAuction) GetBidder() sdk.AccAddress { return a.Bidder } // GetLot getter for auction lot func (a *BaseAuction) GetLot() sdk.Coin { return a.Lot } // 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} -// } - -func (e EndTime) String() string { - return string(e) -} +func (a *BaseAuction) GetEndTime() time.Time { return a.EndTime } func (a *BaseAuction) String() string { return fmt.Sprintf(`Auction %d: @@ -99,7 +76,7 @@ type ForwardAuction struct { } // NewForwardAuction creates a new forward auction -func NewForwardAuction(seller string, lot sdk.Coin, bidDenom string, EndTime EndTime) ForwardAuction { +func NewForwardAuction(seller string, lot sdk.Coin, bidDenom string, EndTime time.Time) ForwardAuction { auction := ForwardAuction{&BaseAuction{ // no ID Initiator: seller, @@ -113,42 +90,17 @@ func NewForwardAuction(seller string, lot sdk.Coin, bidDenom string, EndTime End return auction } -// 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(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime EndTime) ReverseAuction { - // Bidder set here receives the proceeds from the first bid placed. This is set to the address of the module account. - // When this happens it uses supply.SendCoinsFromModuleToAccount, rather than SendCoinsFromModuleToModule. - // Currently not a problem but if extra checks are added to module accounts this will skip them. - // TODO description +func NewReverseAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime time.Time) ReverseAuction { + // TODO setting the bidder here is a bit hacky + // Needs to be set so that when the first bid is placed, it is paid out to the initiator. + // Setting to the module account address bypasses calling supply.SendCoinsFromModuleToModule, instead calls SendCoinsFromModuleToModule. Not a problem currently but if checks/logic regarding modules accounts where added to those methods they would be bypassed. + // Alternative: set address to nil, and catch it in an if statement in place bid auction := ReverseAuction{&BaseAuction{ // no ID Initiator: buyerModAccName, @@ -158,35 +110,9 @@ func NewReverseAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin EndTime: EndTime, MaxEndTime: EndTime, }} - //output := BankOutput{buyer, initialLot} return auction } -// 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 @@ -211,7 +137,7 @@ func (a *ForwardReverseAuction) String() string { } // NewForwardReverseAuction creates a new forward reverse auction -func NewForwardReverseAuction(seller string, lot sdk.Coin, EndTime EndTime, maxBid sdk.Coin, otherPerson sdk.AccAddress) ForwardReverseAuction { +func NewForwardReverseAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, otherPerson sdk.AccAddress) ForwardReverseAuction { auction := ForwardReverseAuction{ BaseAuction: &BaseAuction{ // no ID @@ -227,53 +153,3 @@ func NewForwardReverseAuction(seller string, lot sdk.Coin, EndTime EndTime, maxB //output := BankOutput{seller, lot} return auction } - -// 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 -// } diff --git a/x/auction/types/params.go b/x/auction/types/params.go index 527cd844..af189ed2 100644 --- a/x/auction/types/params.go +++ b/x/auction/types/params.go @@ -3,16 +3,17 @@ package types import ( "bytes" "fmt" + "time" "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 + // DefaultMaxAuctionDuration max length of auction + DefaultMaxAuctionDuration time.Duration = 2 * 24 * time.Hour // DefaultBidDuration how long an auction gets extended when someone bids, roughly 3 hours in blocks - DefaultMaxBidDuration EndTime = 3 * 3600 / 5 + DefaultMaxBidDuration time.Duration = 3 * time.Hour // DefaultStartingAuctionID what the id of the first auction will be DefaultStartingAuctionID ID = ID(0) ) @@ -21,24 +22,24 @@ const ( var ( // ParamStoreKeyAuctionParams Param store key for auction params KeyAuctionBidDuration = []byte("MaxBidDuration") - KeyAuctionDuration = []byte("MaxAuctionDuration") - KeyAuctionStartingID = []byte("StartingAuctionID") + 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"` + MaxAuctionDuration time.Duration `json:"max_auction_duration" yaml:"max_auction_duration"` // max length of auction, in blocks + MaxBidDuration time.Duration `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 { +func NewAuctionParams(maxAuctionDuration time.Duration, bidDuration time.Duration, startingID ID) AuctionParams { return AuctionParams{ MaxAuctionDuration: maxAuctionDuration, - MaxBidDuration: bidDuration, + MaxBidDuration: bidDuration, StartingAuctionID: startingID, } } @@ -85,12 +86,6 @@ func (ap AuctionParams) String() string { // 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) } From 5618e11990aced27109b9e15c6083faab2a82cf8 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 12 Dec 2019 00:16:10 +0000 Subject: [PATCH 25/99] split keeper file up --- x/auction/keeper/auctions.go | 297 ++++++++++++++++++++++ x/auction/keeper/keeper.go | 471 ----------------------------------- x/auction/keeper/store.go | 187 ++++++++++++++ 3 files changed, 484 insertions(+), 471 deletions(-) create mode 100644 x/auction/keeper/auctions.go create mode 100644 x/auction/keeper/store.go diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go new file mode 100644 index 00000000..9a5630d6 --- /dev/null +++ b/x/auction/keeper/auctions.go @@ -0,0 +1,297 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/supply" + "github.com/kava-labs/kava/x/auction/types" +) + +// StartForwardAuction starts a normal auction. +func (k Keeper) StartForwardAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (types.ID, sdk.Error) { + // create auction + auction := types.NewForwardAuction(seller, lot, bidDenom, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) + + // take coins from module account + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) + if err != nil { + return 0, err + } + // store the auction + auctionID, err := k.storeNewAuction(ctx, auction) // TODO does this need to be a pointer to satisfy the interface? + if err != nil { + return 0, err + } + return auctionID, nil +} + +// StartReverseAuction starts an auction where sellers compete by offering decreasing prices. +func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin) (types.ID, sdk.Error) { + // create auction + auction := types.NewReverseAuction(buyer, bid, initialLot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) + + // This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker. + macc := k.supplyKeeper.GetModuleAccount(ctx, buyer) + if !macc.HasPermission(supply.Minter) { // TODO ideally don't want to import supply + return 0, sdk.ErrInternal("module does not have minting permissions") + } + // store the auction + auctionID, err := k.storeNewAuction(ctx, &auction) + 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. +func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, otherPerson sdk.AccAddress) (types.ID, sdk.Error) { + // create auction + auction := types.NewForwardReverseAuction(seller, lot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), maxBid, otherPerson) + + // take coins from module account + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.Coins{lot}) + if err != nil { + return 0, err + } + // store the auction + auctionID, err := k.storeNewAuction(ctx, &auction) + if err != nil { + return 0, err + } + return auctionID, 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") + } + + // check end time + if ctx.BlockTime().After(auction.GetEndTime()) { + return sdk.ErrInternal("auction has closed") + } + + var err sdk.Error + var a types.Auction + switch auc := auction.(type) { + case types.ForwardAuction: + a, err = k.PlaceBidForward(ctx, auc, bidder, bid) + if err != nil { + return err + } + case types.ReverseAuction: + a, err = k.PlaceBidReverse(ctx, auc, bidder, lot) + if err != nil { + return err + } + case types.ForwardReverseAuction: + a, err = k.PlaceBidForwardReverse(ctx, auc, bidder, bid, lot) + if err != nil { + return err + } + default: + panic("unrecognized auction type") + } + + // store updated auction + k.SetAuction(ctx, a) // TODO maybe move into above funcs + + return nil +} + +func (k Keeper) PlaceBidForward(ctx sdk.Context, a types.ForwardAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.ForwardAuction, sdk.Error) { + // Valid New Bid + if bid.Denom != a.Bid.Denom { + return a, sdk.ErrInternal("bid denom doesn't match auction") + } + if !a.Bid.IsLT(bid) { // TODO add minimum bid size + return a, sdk.ErrInternal("bid not greater than last bid") + } + + // Move Coins + increment := bid.Sub(a.Bid) + bidAmtToReturn := a.Bid + if bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance + bidAmtToReturn = sdk.NewInt64Coin(a.Bid.Denom, 0) + } + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(bidAmtToReturn.Add(increment))) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, bidder, sdk.NewCoins(bidAmtToReturn)) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(increment)) // increase in bid size is burned + if err != nil { + return a, err + } + err = k.supplyKeeper.BurnCoins(ctx, a.Initiator, sdk.NewCoins(increment)) + if err != nil { + return a, err + } + + // Update Auction + a.Bidder = bidder + a.Bid = bid + // increment timeout + a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration), a.MaxEndTime) // TODO write a min func for time types + + return a, nil +} +func (k Keeper) PlaceBidForwardReverse(ctx sdk.Context, a types.ForwardReverseAuction, bidder sdk.AccAddress, bid sdk.Coin, lot sdk.Coin) (types.ForwardReverseAuction, sdk.Error) { + // Validate New Bid // TODO min bid increments, make validation code less confusing + if !a.Bid.IsEqual(a.MaxBid) { + // Auction is in forward phase, a bid here can put the auction into forward or reverse phases + if !a.Bid.IsLT(bid) { + return a, sdk.ErrInternal("auction in forward phase, new bid not higher than last bid") + } + if a.MaxBid.IsLT(bid) { + return a, sdk.ErrInternal("bid higher than max bid") + } + if lot.IsNegative() || a.Lot.IsLT(lot) { + return a, sdk.ErrInternal("lot out of bounds") + } + if lot.IsLT(a.Lot) && !bid.IsEqual(a.MaxBid) { + return a, sdk.ErrInternal("auction cannot enter reverse phase without bidding max bid") + } + } else { + // Auction is in reverse phase, it can never leave reverse phase + if !bid.IsEqual(a.MaxBid) { + return a, sdk.ErrInternal("") // not necessary + } + if lot.IsNegative() { + return a, sdk.ErrInternal("can't bid negative amount") + } + if !lot.IsLT(a.Lot) { + return a, sdk.ErrInternal("auction in reverse phase, new bid not less than previous amount") + } + } + + // Move Coins + bidIncrement := bid.Sub(a.Bid) + bidAmtToReturn := a.Bid + lotDecrement := a.Lot.Sub(lot) + if bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance + bidAmtToReturn = sdk.NewInt64Coin(a.Bid.Denom, 0) + } + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(bidAmtToReturn.Add(bidIncrement))) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, bidder, sdk.NewCoins(bidAmtToReturn)) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(bidIncrement)) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.OtherPerson, sdk.NewCoins(lotDecrement)) + if err != nil { + return a, err + } + + // Update Auction + a.Bidder = bidder + a.Lot = lot + a.Bid = bid + // increment timeout + a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration), a.MaxEndTime) + + return types.ForwardReverseAuction{}, nil +} +func (k Keeper) PlaceBidReverse(ctx sdk.Context, a types.ReverseAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.ReverseAuction, sdk.Error) { + // Validate New Bid + if lot.Denom != a.Lot.Denom { + return a, sdk.ErrInternal("lot denom doesn't match auction") + } + if lot.IsNegative() { + return a, sdk.ErrInternal("lot less than 0") + } + if !lot.IsLT(a.Lot) { // TODO add min bid decrements + return a, sdk.ErrInternal("lot not smaller than last lot") + } + + // Move Coins + bidAmtToReturn := a.Bid + if bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance + bidAmtToReturn = sdk.NewInt64Coin(a.Bid.Denom, 0) + } + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(bidAmtToReturn)) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, bidder, sdk.NewCoins(bidAmtToReturn)) + if err != nil { + return a, err + } + + // Update Auction + a.Bidder = bidder + a.Lot = lot + // increment timeout + a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration), a.MaxEndTime) + + return a, nil +} + +// CloseAuction closes an auction and distributes funds to the highest bidder. +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.BlockTime().Before(auction.GetEndTime()) { + return sdk.ErrInternal(fmt.Sprintf("auction can't be closed as curent block time (%v) is under auction end time (%v)", ctx.BlockTime(), auction.GetEndTime())) + } + + // payout to the last bidder + var err sdk.Error + switch auc := auction.(type) { + case types.ForwardAuction, types.ForwardReverseAuction: + err = k.PayoutAuctionLot(ctx, auc) + if err != nil { + return err + } + case types.ReverseAuction: + err = k.MintAndPayoutAuctionLot(ctx, auc) + if err != nil { + return err + } + default: + panic("unrecognized auction type") + } + + // Delete auction from store (and queue) + k.DeleteAuction(ctx, auctionID) + + return nil +} +func (k Keeper) MintAndPayoutAuctionLot(ctx sdk.Context, a types.ReverseAuction) sdk.Error { + err := k.supplyKeeper.MintCoins(ctx, a.Initiator, sdk.NewCoins(a.Lot)) + if err != nil { + return err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, a.Initiator, a.Bidder, sdk.NewCoins(a.Lot)) + if err != nil { + return err + } + return nil +} +func (k Keeper) PayoutAuctionLot(ctx sdk.Context, a types.Auction) sdk.Error { + // TODO this function is responsible for the addition of GetBidder and GetLot to auction interface. Could be split in to two funcs that operate on concrete auction types + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.GetBidder(), sdk.NewCoins(a.GetLot())) + if err != nil { + return err + } + return nil +} diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index acf65cb1..fe4cd04a 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -1,14 +1,9 @@ package keeper import ( - "bytes" - "fmt" - "time" - "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/params/subspace" - "github.com/cosmos/cosmos-sdk/x/supply" "github.com/kava-labs/kava/x/auction/types" ) @@ -30,469 +25,3 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, supplyKeeper types.Suppl paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()), } } - -// StartForwardAuction starts a normal auction. Known as flap in maker. -func (k Keeper) StartForwardAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (types.ID, sdk.Error) { - // create auction - auction := types.NewForwardAuction(seller, lot, bidDenom, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) - - // take coins from module account - err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) - if err != nil { - return 0, err - } - // store the auction - auctionID, err := k.storeNewAuction(ctx, auction) // TODO does this need to be a pointer to satisfy the interface? - 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 string, bid sdk.Coin, initialLot sdk.Coin) (types.ID, sdk.Error) { - // create auction - auction := types.NewReverseAuction(buyer, bid, initialLot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) - - // This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker. - macc := k.supplyKeeper.GetModuleAccount(ctx, buyer) - if !macc.HasPermission(supply.Minter) { // TODO ideally don't want to import supply - return 0, sdk.ErrInternal("module does not have minting permissions") - } - // store the auction - auctionID, err := k.storeNewAuction(ctx, &auction) - 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 string, lot sdk.Coin, maxBid sdk.Coin, otherPerson sdk.AccAddress) (types.ID, sdk.Error) { - // create auction - auction := types.NewForwardReverseAuction(seller, lot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), maxBid, otherPerson) - - // take coins from module account - err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.Coins{lot}) - if err != nil { - return 0, err - } - // store the auction - auctionID, err := k.storeNewAuction(ctx, &auction) - if err != nil { - return 0, err - } - return auctionID, nil -} - -// set an auction in the store, adding a new ID, and setting indexes -func (k Keeper) storeNewAuction(ctx sdk.Context, auction types.Auction) (types.ID, sdk.Error) { - // get ID - newAuctionID, err := k.getNextAuctionID(ctx) - if err != nil { - return 0, err - } - // set ID - auction.SetID(newAuctionID) - - // 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") - } - - // check end time - if ctx.BlockTime().After(auction.GetEndTime()) { - return sdk.ErrInternal("auction has closed") - } - - var err sdk.Error - var a types.Auction - switch auc := auction.(type) { - case types.ForwardAuction: - a, err = k.PlaceBidForward(ctx, auc, bidder, bid) - if err != nil { - return err - } - case types.ReverseAuction: - a, err = k.PlaceBidReverse(ctx, auc, bidder, lot) - if err != nil { - return err - } - case types.ForwardReverseAuction: - a, err = k.PlaceBidForwardReverse(ctx, auc, bidder, bid, lot) - if err != nil { - return err - } - default: - panic("unrecognized auction type") - } - - // store updated auction - k.SetAuction(ctx, a) // TODO maybe move into above funcs - - return nil -} - -func (k Keeper) PlaceBidForward(ctx sdk.Context, a types.ForwardAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.ForwardAuction, sdk.Error) { - // Valid New Bid - if bid.Denom != a.Bid.Denom { - return a, sdk.ErrInternal("bid denom doesn't match auction") - } - if !a.Bid.IsLT(bid) { // TODO add minimum bid size - return a, sdk.ErrInternal("bid not greater than last bid") - } - - // Move Coins - increment := bid.Sub(a.Bid) - bidAmtToReturn := a.Bid - if bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance - bidAmtToReturn = sdk.NewInt64Coin(a.Bid.Denom, 0) - } - err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(bidAmtToReturn.Add(increment))) - if err != nil { - return a, err - } - err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, bidder, sdk.NewCoins(bidAmtToReturn)) - if err != nil { - return a, err - } - err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(increment)) // increase in bid size is burned - if err != nil { - return a, err - } - err = k.supplyKeeper.BurnCoins(ctx, a.Initiator, sdk.NewCoins(increment)) - if err != nil { - return a, err - } - - // Update Auction - a.Bidder = bidder - a.Bid = bid - // increment timeout - a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration), a.MaxEndTime) // TODO write a min func for time types - - return a, nil -} -func (k Keeper) PlaceBidForwardReverse(ctx sdk.Context, a types.ForwardReverseAuction, bidder sdk.AccAddress, bid sdk.Coin, lot sdk.Coin) (types.ForwardReverseAuction, sdk.Error) { - // Validate New Bid // TODO min bid increments, make validation code less confusing - if !a.Bid.IsEqual(a.MaxBid) { - // Auction is in forward phase, a bid here can put the auction into forward or reverse phases - if !a.Bid.IsLT(bid) { - return a, sdk.ErrInternal("auction in forward phase, new bid not higher than last bid") - } - if a.MaxBid.IsLT(bid) { - return a, sdk.ErrInternal("bid higher than max bid") - } - if lot.IsNegative() || a.Lot.IsLT(lot) { - return a, sdk.ErrInternal("lot out of bounds") - } - if lot.IsLT(a.Lot) && !bid.IsEqual(a.MaxBid) { - return a, sdk.ErrInternal("auction cannot enter reverse phase without bidding max bid") - } - } else { - // Auction is in reverse phase, it can never leave reverse phase - if !bid.IsEqual(a.MaxBid) { - return a, sdk.ErrInternal("") // not necessary - } - if lot.IsNegative() { - return a, sdk.ErrInternal("can't bid negative amount") - } - if !lot.IsLT(a.Lot) { - return a, sdk.ErrInternal("auction in reverse phase, new bid not less than previous amount") - } - } - - // Move Coins - bidIncrement := bid.Sub(a.Bid) - bidAmtToReturn := a.Bid - lotDecrement := a.Lot.Sub(lot) - if bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance - bidAmtToReturn = sdk.NewInt64Coin(a.Bid.Denom, 0) - } - err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(bidAmtToReturn.Add(bidIncrement))) - if err != nil { - return a, err - } - err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, bidder, sdk.NewCoins(bidAmtToReturn)) - if err != nil { - return a, err - } - err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(bidIncrement)) - if err != nil { - return a, err - } - err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.OtherPerson, sdk.NewCoins(lotDecrement)) - if err != nil { - return a, err - } - - // Update Auction - a.Bidder = bidder - a.Lot = lot - a.Bid = bid - // increment timeout - a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration), a.MaxEndTime) - - return types.ForwardReverseAuction{}, nil -} -func (k Keeper) PlaceBidReverse(ctx sdk.Context, a types.ReverseAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.ReverseAuction, sdk.Error) { - // Validate New Bid - if lot.Denom != a.Lot.Denom { - return a, sdk.ErrInternal("lot denom doesn't match auction") - } - if lot.IsNegative() { - return a, sdk.ErrInternal("lot less than 0") - } - if !lot.IsLT(a.Lot) { // TODO add min bid decrements - return a, sdk.ErrInternal("lot not smaller than last lot") - } - - // Move Coins - bidAmtToReturn := a.Bid - if bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance - bidAmtToReturn = sdk.NewInt64Coin(a.Bid.Denom, 0) - } - err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(bidAmtToReturn)) - if err != nil { - return a, err - } - err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, bidder, sdk.NewCoins(bidAmtToReturn)) - if err != nil { - return a, err - } - - // Update Auction - a.Bidder = bidder - a.Lot = lot - // increment timeout - a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration), a.MaxEndTime) - - return a, nil -} - -// CloseAuction closes an auction and distributes funds to the highest bidder. -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.BlockTime().Before(auction.GetEndTime()) { - return sdk.ErrInternal(fmt.Sprintf("auction can't be closed as curent block time (%v) is under auction end time (%v)", ctx.BlockTime(), auction.GetEndTime())) - } - - // payout to the last bidder - var err sdk.Error - switch auc := auction.(type) { - case types.ForwardAuction, types.ForwardReverseAuction: - err = k.PayoutAuctionLot(ctx, auc) - if err != nil { - return err - } - case types.ReverseAuction: - err = k.MintAndPayoutAuctionLot(ctx, auc) - if err != nil { - return err - } - default: - panic("unrecognized auction type") - } - - // Delete auction from store (and queue) - k.DeleteAuction(ctx, auctionID) - - return nil -} -func (k Keeper) MintAndPayoutAuctionLot(ctx sdk.Context, a types.ReverseAuction) sdk.Error { - err := k.supplyKeeper.MintCoins(ctx, a.Initiator, sdk.NewCoins(a.Lot)) - if err != nil { - return err - } - err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, a.Initiator, a.Bidder, sdk.NewCoins(a.Lot)) - if err != nil { - return err - } - return nil -} -func (k Keeper) PayoutAuctionLot(ctx sdk.Context, a types.Auction) sdk.Error { - err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.GetBidder(), sdk.NewCoins(a.GetLot())) - if err != nil { - return err - } - 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) { - // 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 Set auction 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 - } - 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 - } - - 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 time.Time, 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 time.Time, 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 time.Time) 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 time.Time) []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 time.Time, 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 -} diff --git a/x/auction/keeper/store.go b/x/auction/keeper/store.go new file mode 100644 index 00000000..04ee36a3 --- /dev/null +++ b/x/auction/keeper/store.go @@ -0,0 +1,187 @@ +package keeper + +import ( + "bytes" + "fmt" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/auction/types" +) + +// set an auction in the store, adding a new ID, and setting indexes +func (k Keeper) storeNewAuction(ctx sdk.Context, auction types.Auction) (types.ID, sdk.Error) { + // get ID + newAuctionID, err := k.getNextAuctionID(ctx) + if err != nil { + return 0, err + } + // set ID + auction.SetID(newAuctionID) + + // store auction + k.SetAuction(ctx, auction) + k.incrementNextAuctionID(ctx) + return newAuctionID, nil +} + +// getNextAuctionID gets the next available global AuctionID +func (k Keeper) getNextAuctionID(ctx sdk.Context) (types.ID, sdk.Error) { + // 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 Set auction 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 + } + 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 + } + + 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 time.Time, 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 time.Time, 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 time.Time) 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 time.Time) []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 time.Time, 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 +} From 5363541de38fdeec230920f467d455bcb58d5749 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 21 Dec 2019 01:04:04 +0000 Subject: [PATCH 26/99] update store methods --- app/app.go | 18 +-- x/auction/abci.go | 19 ++- x/auction/alias.go | 4 - x/auction/genesis.go | 11 +- x/auction/keeper/auctions.go | 6 + x/auction/keeper/keeper_test.go | 279 +++++++++++++++++++------------- x/auction/keeper/querier.go | 17 +- x/auction/keeper/store.go | 205 ++++++++++------------- x/auction/types/auctions.go | 42 +++-- x/auction/types/keys.go | 22 +++ 10 files changed, 332 insertions(+), 291 deletions(-) diff --git a/app/app.go b/app/app.go index 088e26a5..90f4f5fe 100644 --- a/app/app.go +++ b/app/app.go @@ -151,7 +151,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, crisisSubspace := app.paramsKeeper.Subspace(crisis.DefaultParamspace) auctionSubspace := app.paramsKeeper.Subspace(auction.DefaultParamspace) cdpSubspace := app.paramsKeeper.Subspace(cdp.DefaultParamspace) - liquidatorSubspace := app.paramsKeeper.Subspace(liquidator.DefaultParamspace) + //liquidatorSubspace := app.paramsKeeper.Subspace(liquidator.DefaultParamspace) pricefeedSubspace := app.paramsKeeper.Subspace(pricefeed.DefaultParamspace) // add keepers @@ -237,16 +237,16 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, app.bankKeeper) app.auctionKeeper = auction.NewKeeper( app.cdc, - app.cdpKeeper, // CDP keeper standing in for bank keys[auction.StoreKey], + app.supplyKeeper, // CDP keeper standing in for bank auctionSubspace) - app.liquidatorKeeper = liquidator.NewKeeper( - app.cdc, - keys[liquidator.StoreKey], - liquidatorSubspace, - app.cdpKeeper, - app.auctionKeeper, - app.cdpKeeper) // CDP keeper standing in for bank + // app.liquidatorKeeper = liquidator.NewKeeper( + // app.cdc, + // keys[liquidator.StoreKey], + // liquidatorSubspace, + // app.cdpKeeper, + // app.auctionKeeper, + // app.cdpKeeper) // CDP keeper standing in for bank // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks diff --git a/x/auction/abci.go b/x/auction/abci.go index 164b35b7..f05a518a 100644 --- a/x/auction/abci.go +++ b/x/auction/abci.go @@ -7,17 +7,16 @@ import ( // 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) + var expiredAuctions []ID + k.IterateAuctionsByTime(ctx, ctx.BlockTime(), func(id ID) bool { + expiredAuctions = append(expiredAuctions, id) + return false + }) + // Note: iteration and auction closing are in separate loops as db should not be modified during iteration // TODO is this correct? gov modifies during iteration + for _, id := range expiredAuctions { + err := k.CloseAuction(ctx, id) if err != nil { - panic(err) // TODO how should errors be handled here? + panic(err) } } } diff --git a/x/auction/alias.go b/x/auction/alias.go index 9f86f9e7..33b64525 100644 --- a/x/auction/alias.go +++ b/x/auction/alias.go @@ -49,13 +49,9 @@ 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 diff --git a/x/auction/genesis.go b/x/auction/genesis.go index 1b7a327c..7b16562b 100644 --- a/x/auction/genesis.go +++ b/x/auction/genesis.go @@ -18,13 +18,10 @@ func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { params := keeper.GetParams(ctx) var genAuctions GenesisAuctions - iterator := keeper.GetAuctionIterator(ctx) + keeper.IterateAuctions(ctx, func(a Auction) bool { + genAuctions = append(genAuctions, a) + return false + }) - for ; iterator.Valid(); iterator.Next() { - - auction := keeper.DecodeAuction(ctx, iterator.Value()) - genAuctions = append(genAuctions, auction) - - } return NewGenesisState(params, genAuctions) } diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index 9a5630d6..b5ca5850 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -2,6 +2,7 @@ package keeper import ( "fmt" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/supply" @@ -295,3 +296,8 @@ func (k Keeper) PayoutAuctionLot(ctx sdk.Context, a types.Auction) sdk.Error { } return nil } + +// FIXME stand in func for compiler +func earliestTime(t1, t2 time.Time) time.Time { + return t1 +} diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go index 873dcb81..56d6065f 100644 --- a/x/auction/keeper/keeper_test.go +++ b/x/auction/keeper/keeper_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" @@ -12,118 +13,118 @@ import ( "github.com/kava-labs/kava/x/auction/types" ) -func TestKeeper_ForwardAuction(t *testing.T) { - // Setup - _, addrs := app.GeneratePrivKeyAddressPairs(2) - seller := addrs[0] - buyer := addrs[1] +// func TestKeeper_ForwardAuction(t *testing.T) { +// // Setup +// _, addrs := app.GeneratePrivKeyAddressPairs(2) +// seller := addrs[0] +// buyer := addrs[1] - tApp := app.NewTestApp() - tApp.InitializeFromGenesisStates( - app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}), - ) +// tApp := app.NewTestApp() +// tApp.InitializeFromGenesisStates( +// app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}), +// ) - ctx := tApp.NewContext(false, abci.Header{}) - keeper := tApp.GetAuctionKeeper() +// ctx := tApp.NewContext(false, abci.Header{}) +// keeper := tApp.GetAuctionKeeper() - // Create an auction (lot: 20 t1, initialBid: 0 t2) - auctionID, err := keeper.StartForwardAuction(ctx, seller, c("token1", 20), c("token2", 0)) // lot, initialBid - require.NoError(t, err) - // Check seller's coins have decreased - tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) +// // Create an auction (lot: 20 t1, initialBid: 0 t2) +// auctionID, err := keeper.StartForwardAuction(ctx, seller, c("token1", 20), c("token2", 0)) // lot, initialBid +// require.NoError(t, err) +// // Check seller's coins have decreased +// tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) - // PlaceBid (bid: 10 t2, lot: same as starting) - require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 10), c("token1", 20))) // bid, lot - // Check buyer's coins have decreased - tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) - // Check seller's coins have increased - tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) +// // PlaceBid (bid: 10 t2, lot: same as starting) +// require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 10), c("token1", 20))) // bid, lot +// // Check buyer's coins have decreased +// tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) +// // Check seller's coins have increased +// tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) - // Close auction at just after auction expiry - ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration)) - require.NoError(t, keeper.CloseAuction(ctx, auctionID)) - // Check buyer's coins increased - tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90))) -} +// // Close auction at just after auction expiry +// ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration)) +// require.NoError(t, keeper.CloseAuction(ctx, auctionID)) +// // Check buyer's coins increased +// tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90))) +// } -func TestKeeper_ReverseAuction(t *testing.T) { - // Setup - _, addrs := app.GeneratePrivKeyAddressPairs(2) - seller := addrs[0] - buyer := addrs[1] +// func TestKeeper_ReverseAuction(t *testing.T) { +// // Setup +// _, addrs := app.GeneratePrivKeyAddressPairs(2) +// seller := addrs[0] +// buyer := addrs[1] - tApp := app.NewTestApp() - tApp.InitializeFromGenesisStates( - app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}), - ) +// tApp := app.NewTestApp() +// tApp.InitializeFromGenesisStates( +// app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}), +// ) - ctx := tApp.NewContext(false, abci.Header{}) - keeper := tApp.GetAuctionKeeper() +// ctx := tApp.NewContext(false, abci.Header{}) +// keeper := tApp.GetAuctionKeeper() - // Start auction - auctionID, err := keeper.StartReverseAuction(ctx, buyer, c("token1", 20), c("token2", 99)) // buyer, bid, initialLot - require.NoError(t, err) - // Check buyer's coins have decreased - tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 1))) +// // Start auction +// auctionID, err := keeper.StartReverseAuction(ctx, buyer, c("token1", 20), c("token2", 99)) // buyer, bid, initialLot +// require.NoError(t, err) +// // Check buyer's coins have decreased +// tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 1))) - // Place a bid - require.NoError(t, keeper.PlaceBid(ctx, 0, seller, c("token1", 20), c("token2", 10))) // bid, lot - // Check seller's coins have decreased - tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) - // Check buyer's coins have increased - tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90))) +// // Place a bid +// require.NoError(t, keeper.PlaceBid(ctx, 0, seller, c("token1", 20), c("token2", 10))) // bid, lot +// // Check seller's coins have decreased +// tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) +// // Check buyer's coins have increased +// tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90))) - // Close auction at just after auction expiry - ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration)) - require.NoError(t, keeper.CloseAuction(ctx, auctionID)) - // Check seller's coins increased - tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) -} +// // Close auction at just after auction expiry +// ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration)) +// require.NoError(t, keeper.CloseAuction(ctx, auctionID)) +// // Check seller's coins increased +// tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) +// } -func TestKeeper_ForwardReverseAuction(t *testing.T) { - // Setup - _, addrs := app.GeneratePrivKeyAddressPairs(3) - seller := addrs[0] - buyer := addrs[1] - recipient := addrs[2] +// func TestKeeper_ForwardReverseAuction(t *testing.T) { +// // Setup +// _, addrs := app.GeneratePrivKeyAddressPairs(3) +// seller := addrs[0] +// buyer := addrs[1] +// recipient := addrs[2] - tApp := app.NewTestApp() - tApp.InitializeFromGenesisStates( - app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}), - ) +// tApp := app.NewTestApp() +// tApp.InitializeFromGenesisStates( +// app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}), +// ) - ctx := tApp.NewContext(false, abci.Header{}) - keeper := tApp.GetAuctionKeeper() +// ctx := tApp.NewContext(false, abci.Header{}) +// keeper := tApp.GetAuctionKeeper() - // Start auction - auctionID, err := keeper.StartForwardReverseAuction(ctx, seller, c("token1", 20), c("token2", 50), recipient) // seller, lot, maxBid, otherPerson - require.NoError(t, err) - // Check seller's coins have decreased - tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) +// // Start auction +// auctionID, err := keeper.StartForwardReverseAuction(ctx, seller, c("token1", 20), c("token2", 50), recipient) // seller, lot, maxBid, otherPerson +// require.NoError(t, err) +// // Check seller's coins have decreased +// tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) - // Place a bid - require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50), c("token1", 15))) // bid, lot - // Check bidder's coins have decreased - tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50))) - // Check seller's coins have increased - tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 150))) - // Check "recipient" has received coins - tApp.CheckBalance(t, ctx, recipient, cs(c("token1", 105), c("token2", 100))) +// // Place a bid +// require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50), c("token1", 15))) // bid, lot +// // Check bidder's coins have decreased +// tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50))) +// // Check seller's coins have increased +// tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 150))) +// // Check "recipient" has received coins +// tApp.CheckBalance(t, ctx, recipient, cs(c("token1", 105), c("token2", 100))) - // Close auction at just after auction expiry - ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration)) - require.NoError(t, keeper.CloseAuction(ctx, auctionID)) - // Check buyer's coins increased - tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 115), c("token2", 50))) -} +// // Close auction at just after auction expiry +// ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration)) +// require.NoError(t, keeper.CloseAuction(ctx, auctionID)) +// // Check buyer's coins increased +// tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 115), c("token2", 50))) +// } -func TestKeeper_SetGetDeleteAuction(t *testing.T) { +func SetGetDeleteAuction(t *testing.T) { // setup keeper, create auction - _, addrs := app.GeneratePrivKeyAddressPairs(1) tApp := app.NewTestApp() keeper := tApp.GetAuctionKeeper() ctx := tApp.NewContext(true, abci.Header{}) - auction, _ := types.NewForwardAuction(addrs[0], c("usdx", 100), c("kava", 0), types.EndTime(1000)) + someTime := time.Date(43, time.January, 1, 0, 0, 0, 0, time.UTC) // need to specify UTC as tz info is lost on unmarshal + auction := types.NewForwardAuction("some_module", c("usdx", 100), "kava", someTime) id := types.ID(5) auction.SetID(id) @@ -135,9 +136,9 @@ func TestKeeper_SetGetDeleteAuction(t *testing.T) { require.True(t, found) require.Equal(t, &auction, readAuction) // check auction is in queue - iter := keeper.GetQueueIterator(ctx, 100000) - require.Equal(t, 1, len(convertIteratorToSlice(keeper, iter))) - iter.Close() + // iter := keeper.GetQueueIterator(ctx, 100000) + // require.Equal(t, 1, len(convertIteratorToSlice(keeper, iter))) + // iter.Close() // delete auction keeper.DeleteAuction(ctx, id) @@ -146,41 +147,93 @@ func TestKeeper_SetGetDeleteAuction(t *testing.T) { _, 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() + // 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) { +func TestIncrementNextAuctionID(t *testing.T) { // setup keeper tApp := app.NewTestApp() keeper := tApp.GetAuctionKeeper() ctx := tApp.NewContext(true, abci.Header{}) - // create an example queue - type queue []struct { - endTime types.EndTime + // store id + id := types.ID(123456) + keeper.SetNextAuctionID(ctx, id) + + require.NoError(t, keeper.IncrementNextAuctionID(ctx)) + + // check id was incremented + readID, err := keeper.GetNextAuctionID(ctx) + require.NoError(t, err) + require.Equal(t, id+1, readID) + +} + +// func TestIterateAuctions(t *testing.T) { +// // setup keeper +// tApp := app.NewTestApp() +// keeper := tApp.GetAuctionKeeper() +// ctx := tApp.NewContext(true, abci.Header{}) + +// auctions := []types.Auction{ +// &types.ForwardAuction{}, +// } +// for _, a := range auctions { +// keeper.SetAuction(ctx, a) +// } + +// var readAuctions []types.Auction +// keeper.IterateAuctions(ctx, func(a types.Auction) bool { +// readAuctions = append(readAuctions, a) +// return false +// }) + +// require.Equal(t, auctions, readAuctions) +// } + +func TestIterateAuctionsByTime(t *testing.T) { + // setup keeper + tApp := app.NewTestApp() + keeper := tApp.GetAuctionKeeper() + ctx := tApp.NewContext(true, abci.Header{}) + + // create a list of times + queue := []struct { + endTime time.Time auctionID types.ID + }{ + {time.Date(84, time.January, 1, 0, 0, 0, 0, time.UTC), 34345345}, + {time.Date(98, time.January, 2, 0, 0, 0, 0, time.UTC), 5}, + {time.Date(98, time.January, 2, 13, 5, 0, 0, time.UTC), 6}, + {time.Date(98, time.January, 2, 16, 0, 0, 0, time.UTC), 1}, + {time.Date(98, time.January, 2, 16, 0, 0, 0, time.UTC), 3}, + {time.Date(98, time.January, 2, 16, 0, 0, 0, time.UTC), 4}, + {time.Date(98, time.January, 2, 16, 0, 0, 1, time.UTC), 0}, // TODO tidy up redundant entries + } + cutoffTime := time.Date(98, time.January, 2, 16, 0, 0, 0, time.UTC) + + var expectedQueue []types.ID + for _, i := range queue { + if i.endTime.After(cutoffTime) { // only append items where endTime ≤ cutoffTime + break + } + expectedQueue = append(expectedQueue, i.auctionID) } - q := queue{{1000, 0}, {1300, 2}, {5200, 1}} // write and read queue - for _, v := range q { + for _, v := range queue { 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 - tApp.Codec().MustUnmarshalBinaryLengthPrefixed(iter.Value(), &auctionID) - require.Equal(t, q[i].auctionID, auctionID) - i++ - } + var readQueue []types.ID + keeper.IterateAuctionsByTime(ctx, cutoffTime, func(id types.ID) bool { + readQueue = append(readQueue, id) + return false + }) + require.Equal(t, expectedQueue, readQueue) } func convertIteratorToSlice(keeper keeper.Keeper, iterator sdk.Iterator) []types.ID { diff --git a/x/auction/keeper/querier.go b/x/auction/keeper/querier.go index 15748733..3b92476e 100644 --- a/x/auction/keeper/querier.go +++ b/x/auction/keeper/querier.go @@ -1,6 +1,7 @@ package keeper import ( + "fmt" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/x/auction/types" @@ -20,18 +21,14 @@ func NewQuerier(keeper Keeper) sdk.Querier { } func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { - var AuctionsList types.QueryResAuctions + var auctionsList types.QueryResAuctions - iterator := keeper.GetAuctionIterator(ctx) + keeper.IterateAuctions(ctx, func(a types.Auction) bool { + auctionsList = append(auctionsList, fmt.Sprintf("%+v", a)) // TODO formatting + return false + }) - 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) + bz, err2 := codec.MarshalJSONIndent(keeper.cdc, auctionsList) if err2 != nil { panic("could not marshal result to JSON") } diff --git a/x/auction/keeper/store.go b/x/auction/keeper/store.go index 04ee36a3..f65375a6 100644 --- a/x/auction/keeper/store.go +++ b/x/auction/keeper/store.go @@ -1,91 +1,84 @@ package keeper import ( - "bytes" - "fmt" "time" + "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/x/auction/types" ) -// set an auction in the store, adding a new ID, and setting indexes -func (k Keeper) storeNewAuction(ctx sdk.Context, auction types.Auction) (types.ID, sdk.Error) { - // get ID - newAuctionID, err := k.getNextAuctionID(ctx) - if err != nil { - return 0, err - } - // set ID - auction.SetID(newAuctionID) - - // store auction - k.SetAuction(ctx, auction) - k.incrementNextAuctionID(ctx) - return newAuctionID, nil +// SetNextAuctionID stores an ID to be used for the next created auction +func (k Keeper) SetNextAuctionID(ctx sdk.Context, id types.ID) { + store := ctx.KVStore(k.storeKey) + store.Set(types.NextAuctionIDKey, id.Bytes()) } -// getNextAuctionID gets the next available global AuctionID -func (k Keeper) getNextAuctionID(ctx sdk.Context) (types.ID, sdk.Error) { - // get next ID from store +// GetNextAuctionID reads the next available global ID from store +func (k Keeper) GetNextAuctionID(ctx sdk.Context) (types.ID, sdk.Error) { store := ctx.KVStore(k.storeKey) - bz := store.Get(k.getNextAuctionIDKey()) + bz := store.Get(types.NextAuctionIDKey) if bz == nil { - // if not found, set the id at 0 - bz = k.cdc.MustMarshalBinaryLengthPrefixed(types.ID(0)) - store.Set(k.getNextAuctionIDKey(), bz) - // TODO Set auction ID in genesis - //return 0, ErrInvalidGenesis(keeper.codespace, "InitialProposalID never set") + //return 0, types.ErrInvalidGenesis(k.codespace, "initial auction ID hasn't been set") // TODO create error + return 0, sdk.ErrInternal("initial auction ID hasn't been set") } - var auctionID types.ID - k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &auctionID) - return auctionID, nil + return types.NewIDFromBytes(bz), 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 +func (k Keeper) IncrementNextAuctionID(ctx sdk.Context) sdk.Error { + id, err := k.GetNextAuctionID(ctx) + if err != nil { + return err } - 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) - + k.SetNextAuctionID(ctx, id+1) return nil } +// storeNewAuction stores an auction, adding a new ID, and setting indexes +func (k Keeper) storeNewAuction(ctx sdk.Context, auction types.Auction) (types.ID, sdk.Error) { + newAuctionID, err := k.GetNextAuctionID(ctx) + if err != nil { + return 0, err + } + auction.SetID(newAuctionID) + + k.SetAuction(ctx, auction) + + err = k.IncrementNextAuctionID(ctx) + if err != nil { + return 0, err + } + return newAuctionID, nil +} + +// TODO should get/set/delete be responsible for updating auctionByTime index? + // 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()) - } + // existingAuction, found := k.GetAuction(ctx, auction.GetID()) + // if found { + // k.removeFromQueue(ctx, existingAuction.GetEndTime(), existingAuction.GetID()) + // } // store auction - store := ctx.KVStore(k.storeKey) + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) bz := k.cdc.MustMarshalBinaryLengthPrefixed(auction) - store.Set(k.getAuctionKey(auction.GetID()), bz) + store.Set(types.GetAuctionKey(auction.GetID()), bz) // add to the queue - k.InsertIntoQueue(ctx, auction.GetEndTime(), auction.GetID()) + //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)) + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) + bz := store.Get(types.GetAuctionKey(auctionID)) if bz == nil { return auction, false } @@ -97,91 +90,59 @@ func (k Keeper) GetAuction(ctx sdk.Context, auctionID types.ID) (types.Auction, // 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) - } + //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)) + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) + store.Delete(types.GetAuctionKey(auctionID)) } // Inserts a AuctionID into the queue at endTime func (k Keeper) InsertIntoQueue(ctx sdk.Context, endTime time.Time, 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, - ) + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) + store.Set(types.GetAuctionByTimeKey(endTime, auctionID), auctionID.Bytes()) } // removes an auctionID from the queue -func (k Keeper) removeFromQueue(ctx sdk.Context, endTime time.Time, auctionID types.ID) { - store := ctx.KVStore(k.storeKey) - store.Delete(getQueueElementKey(endTime, auctionID)) +func (k Keeper) RemoveFromQueue(ctx sdk.Context, endTime time.Time, auctionID types.ID) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) + store.Delete(types.GetAuctionByTimeKey(endTime, auctionID)) } -// Returns an iterator for all the auctions in the queue that expire by endTime -func (k Keeper) GetQueueIterator(ctx sdk.Context, endTime time.Time) 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) +func (k Keeper) IterateAuctionsByTime(ctx sdk.Context, inclusiveCutoffTime time.Time, cb func(auctionID types.ID) (stop bool)) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) + iterator := store.Iterator( + nil, // start at the very start of the prefix store + sdk.PrefixEndBytes(sdk.FormatTimeBytes(inclusiveCutoffTime)), // include any keys with times equal to inclusiveCutoffTime ) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + // TODO get the auction ID - either read from store, or extract from key + auctionID := types.NewIDFromBytes(iterator.Value()) + + if cb(auctionID) { + break + } + } } -// 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) -} +// IterateAuctions provides an iterator over all stored auctions. For +// each auction, cb will be called. If the cb returns true, the iterator +// will close and stop. +func (k Keeper) IterateAuctions(ctx sdk.Context, cb func(auction types.Auction) (stop bool)) { + iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) -var queueKeyPrefix = []byte("queue") -var keyDelimiter = []byte(":") + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var auction types.Auction + k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &auction) -// Returns half a key for an auctionID in the queue, it missed the id off the end -func getQueueElementKeyPrefix(endTime time.Time) []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 time.Time, 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 + if cb(auction) { + break + } + } } diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index aa1e6e96..dbe1a66a 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -1,6 +1,7 @@ package types import ( + "encoding/binary" "fmt" "strconv" "time" @@ -9,6 +10,27 @@ import ( "github.com/cosmos/cosmos-sdk/x/supply" ) +// 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 +} +func NewIDFromBytes(bz []byte) ID { + return ID(binary.BigEndian.Uint64(bz)) + +} +func (id ID) Bytes() []byte { + bz := make([]byte, 8) + binary.BigEndian.PutUint64(bz, uint64(id)) + return bz +} + // Auction is an interface to several types of auction. type Auction interface { GetID() ID @@ -29,23 +51,11 @@ type BaseAuction struct { MaxEndTime time.Time // 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 -} - // 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 } +func (a *BaseAuction) SetID(id ID) { a.ID = id } // TODO if this returns a new auction with ID then no pointers are needed // GetBid getter for auction bid func (a *BaseAuction) GetBidder() sdk.AccAddress { return a.Bidder } @@ -76,15 +86,15 @@ type ForwardAuction struct { } // NewForwardAuction creates a new forward auction -func NewForwardAuction(seller string, lot sdk.Coin, bidDenom string, EndTime time.Time) ForwardAuction { +func NewForwardAuction(seller string, lot sdk.Coin, bidDenom string, endTime time.Time) ForwardAuction { auction := ForwardAuction{&BaseAuction{ // no ID Initiator: seller, Lot: lot, Bidder: nil, // TODO on the first place bid, 0 coins will be sent to this address, check if this causes problems or can be avoided Bid: sdk.NewInt64Coin(bidDenom, 0), - EndTime: EndTime, - MaxEndTime: EndTime, + EndTime: endTime, + MaxEndTime: endTime, }} // output := BankOutput{seller, lot} return auction diff --git a/x/auction/types/keys.go b/x/auction/types/keys.go index cf2153b3..fc20ef14 100644 --- a/x/auction/types/keys.go +++ b/x/auction/types/keys.go @@ -1,5 +1,11 @@ package types +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + const ( // ModuleName The name that will be used throughout the module ModuleName = "auction" @@ -13,3 +19,19 @@ const ( // DefaultParamspace default name for parameter store DefaultParamspace = ModuleName ) + +// TODO use cont to keep immutability? +var ( + AuctionKeyPrefix = []byte{0x00} // prefix for keys that store auctions + AuctionByTimeKeyPrefix = []byte{0x01} // prefix for keys that are part of the auctionsByTime index + + NextAuctionIDKey = []byte{0x02} +) + +func GetAuctionKey(auctionID ID) []byte { + return auctionID.Bytes() +} + +func GetAuctionByTimeKey(endTime time.Time, auctionID ID) []byte { + return append(sdk.FormatTimeBytes(endTime), auctionID.Bytes()...) +} From c786850b1cf7975ffaab8ea41667b9402f8be4ae Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 28 Dec 2019 17:08:51 +0000 Subject: [PATCH 27/99] move store methods to keeper.go --- x/auction/keeper/keeper.go | 142 +++++++++++++++++++++++++++++++++++ x/auction/keeper/store.go | 148 ------------------------------------- 2 files changed, 142 insertions(+), 148 deletions(-) delete mode 100644 x/auction/keeper/store.go diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index fe4cd04a..f91774c9 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -1,7 +1,10 @@ package keeper import ( + "time" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/params/subspace" @@ -25,3 +28,142 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, supplyKeeper types.Suppl paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()), } } + +// SetNextAuctionID stores an ID to be used for the next created auction +func (k Keeper) SetNextAuctionID(ctx sdk.Context, id types.ID) { + store := ctx.KVStore(k.storeKey) + store.Set(types.NextAuctionIDKey, id.Bytes()) +} + +// GetNextAuctionID reads the next available global ID from store +// TODO might be nicer to convert not found error to a panic, it's not an error that can be recovered from +func (k Keeper) GetNextAuctionID(ctx sdk.Context) (types.ID, sdk.Error) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.NextAuctionIDKey) + if bz == nil { + //return 0, types.ErrInvalidGenesis(k.codespace, "initial auction ID hasn't been set") // TODO create error + return 0, sdk.ErrInternal("initial auction ID hasn't been set") + } + return types.NewIDFromBytes(bz), nil +} + +// incrementNextAuctionID increments the global ID in the store by 1 +func (k Keeper) IncrementNextAuctionID(ctx sdk.Context) sdk.Error { + id, err := k.GetNextAuctionID(ctx) + if err != nil { + return err + } + k.SetNextAuctionID(ctx, id+1) + return nil +} + +// storeNewAuction stores an auction, adding a new ID, and setting indexes +func (k Keeper) storeNewAuction(ctx sdk.Context, auction types.Auction) (types.ID, sdk.Error) { + newAuctionID, err := k.GetNextAuctionID(ctx) + if err != nil { + return 0, err + } + auction = auction.WithID(newAuctionID) + + k.SetAuction(ctx, auction) + + err = k.IncrementNextAuctionID(ctx) + if err != nil { + return 0, err + } + return newAuctionID, nil +} + +// TODO should get/set/delete be responsible for updating auctionByTime index? + +// 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 := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) + bz := k.cdc.MustMarshalBinaryLengthPrefixed(auction) + store.Set(types.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 := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) + bz := store.Get(types.GetAuctionKey(auctionID)) + if bz == nil { + return auction, false + } + + 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 := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) + store.Delete(types.GetAuctionKey(auctionID)) +} + +// Inserts a AuctionID into the queue at endTime +func (k Keeper) InsertIntoQueue(ctx sdk.Context, endTime time.Time, auctionID types.ID) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) + store.Set(types.GetAuctionByTimeKey(endTime, auctionID), auctionID.Bytes()) +} + +// removes an auctionID from the queue +func (k Keeper) RemoveFromQueue(ctx sdk.Context, endTime time.Time, auctionID types.ID) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) + store.Delete(types.GetAuctionByTimeKey(endTime, auctionID)) +} + +func (k Keeper) IterateAuctionsByTime(ctx sdk.Context, inclusiveCutoffTime time.Time, cb func(auctionID types.ID) (stop bool)) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) + iterator := store.Iterator( + nil, // start at the very start of the prefix store + sdk.PrefixEndBytes(sdk.FormatTimeBytes(inclusiveCutoffTime)), // include any keys with times equal to inclusiveCutoffTime + ) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + // TODO get the auction ID - either read from store, or extract from key + auctionID := types.NewIDFromBytes(iterator.Value()) + + if cb(auctionID) { + break + } + } +} + +// IterateAuctions provides an iterator over all stored auctions. For +// each auction, cb will be called. If the cb returns true, the iterator +// will close and stop. +func (k Keeper) IterateAuctions(ctx sdk.Context, cb func(auction types.Auction) (stop bool)) { + iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var auction types.Auction + k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &auction) + + if cb(auction) { + break + } + } +} diff --git a/x/auction/keeper/store.go b/x/auction/keeper/store.go deleted file mode 100644 index f65375a6..00000000 --- a/x/auction/keeper/store.go +++ /dev/null @@ -1,148 +0,0 @@ -package keeper - -import ( - "time" - - "github.com/cosmos/cosmos-sdk/store/prefix" - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/kava-labs/kava/x/auction/types" -) - -// SetNextAuctionID stores an ID to be used for the next created auction -func (k Keeper) SetNextAuctionID(ctx sdk.Context, id types.ID) { - store := ctx.KVStore(k.storeKey) - store.Set(types.NextAuctionIDKey, id.Bytes()) -} - -// GetNextAuctionID reads the next available global ID from store -func (k Keeper) GetNextAuctionID(ctx sdk.Context) (types.ID, sdk.Error) { - store := ctx.KVStore(k.storeKey) - bz := store.Get(types.NextAuctionIDKey) - if bz == nil { - //return 0, types.ErrInvalidGenesis(k.codespace, "initial auction ID hasn't been set") // TODO create error - return 0, sdk.ErrInternal("initial auction ID hasn't been set") - } - return types.NewIDFromBytes(bz), nil -} - -// incrementNextAuctionID increments the global ID in the store by 1 -func (k Keeper) IncrementNextAuctionID(ctx sdk.Context) sdk.Error { - id, err := k.GetNextAuctionID(ctx) - if err != nil { - return err - } - k.SetNextAuctionID(ctx, id+1) - return nil -} - -// storeNewAuction stores an auction, adding a new ID, and setting indexes -func (k Keeper) storeNewAuction(ctx sdk.Context, auction types.Auction) (types.ID, sdk.Error) { - newAuctionID, err := k.GetNextAuctionID(ctx) - if err != nil { - return 0, err - } - auction.SetID(newAuctionID) - - k.SetAuction(ctx, auction) - - err = k.IncrementNextAuctionID(ctx) - if err != nil { - return 0, err - } - return newAuctionID, nil -} - -// TODO should get/set/delete be responsible for updating auctionByTime index? - -// 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 := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) - bz := k.cdc.MustMarshalBinaryLengthPrefixed(auction) - store.Set(types.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 := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) - bz := store.Get(types.GetAuctionKey(auctionID)) - if bz == nil { - return auction, false - } - - 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 := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) - store.Delete(types.GetAuctionKey(auctionID)) -} - -// Inserts a AuctionID into the queue at endTime -func (k Keeper) InsertIntoQueue(ctx sdk.Context, endTime time.Time, auctionID types.ID) { - store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) - store.Set(types.GetAuctionByTimeKey(endTime, auctionID), auctionID.Bytes()) -} - -// removes an auctionID from the queue -func (k Keeper) RemoveFromQueue(ctx sdk.Context, endTime time.Time, auctionID types.ID) { - store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) - store.Delete(types.GetAuctionByTimeKey(endTime, auctionID)) -} - -func (k Keeper) IterateAuctionsByTime(ctx sdk.Context, inclusiveCutoffTime time.Time, cb func(auctionID types.ID) (stop bool)) { - store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) - iterator := store.Iterator( - nil, // start at the very start of the prefix store - sdk.PrefixEndBytes(sdk.FormatTimeBytes(inclusiveCutoffTime)), // include any keys with times equal to inclusiveCutoffTime - ) - - defer iterator.Close() - for ; iterator.Valid(); iterator.Next() { - // TODO get the auction ID - either read from store, or extract from key - auctionID := types.NewIDFromBytes(iterator.Value()) - - if cb(auctionID) { - break - } - } -} - -// IterateAuctions provides an iterator over all stored auctions. For -// each auction, cb will be called. If the cb returns true, the iterator -// will close and stop. -func (k Keeper) IterateAuctions(ctx sdk.Context, cb func(auction types.Auction) (stop bool)) { - iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) - - defer iterator.Close() - for ; iterator.Valid(); iterator.Next() { - var auction types.Auction - k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &auction) - - if cb(auction) { - break - } - } -} From c867e8ba9eb032e52fbce644119f13f0571987f8 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 28 Dec 2019 17:16:08 +0000 Subject: [PATCH 28/99] move nextAuctionID from params to genState --- x/auction/alias.go | 2 -- x/auction/genesis.go | 9 ++++++++- x/auction/types/genesis.go | 6 ++++-- x/auction/types/params.go | 16 +++------------- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/x/auction/alias.go b/x/auction/alias.go index 33b64525..799cc625 100644 --- a/x/auction/alias.go +++ b/x/auction/alias.go @@ -17,7 +17,6 @@ const ( DefaultParamspace = types.DefaultParamspace DefaultMaxAuctionDuration = types.DefaultMaxAuctionDuration DefaultMaxBidDuration = types.DefaultMaxBidDuration - DefaultStartingAuctionID = types.DefaultStartingAuctionID QueryGetAuction = types.QueryGetAuction ) @@ -42,7 +41,6 @@ var ( ModuleCdc = types.ModuleCdc KeyAuctionBidDuration = types.KeyAuctionBidDuration KeyAuctionDuration = types.KeyAuctionDuration - KeyAuctionStartingID = types.KeyAuctionStartingID ) type ( diff --git a/x/auction/genesis.go b/x/auction/genesis.go index 7b16562b..6136df9e 100644 --- a/x/auction/genesis.go +++ b/x/auction/genesis.go @@ -6,6 +6,8 @@ import ( // InitGenesis - initializes the store state from genesis data func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { + keeper.SetNextAuctionID(ctx, data.NextAuctionID) + keeper.SetParams(ctx, data.AuctionParams) for _, a := range data.Auctions { @@ -15,6 +17,11 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { // ExportGenesis returns a GenesisState for a given context and keeper. func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { + nextAuctionID, err := keeper.GetNextAuctionID(ctx) + if err != nil { + panic(err) + } + params := keeper.GetParams(ctx) var genAuctions GenesisAuctions @@ -23,5 +30,5 @@ func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { return false }) - return NewGenesisState(params, genAuctions) + return NewGenesisState(nextAuctionID, params, genAuctions) } diff --git a/x/auction/types/genesis.go b/x/auction/types/genesis.go index 31c1efb5..15530961 100644 --- a/x/auction/types/genesis.go +++ b/x/auction/types/genesis.go @@ -9,13 +9,15 @@ type GenesisAuctions []Auction // GenesisState - auction state that must be provided at genesis type GenesisState struct { + NextAuctionID ID 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 { +func NewGenesisState(nextID ID, ap AuctionParams, ga GenesisAuctions) GenesisState { return GenesisState{ + NextAuctionID: nextID, AuctionParams: ap, Auctions: ga, } @@ -23,7 +25,7 @@ func NewGenesisState(ap AuctionParams, ga GenesisAuctions) GenesisState { // DefaultGenesisState defines default genesis state for auction module func DefaultGenesisState() GenesisState { - return NewGenesisState(DefaultAuctionParams(), GenesisAuctions{}) + return NewGenesisState(ID(0), DefaultAuctionParams(), GenesisAuctions{}) } // Equal checks whether two GenesisState structs are equivalent diff --git a/x/auction/types/params.go b/x/auction/types/params.go index af189ed2..8f9b59ac 100644 --- a/x/auction/types/params.go +++ b/x/auction/types/params.go @@ -14,8 +14,6 @@ const ( DefaultMaxAuctionDuration time.Duration = 2 * 24 * time.Hour // DefaultBidDuration how long an auction gets extended when someone bids, roughly 3 hours in blocks DefaultMaxBidDuration time.Duration = 3 * time.Hour - // DefaultStartingAuctionID what the id of the first auction will be - DefaultStartingAuctionID ID = ID(0) ) // Parameter keys @@ -23,7 +21,6 @@ var ( // ParamStoreKeyAuctionParams Param store key for auction params KeyAuctionBidDuration = []byte("MaxBidDuration") KeyAuctionDuration = []byte("MaxAuctionDuration") - KeyAuctionStartingID = []byte("StartingAuctionID") ) var _ subspace.ParamSet = &AuctionParams{} @@ -32,15 +29,13 @@ var _ subspace.ParamSet = &AuctionParams{} type AuctionParams struct { MaxAuctionDuration time.Duration `json:"max_auction_duration" yaml:"max_auction_duration"` // max length of auction, in blocks MaxBidDuration time.Duration `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 time.Duration, bidDuration time.Duration, startingID ID) AuctionParams { +func NewAuctionParams(maxAuctionDuration time.Duration, bidDuration time.Duration) AuctionParams { return AuctionParams{ MaxAuctionDuration: maxAuctionDuration, MaxBidDuration: bidDuration, - StartingAuctionID: startingID, } } @@ -49,7 +44,6 @@ func DefaultAuctionParams() AuctionParams { return NewAuctionParams( DefaultMaxAuctionDuration, DefaultMaxBidDuration, - DefaultStartingAuctionID, ) } @@ -65,7 +59,6 @@ func (ap *AuctionParams) ParamSetPairs() subspace.ParamSetPairs { return subspace.ParamSetPairs{ {KeyAuctionBidDuration, &ap.MaxBidDuration}, {KeyAuctionDuration, &ap.MaxAuctionDuration}, - {KeyAuctionStartingID, &ap.StartingAuctionID}, } } @@ -80,14 +73,11 @@ func (ap AuctionParams) Equal(ap2 AuctionParams) bool { 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) + Max Bid Duration: %s`, ap.MaxAuctionDuration, ap.MaxBidDuration) } // Validate checks that the parameters have valid values. func (ap AuctionParams) Validate() error { - if ap.StartingAuctionID <= ID(0) { - return fmt.Errorf("starting auction ID should be positive, is %v", ap.StartingAuctionID) - } + // TODO check durations are within acceptable limits, if needed return nil } From ac27571d15a9ac7394f279f82ce07287f4762e64 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 28 Dec 2019 17:17:46 +0000 Subject: [PATCH 29/99] simplify auction type to not use pointers --- x/auction/keeper/auctions.go | 2 +- x/auction/types/auctions.go | 39 +++++++++++++++++++++--------------- x/auction/types/codec.go | 6 +++--- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index b5ca5850..f292a083 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -96,7 +96,7 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID types.ID, bidder sdk.AccAddr return err } default: - panic("unrecognized auction type") + panic(fmt.Sprintf("unrecognized auction type: %T", auction)) } // store updated auction diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index dbe1a66a..72bd8ed8 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -13,6 +13,7 @@ import ( // ID type for auction IDs type ID uint64 +// TODO can this be removed? // 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 @@ -34,7 +35,7 @@ func (id ID) Bytes() []byte { // Auction is an interface to several types of auction. type Auction interface { GetID() ID - SetID(ID) + WithID(ID) Auction GetBidder() sdk.AccAddress GetLot() sdk.Coin GetEndTime() time.Time @@ -52,21 +53,18 @@ type BaseAuction struct { } // 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 } // TODO if this returns a new auction with ID then no pointers are needed +func (a BaseAuction) GetID() ID { return a.ID } // GetBid getter for auction bid -func (a *BaseAuction) GetBidder() sdk.AccAddress { return a.Bidder } +func (a BaseAuction) GetBidder() sdk.AccAddress { return a.Bidder } // GetLot getter for auction lot -func (a *BaseAuction) GetLot() sdk.Coin { return a.Lot } +func (a BaseAuction) GetLot() sdk.Coin { return a.Lot } // GetEndTime getter for auction end time -func (a *BaseAuction) GetEndTime() time.Time { return a.EndTime } +func (a BaseAuction) GetEndTime() time.Time { return a.EndTime } -func (a *BaseAuction) String() string { +func (a BaseAuction) String() string { return fmt.Sprintf(`Auction %d: Initiator: %s Lot: %s @@ -82,12 +80,15 @@ func (a *BaseAuction) String() string { // ForwardAuction type for forward auctions type ForwardAuction struct { - *BaseAuction + BaseAuction } +// WithID returns an auction wtih the ID set +func (a ForwardAuction) WithID(id ID) Auction { a.ID = id; return a } + // NewForwardAuction creates a new forward auction func NewForwardAuction(seller string, lot sdk.Coin, bidDenom string, endTime time.Time) ForwardAuction { - auction := ForwardAuction{&BaseAuction{ + auction := ForwardAuction{BaseAuction{ // no ID Initiator: seller, Lot: lot, @@ -102,16 +103,19 @@ func NewForwardAuction(seller string, lot sdk.Coin, bidDenom string, endTime tim // ReverseAuction type for reverse auctions type ReverseAuction struct { - *BaseAuction + BaseAuction } +// WithID returns an auction wtih the ID set +func (a ReverseAuction) WithID(id ID) Auction { a.ID = id; return a } + // NewReverseAuction creates a new reverse auction func NewReverseAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime time.Time) ReverseAuction { // TODO setting the bidder here is a bit hacky // Needs to be set so that when the first bid is placed, it is paid out to the initiator. // Setting to the module account address bypasses calling supply.SendCoinsFromModuleToModule, instead calls SendCoinsFromModuleToModule. Not a problem currently but if checks/logic regarding modules accounts where added to those methods they would be bypassed. // Alternative: set address to nil, and catch it in an if statement in place bid - auction := ReverseAuction{&BaseAuction{ + auction := ReverseAuction{BaseAuction{ // no ID Initiator: buyerModAccName, Lot: initialLot, @@ -125,12 +129,15 @@ func NewReverseAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin // ForwardReverseAuction type for forward reverse auction type ForwardReverseAuction struct { - *BaseAuction + BaseAuction MaxBid sdk.Coin OtherPerson sdk.AccAddress // TODO rename, this is normally the original CDP owner, will have to be updated to account for deposits } -func (a *ForwardReverseAuction) String() string { +// WithID returns an auction wtih the ID set +func (a ForwardReverseAuction) WithID(id ID) Auction { a.ID = id; return a } + +func (a ForwardReverseAuction) String() string { return fmt.Sprintf(`Auction %d: Initiator: %s Lot: %s @@ -149,7 +156,7 @@ func (a *ForwardReverseAuction) String() string { // NewForwardReverseAuction creates a new forward reverse auction func NewForwardReverseAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, otherPerson sdk.AccAddress) ForwardReverseAuction { auction := ForwardReverseAuction{ - BaseAuction: &BaseAuction{ + BaseAuction: BaseAuction{ // no ID Initiator: seller, Lot: lot, diff --git a/x/auction/types/codec.go b/x/auction/types/codec.go index 00e97c8b..75ede175 100644 --- a/x/auction/types/codec.go +++ b/x/auction/types/codec.go @@ -17,7 +17,7 @@ func RegisterCodec(cdc *codec.Codec) { // 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) + cdc.RegisterConcrete(ForwardAuction{}, "auction/ForwardAuction", nil) + cdc.RegisterConcrete(ReverseAuction{}, "auction/ReverseAuction", nil) + cdc.RegisterConcrete(ForwardReverseAuction{}, "auction/ForwardReverseAuction", nil) } From 0d72f47bc29ffceaff8da2940e604c22fbf5ed0f Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 28 Dec 2019 18:46:53 +0000 Subject: [PATCH 30/99] add basic auction tests --- app/app.go | 10 +- x/auction/keeper/auctions.go | 34 +- x/auction/keeper/auctions_test.go | 145 +++++ x/auction/keeper/integration_test.go | 17 + x/auction/keeper/keeper_test.go | 115 +--- x/auction/types/auctions.go | 14 +- x/auction/types/auctions_test.go | 777 +++++++++++++-------------- 7 files changed, 587 insertions(+), 525 deletions(-) create mode 100644 x/auction/keeper/auctions_test.go create mode 100644 x/auction/keeper/integration_test.go diff --git a/app/app.go b/app/app.go index 90f4f5fe..d8eeec15 100644 --- a/app/app.go +++ b/app/app.go @@ -61,7 +61,7 @@ var ( supply.AppModuleBasic{}, auction.AppModuleBasic{}, cdp.AppModuleBasic{}, - liquidator.AppModuleBasic{}, + //liquidator.AppModuleBasic{}, pricefeed.AppModuleBasic{}, ) @@ -74,6 +74,8 @@ var ( staking.NotBondedPoolName: {supply.Burner, supply.Staking}, gov.ModuleName: {supply.Burner}, validatorvesting.ModuleName: {supply.Burner}, + auction.ModuleName: nil, + liquidator.ModuleName: {supply.Minter, supply.Burner}, } ) @@ -238,7 +240,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, app.auctionKeeper = auction.NewKeeper( app.cdc, keys[auction.StoreKey], - app.supplyKeeper, // CDP keeper standing in for bank + app.supplyKeeper, auctionSubspace) // app.liquidatorKeeper = liquidator.NewKeeper( // app.cdc, @@ -269,7 +271,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper), auction.NewAppModule(app.auctionKeeper), cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper), - liquidator.NewAppModule(app.liquidatorKeeper), + //liquidator.NewAppModule(app.liquidatorKeeper), pricefeed.NewAppModule(app.pricefeedKeeper), ) @@ -289,7 +291,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, auth.ModuleName, validatorvesting.ModuleName, distr.ModuleName, staking.ModuleName, bank.ModuleName, slashing.ModuleName, gov.ModuleName, mint.ModuleName, supply.ModuleName, crisis.ModuleName, genutil.ModuleName, - pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, liquidator.ModuleName, // TODO is this order ok? + pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, //liquidator.ModuleName, // TODO is this order ok? ) app.mm.RegisterInvariants(&app.crisisKeeper) diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index f292a083..2b23da74 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -20,7 +20,7 @@ func (k Keeper) StartForwardAuction(ctx sdk.Context, seller string, lot sdk.Coin return 0, err } // store the auction - auctionID, err := k.storeNewAuction(ctx, auction) // TODO does this need to be a pointer to satisfy the interface? + auctionID, err := k.storeNewAuction(ctx, auction) if err != nil { return 0, err } @@ -38,7 +38,7 @@ func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer string, bid sdk.Coin, return 0, sdk.ErrInternal("module does not have minting permissions") } // store the auction - auctionID, err := k.storeNewAuction(ctx, &auction) + auctionID, err := k.storeNewAuction(ctx, auction) if err != nil { return 0, err } @@ -51,12 +51,12 @@ func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller string, lot s auction := types.NewForwardReverseAuction(seller, lot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), maxBid, otherPerson) // take coins from module account - err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.Coins{lot}) + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) if err != nil { return 0, err } // store the auction - auctionID, err := k.storeNewAuction(ctx, &auction) + auctionID, err := k.storeNewAuction(ctx, auction) if err != nil { return 0, err } @@ -64,6 +64,7 @@ func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller string, lot s } // PlaceBid places a bid on any auction. +// TODO passing bid and lot is weird when only one needed 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 @@ -72,11 +73,18 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID types.ID, bidder sdk.AccAddr return sdk.ErrInternal("auction doesn't exist") } - // check end time + // validate if ctx.BlockTime().After(auction.GetEndTime()) { return sdk.ErrInternal("auction has closed") } + if auction.GetBid().Denom != bid.Denom { + return sdk.ErrInternal("bid has incorrect denom") + } + if auction.GetLot().Denom != lot.Denom { + return sdk.ErrInternal("lot has incorrect denom") + } + // place bid var err sdk.Error var a types.Auction switch auc := auction.(type) { @@ -124,7 +132,7 @@ func (k Keeper) PlaceBidForward(ctx sdk.Context, a types.ForwardAuction, bidder if err != nil { return a, err } - err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, bidder, sdk.NewCoins(bidAmtToReturn)) + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(bidAmtToReturn)) if err != nil { return a, err } @@ -185,7 +193,7 @@ func (k Keeper) PlaceBidForwardReverse(ctx sdk.Context, a types.ForwardReverseAu if err != nil { return a, err } - err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, bidder, sdk.NewCoins(bidAmtToReturn)) + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(bidAmtToReturn)) if err != nil { return a, err } @@ -205,7 +213,7 @@ func (k Keeper) PlaceBidForwardReverse(ctx sdk.Context, a types.ForwardReverseAu // increment timeout a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration), a.MaxEndTime) - return types.ForwardReverseAuction{}, nil + return a, nil } func (k Keeper) PlaceBidReverse(ctx sdk.Context, a types.ReverseAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.ReverseAuction, sdk.Error) { // Validate New Bid @@ -228,7 +236,7 @@ func (k Keeper) PlaceBidReverse(ctx sdk.Context, a types.ReverseAuction, bidder if err != nil { return a, err } - err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, bidder, sdk.NewCoins(bidAmtToReturn)) + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(bidAmtToReturn)) if err != nil { return a, err } @@ -297,7 +305,11 @@ func (k Keeper) PayoutAuctionLot(ctx sdk.Context, a types.Auction) sdk.Error { return nil } -// FIXME stand in func for compiler +// earliestTime returns the earliest of two times. func earliestTime(t1, t2 time.Time) time.Time { - return t1 + if t1.Before(t2) { + return t1 + } else { + return t2 // also returned if times are equal + } } diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go new file mode 100644 index 00000000..1cfd254a --- /dev/null +++ b/x/auction/keeper/auctions_test.go @@ -0,0 +1,145 @@ +package keeper_test + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/x/auth" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + "github.com/cosmos/cosmos-sdk/x/supply" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/auction/types" + "github.com/kava-labs/kava/x/liquidator" +) + +func TestForwardAuctionBasic(t *testing.T) { + // Setup + _, addrs := app.GeneratePrivKeyAddressPairs(1) + buyer := addrs[0] + sellerModName := liquidator.ModuleName + sellerAddr := supply.NewModuleAddress(sellerModName) + + tApp := app.NewTestApp() + + sellerAcc := supply.NewEmptyModuleAccount(sellerModName, supply.Burner) // forward auctions burn proceeds + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100)))) + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + sellerAcc, + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Create an auction (lot: 20 token1, initialBid: 0 token2) + auctionID, err := keeper.StartForwardAuction(ctx, sellerModName, c("token1", 20), "token2") // lot, bid denom + require.NoError(t, err) + // Check seller's coins have decreased + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) + + // PlaceBid (bid: 10 token, lot: same as starting) + require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 10), c("token1", 20))) // bid, lot + // Check buyer's coins have decreased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) + // Check seller's coins have not increased (because proceeds are burned) + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) + + // Close auction at just at auction expiry time + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration)) + require.NoError(t, keeper.CloseAuction(ctx, auctionID)) + // Check buyer's coins increased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90))) +} + +func TestReverseAuctionBasic(t *testing.T) { + // Setup + _, addrs := app.GeneratePrivKeyAddressPairs(1) + seller := addrs[0] + buyerModName := liquidator.ModuleName + buyerAddr := supply.NewModuleAddress(buyerModName) + + tApp := app.NewTestApp() + + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(seller, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + supply.NewEmptyModuleAccount(buyerModName, supply.Minter), // reverse auctions mint payout + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Start auction + auctionID, err := keeper.StartReverseAuction(ctx, buyerModName, c("token1", 20), c("token2", 99999)) // buyer, bid, initialLot + require.NoError(t, err) + // Check buyer's coins have not decreased, as lot is minted at the end + tApp.CheckBalance(t, ctx, buyerAddr, nil) // zero coins + + // Place a bid + require.NoError(t, keeper.PlaceBid(ctx, 0, seller, c("token1", 20), c("token2", 10))) // bid, lot + // Check seller's coins have decreased + tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) + // Check buyer's coins have increased + tApp.CheckBalance(t, ctx, buyerAddr, cs(c("token1", 20))) + + // Close auction at just after auction expiry + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration)) + require.NoError(t, keeper.CloseAuction(ctx, auctionID)) + // Check seller's coins increased + tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) +} + +func TestForwardReverseAuctionBasic(t *testing.T) { + // Setup + _, addrs := app.GeneratePrivKeyAddressPairs(2) + buyer := addrs[0] + recipient := addrs[1] + sellerModName := liquidator.ModuleName + sellerAddr := supply.NewModuleAddress(sellerModName) + + tApp := app.NewTestApp() + sellerAcc := supply.NewEmptyModuleAccount(sellerModName) + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100)))) + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + auth.NewBaseAccount(recipient, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + sellerAcc, + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Start auction + auctionID, err := keeper.StartForwardReverseAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), recipient) // seller, lot, maxBid, otherPerson + require.NoError(t, err) + // Check seller's coins have decreased + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) + + // Place a forward bid + require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 10), c("token1", 20))) // bid, lot + // Check bidder's coins have decreased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) + // Check seller's coins have increased + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 110))) + // Check recipient has not received coins + tApp.CheckBalance(t, ctx, recipient, cs(c("token1", 100), c("token2", 100))) + + // Place a reverse bid + require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50), c("token1", 15))) // bid, lot + // Check bidder's coins have decreased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50))) + // Check seller's coins have increased + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 150))) + // Check "recipient" has received coins + tApp.CheckBalance(t, ctx, recipient, cs(c("token1", 105), c("token2", 100))) + + // Close auction at just after auction expiry + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration)) + require.NoError(t, keeper.CloseAuction(ctx, auctionID)) + // Check buyer's coins increased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 115), c("token2", 50))) +} diff --git a/x/auction/keeper/integration_test.go b/x/auction/keeper/integration_test.go new file mode 100644 index 00000000..3ad2500e --- /dev/null +++ b/x/auction/keeper/integration_test.go @@ -0,0 +1,17 @@ +package keeper_test + +import ( + 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/kava-labs/kava/app" +) + +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 NewAuthGenStateFromAccs(accounts authexported.GenesisAccounts) app.GenesisState { + authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts) + return app.GenesisState{auth.ModuleName: auth.ModuleCdc.MustMarshalJSON(authGenesis)} +} diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go index 56d6065f..2e0918af 100644 --- a/x/auction/keeper/keeper_test.go +++ b/x/auction/keeper/keeper_test.go @@ -13,128 +13,22 @@ import ( "github.com/kava-labs/kava/x/auction/types" ) -// func TestKeeper_ForwardAuction(t *testing.T) { -// // Setup -// _, addrs := app.GeneratePrivKeyAddressPairs(2) -// seller := addrs[0] -// buyer := addrs[1] - -// tApp := app.NewTestApp() -// tApp.InitializeFromGenesisStates( -// app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}), -// ) - -// ctx := tApp.NewContext(false, abci.Header{}) -// keeper := tApp.GetAuctionKeeper() - -// // Create an auction (lot: 20 t1, initialBid: 0 t2) -// auctionID, err := keeper.StartForwardAuction(ctx, seller, c("token1", 20), c("token2", 0)) // lot, initialBid -// require.NoError(t, err) -// // Check seller's coins have decreased -// tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) - -// // PlaceBid (bid: 10 t2, lot: same as starting) -// require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 10), c("token1", 20))) // bid, lot -// // Check buyer's coins have decreased -// tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) -// // Check seller's coins have increased -// tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) - -// // Close auction at just after auction expiry -// ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration)) -// require.NoError(t, keeper.CloseAuction(ctx, auctionID)) -// // Check buyer's coins increased -// tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90))) -// } - -// func TestKeeper_ReverseAuction(t *testing.T) { -// // Setup -// _, addrs := app.GeneratePrivKeyAddressPairs(2) -// seller := addrs[0] -// buyer := addrs[1] - -// tApp := app.NewTestApp() -// tApp.InitializeFromGenesisStates( -// app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}), -// ) - -// ctx := tApp.NewContext(false, abci.Header{}) -// keeper := tApp.GetAuctionKeeper() - -// // Start auction -// auctionID, err := keeper.StartReverseAuction(ctx, buyer, c("token1", 20), c("token2", 99)) // buyer, bid, initialLot -// require.NoError(t, err) -// // Check buyer's coins have decreased -// tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 1))) - -// // Place a bid -// require.NoError(t, keeper.PlaceBid(ctx, 0, seller, c("token1", 20), c("token2", 10))) // bid, lot -// // Check seller's coins have decreased -// tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) -// // Check buyer's coins have increased -// tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90))) - -// // Close auction at just after auction expiry -// ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration)) -// require.NoError(t, keeper.CloseAuction(ctx, auctionID)) -// // Check seller's coins increased -// tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) -// } - -// func TestKeeper_ForwardReverseAuction(t *testing.T) { -// // Setup -// _, addrs := app.GeneratePrivKeyAddressPairs(3) -// seller := addrs[0] -// buyer := addrs[1] -// recipient := addrs[2] - -// tApp := app.NewTestApp() -// tApp.InitializeFromGenesisStates( -// app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}), -// ) - -// ctx := tApp.NewContext(false, abci.Header{}) -// keeper := tApp.GetAuctionKeeper() - -// // Start auction -// auctionID, err := keeper.StartForwardReverseAuction(ctx, seller, c("token1", 20), c("token2", 50), recipient) // seller, lot, maxBid, otherPerson -// require.NoError(t, err) -// // Check seller's coins have decreased -// tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) - -// // Place a bid -// require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50), c("token1", 15))) // bid, lot -// // Check bidder's coins have decreased -// tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50))) -// // Check seller's coins have increased -// tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 150))) -// // Check "recipient" has received coins -// tApp.CheckBalance(t, ctx, recipient, cs(c("token1", 105), c("token2", 100))) - -// // Close auction at just after auction expiry -// ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration)) -// require.NoError(t, keeper.CloseAuction(ctx, auctionID)) -// // Check buyer's coins increased -// tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 115), c("token2", 50))) -// } - func SetGetDeleteAuction(t *testing.T) { // setup keeper, create auction tApp := app.NewTestApp() keeper := tApp.GetAuctionKeeper() ctx := tApp.NewContext(true, abci.Header{}) someTime := time.Date(43, time.January, 1, 0, 0, 0, 0, time.UTC) // need to specify UTC as tz info is lost on unmarshal - auction := types.NewForwardAuction("some_module", c("usdx", 100), "kava", someTime) id := types.ID(5) - auction.SetID(id) + auction := types.NewForwardAuction("some_module", c("usdx", 100), "kava", someTime).WithID(id) // write and read from store - keeper.SetAuction(ctx, &auction) + keeper.SetAuction(ctx, auction) readAuction, found := keeper.GetAuction(ctx, id) // check before and after match require.True(t, found) - require.Equal(t, &auction, readAuction) + require.Equal(t, auction, readAuction) // check auction is in queue // iter := keeper.GetQueueIterator(ctx, 100000) // require.Equal(t, 1, len(convertIteratorToSlice(keeper, iter))) @@ -245,6 +139,3 @@ func convertIteratorToSlice(keeper keeper.Keeper, iterator sdk.Iterator) []types } return queue } - -func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } -func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 72bd8ed8..9e45ee5c 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -37,6 +37,7 @@ type Auction interface { GetID() ID WithID(ID) Auction GetBidder() sdk.AccAddress + GetBid() sdk.Coin GetLot() sdk.Coin GetEndTime() time.Time } @@ -44,7 +45,7 @@ type Auction interface { // BaseAuction type shared by all Auctions type BaseAuction struct { ID ID - Initiator string // Module who starts the auction. Giving away Lot (aka seller in a forward auction). Restricted to being a module account name rather than any account. + Initiator string // Module that starts the auction. Giving away Lot (aka seller in a forward auction). Restricted to being a module account name rather than any account. 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) @@ -58,6 +59,9 @@ func (a BaseAuction) GetID() ID { return a.ID } // GetBid getter for auction bid func (a BaseAuction) GetBidder() sdk.AccAddress { return a.Bidder } +// GetBid getter for auction lot +func (a BaseAuction) GetBid() sdk.Coin { return a.Bid } + // GetLot getter for auction lot func (a BaseAuction) GetLot() sdk.Coin { return a.Lot } @@ -83,7 +87,7 @@ type ForwardAuction struct { BaseAuction } -// WithID returns an auction wtih the ID set +// WithID returns an auction with the ID set func (a ForwardAuction) WithID(id ID) Auction { a.ID = id; return a } // NewForwardAuction creates a new forward auction @@ -97,7 +101,6 @@ func NewForwardAuction(seller string, lot sdk.Coin, bidDenom string, endTime tim EndTime: endTime, MaxEndTime: endTime, }} - // output := BankOutput{seller, lot} return auction } @@ -106,7 +109,7 @@ type ReverseAuction struct { BaseAuction } -// WithID returns an auction wtih the ID set +// WithID returns an auction with the ID set func (a ReverseAuction) WithID(id ID) Auction { a.ID = id; return a } // NewReverseAuction creates a new reverse auction @@ -134,7 +137,7 @@ type ForwardReverseAuction struct { OtherPerson sdk.AccAddress // TODO rename, this is normally the original CDP owner, will have to be updated to account for deposits } -// WithID returns an auction wtih the ID set +// WithID returns an auction with the ID set func (a ForwardReverseAuction) WithID(id ID) Auction { a.ID = id; return a } func (a ForwardReverseAuction) String() string { @@ -167,6 +170,5 @@ func NewForwardReverseAuction(seller string, lot sdk.Coin, EndTime time.Time, ma MaxBid: maxBid, OtherPerson: otherPerson, } - //output := BankOutput{seller, lot} return auction } diff --git a/x/auction/types/auctions_test.go b/x/auction/types/auctions_test.go index b2252f55..bd0db597 100644 --- a/x/auction/types/auctions_test.go +++ b/x/auction/types/auctions_test.go @@ -1,403 +1,396 @@ package types -import ( - "testing" +// // 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) - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" -) +// 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) -// 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) +// // 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) +// }) +// } +// } - 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) +// 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) - // 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) - }) - } -} +// 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) -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) +// // 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) +// }) +// } +// } - 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) +// 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) - // 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) - }) - } -} +// 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) -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) +// // 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) +// }) +// } +// } - 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) -} +// // defined to avoid cluttering test cases with long function name +// func c(denom string, amount int64) sdk.Coin { +// return sdk.NewInt64Coin(denom, amount) +// } From 8a4109ff266e814b5871a8e39a4a32c66176b77a Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sat, 28 Dec 2019 22:00:04 +0000 Subject: [PATCH 31/99] update endblocker test --- x/auction/abci_test.go | 38 +++++++++++++++++------- x/auction/alias.go | 2 +- x/auction/doc.go | 14 --------- x/auction/keeper/auctions.go | 17 +++++++---- x/auction/keeper/auctions_test.go | 6 ++-- x/auction/keeper/keeper.go | 1 + x/auction/keeper/keeper_test.go | 48 +++++++++++++++---------------- x/auction/types/params.go | 4 +-- 8 files changed, 70 insertions(+), 60 deletions(-) delete mode 100644 x/auction/doc.go diff --git a/x/auction/abci_test.go b/x/auction/abci_test.go index 38941aeb..bb6a6585 100644 --- a/x/auction/abci_test.go +++ b/x/auction/abci_test.go @@ -4,40 +4,53 @@ 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/supply" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/auction" + "github.com/kava-labs/kava/x/liquidator" ) func TestKeeper_EndBlocker(t *testing.T) { // Setup - _, addrs := app.GeneratePrivKeyAddressPairs(1) - seller := addrs[0] + _, addrs := app.GeneratePrivKeyAddressPairs(2) + buyer := addrs[0] + recipient := addrs[1] + sellerModName := liquidator.ModuleName + //sellerAddr := supply.NewModuleAddress(sellerModName) tApp := app.NewTestApp() + sellerAcc := supply.NewEmptyModuleAccount(sellerModName) + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100)))) tApp.InitializeFromGenesisStates( - app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100))}), + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + sellerAcc, + }), ) ctx := tApp.NewContext(true, abci.Header{}) keeper := tApp.GetAuctionKeeper() - auctionID, err := keeper.StartForwardAuction(ctx, seller, c("token1", 20), c("token2", 0)) + auctionID, err := keeper.StartForwardReverseAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), recipient) require.NoError(t, err) + require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 30), c("token1", 20))) - // Run the endblocker, simulating a block height just before auction expiry - preExpiryHeight := ctx.BlockHeight() + int64(auction.DefaultMaxAuctionDuration) - 1 - auction.EndBlocker(ctx.WithBlockHeight(preExpiryHeight), keeper) + // Run the endblocker, simulating a block time 1ns before auction expiry + preExpiryTime := ctx.BlockTime().Add(auction.DefaultBidDuration - 1) + auction.EndBlocker(ctx.WithBlockTime(preExpiryTime), keeper) // Check auction has not been closed yet _, found := keeper.GetAuction(ctx, auctionID) require.True(t, found) - // Run the endblocker, simulating a block height just after auction expiry - expiryHeight := preExpiryHeight + 1 - auction.EndBlocker(ctx.WithBlockHeight(expiryHeight), keeper) + // Run the endblocker, simulating a block time equal to auction expiry + expiryTime := ctx.BlockTime().Add(auction.DefaultBidDuration) + auction.EndBlocker(ctx.WithBlockTime(expiryTime), keeper) // Check auction has been closed _, found = keeper.GetAuction(ctx, auctionID) @@ -46,3 +59,8 @@ func TestKeeper_EndBlocker(t *testing.T) { 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 NewAuthGenStateFromAccs(accounts authexported.GenesisAccounts) app.GenesisState { + authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts) + return app.GenesisState{auth.ModuleName: auth.ModuleCdc.MustMarshalJSON(authGenesis)} +} diff --git a/x/auction/alias.go b/x/auction/alias.go index 799cc625..acf20129 100644 --- a/x/auction/alias.go +++ b/x/auction/alias.go @@ -16,7 +16,7 @@ const ( RouterKey = types.RouterKey DefaultParamspace = types.DefaultParamspace DefaultMaxAuctionDuration = types.DefaultMaxAuctionDuration - DefaultMaxBidDuration = types.DefaultMaxBidDuration + DefaultBidDuration = types.DefaultBidDuration QueryGetAuction = types.QueryGetAuction ) diff --git a/x/auction/doc.go b/x/auction/doc.go deleted file mode 100644 index 79e8def7..00000000 --- a/x/auction/doc.go +++ /dev/null @@ -1,14 +0,0 @@ -/* -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 diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index 2b23da74..a1215624 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -9,7 +9,7 @@ import ( "github.com/kava-labs/kava/x/auction/types" ) -// StartForwardAuction starts a normal auction. +// StartForwardAuction starts a normal auction that mints the sold coins. func (k Keeper) StartForwardAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (types.ID, sdk.Error) { // create auction auction := types.NewForwardAuction(seller, lot, bidDenom, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) @@ -108,7 +108,12 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID types.ID, bidder sdk.AccAddr } // store updated auction - k.SetAuction(ctx, a) // TODO maybe move into above funcs + existing, found := k.GetAuction(ctx, a.GetID()) + if found { + k.RemoveFromQueue(ctx, existing.GetEndTime(), existing.GetID()) + } + k.SetAuction(ctx, a) + k.InsertIntoQueue(ctx, a.GetEndTime(), a.GetID()) return nil } @@ -149,7 +154,7 @@ func (k Keeper) PlaceBidForward(ctx sdk.Context, a types.ForwardAuction, bidder a.Bidder = bidder a.Bid = bid // increment timeout - a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration), a.MaxEndTime) // TODO write a min func for time types + a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultBidDuration), a.MaxEndTime) return a, nil } @@ -211,7 +216,7 @@ func (k Keeper) PlaceBidForwardReverse(ctx sdk.Context, a types.ForwardReverseAu a.Lot = lot a.Bid = bid // increment timeout - a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration), a.MaxEndTime) + a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultBidDuration), a.MaxEndTime) return a, nil } @@ -245,7 +250,7 @@ func (k Keeper) PlaceBidReverse(ctx sdk.Context, a types.ReverseAuction, bidder a.Bidder = bidder a.Lot = lot // increment timeout - a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration), a.MaxEndTime) + a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultBidDuration), a.MaxEndTime) return a, nil } @@ -282,6 +287,7 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID types.ID) sdk.Error { // Delete auction from store (and queue) k.DeleteAuction(ctx, auctionID) + k.RemoveFromQueue(ctx, auction.GetEndTime(), auction.GetID()) return nil } @@ -297,7 +303,6 @@ func (k Keeper) MintAndPayoutAuctionLot(ctx sdk.Context, a types.ReverseAuction) return nil } func (k Keeper) PayoutAuctionLot(ctx sdk.Context, a types.Auction) sdk.Error { - // TODO this function is responsible for the addition of GetBidder and GetLot to auction interface. Could be split in to two funcs that operate on concrete auction types err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.GetBidder(), sdk.NewCoins(a.GetLot())) if err != nil { return err diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index 1cfd254a..77f174e7 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -48,7 +48,7 @@ func TestForwardAuctionBasic(t *testing.T) { tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) // Close auction at just at auction expiry time - ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration)) + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration)) require.NoError(t, keeper.CloseAuction(ctx, auctionID)) // Check buyer's coins increased tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90))) @@ -86,7 +86,7 @@ func TestReverseAuctionBasic(t *testing.T) { tApp.CheckBalance(t, ctx, buyerAddr, cs(c("token1", 20))) // Close auction at just after auction expiry - ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration)) + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration)) require.NoError(t, keeper.CloseAuction(ctx, auctionID)) // Check seller's coins increased tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) @@ -138,7 +138,7 @@ func TestForwardReverseAuctionBasic(t *testing.T) { tApp.CheckBalance(t, ctx, recipient, cs(c("token1", 105), c("token2", 100))) // Close auction at just after auction expiry - ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultMaxBidDuration)) + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration)) require.NoError(t, keeper.CloseAuction(ctx, auctionID)) // Check buyer's coins increased tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 115), c("token2", 50))) diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index f91774c9..dfb7dd49 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -66,6 +66,7 @@ func (k Keeper) storeNewAuction(ctx sdk.Context, auction types.Auction) (types.I auction = auction.WithID(newAuctionID) k.SetAuction(ctx, auction) + k.InsertIntoQueue(ctx, auction.GetEndTime(), auction.GetID()) err = k.IncrementNextAuctionID(ctx) if err != nil { diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go index 2e0918af..f6e9a1d9 100644 --- a/x/auction/keeper/keeper_test.go +++ b/x/auction/keeper/keeper_test.go @@ -94,40 +94,40 @@ func TestIterateAuctionsByTime(t *testing.T) { keeper := tApp.GetAuctionKeeper() ctx := tApp.NewContext(true, abci.Header{}) - // create a list of times - queue := []struct { + // setup byTime index + byTimeIndex := []struct { endTime time.Time auctionID types.ID }{ - {time.Date(84, time.January, 1, 0, 0, 0, 0, time.UTC), 34345345}, - {time.Date(98, time.January, 2, 0, 0, 0, 0, time.UTC), 5}, - {time.Date(98, time.January, 2, 13, 5, 0, 0, time.UTC), 6}, - {time.Date(98, time.January, 2, 16, 0, 0, 0, time.UTC), 1}, - {time.Date(98, time.January, 2, 16, 0, 0, 0, time.UTC), 3}, - {time.Date(98, time.January, 2, 16, 0, 0, 0, time.UTC), 4}, - {time.Date(98, time.January, 2, 16, 0, 0, 1, time.UTC), 0}, // TODO tidy up redundant entries + {time.Date(0, time.January, 1, 0, 0, 0, 0, time.UTC), 9999}, // distant past + {time.Date(1998, time.January, 1, 11, 59, 59, 999999999, time.UTC), 1}, // just before cutoff + {time.Date(1998, time.January, 1, 11, 59, 59, 999999999, time.UTC), 2}, // + {time.Date(1998, time.January, 1, 12, 0, 0, 0, time.UTC), 3}, // equal to cutoff + {time.Date(1998, time.January, 1, 12, 0, 0, 0, time.UTC), 4}, // + {time.Date(1998, time.January, 1, 12, 0, 0, 1, time.UTC), 5}, // just after cutoff + {time.Date(1998, time.January, 1, 12, 0, 0, 1, time.UTC), 6}, // + {time.Date(9999, time.January, 1, 0, 0, 0, 0, time.UTC), 0}, // distant future } - cutoffTime := time.Date(98, time.January, 2, 16, 0, 0, 0, time.UTC) - - var expectedQueue []types.ID - for _, i := range queue { - if i.endTime.After(cutoffTime) { // only append items where endTime ≤ cutoffTime - break - } - expectedQueue = append(expectedQueue, i.auctionID) - } - - // write and read queue - for _, v := range queue { + for _, v := range byTimeIndex { keeper.InsertIntoQueue(ctx, v.endTime, v.auctionID) } - var readQueue []types.ID + + // read out values from index up to a cutoff time and check they are as expected + cutoffTime := time.Date(1998, time.January, 1, 12, 0, 0, 0, time.UTC) + var expectedIndex []types.ID + for _, v := range byTimeIndex { + if v.endTime.Before(cutoffTime) || v.endTime.Equal(cutoffTime) { // endTime ≤ cutoffTime + expectedIndex = append(expectedIndex, v.auctionID) + } + + } + var readIndex []types.ID keeper.IterateAuctionsByTime(ctx, cutoffTime, func(id types.ID) bool { - readQueue = append(readQueue, id) + readIndex = append(readIndex, id) return false }) - require.Equal(t, expectedQueue, readQueue) + require.Equal(t, expectedIndex, readIndex) } func convertIteratorToSlice(keeper keeper.Keeper, iterator sdk.Iterator) []types.ID { diff --git a/x/auction/types/params.go b/x/auction/types/params.go index 8f9b59ac..5b7cfdf0 100644 --- a/x/auction/types/params.go +++ b/x/auction/types/params.go @@ -13,7 +13,7 @@ const ( // DefaultMaxAuctionDuration max length of auction DefaultMaxAuctionDuration time.Duration = 2 * 24 * time.Hour // DefaultBidDuration how long an auction gets extended when someone bids, roughly 3 hours in blocks - DefaultMaxBidDuration time.Duration = 3 * time.Hour + DefaultBidDuration time.Duration = 3 * time.Hour ) // Parameter keys @@ -43,7 +43,7 @@ func NewAuctionParams(maxAuctionDuration time.Duration, bidDuration time.Duratio func DefaultAuctionParams() AuctionParams { return NewAuctionParams( DefaultMaxAuctionDuration, - DefaultMaxBidDuration, + DefaultBidDuration, ) } From 77bfe11f899ca82fa45de736b9c41affcba0db28 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 31 Dec 2019 11:10:15 +0000 Subject: [PATCH 32/99] add payout to depositors feature --- app/test_common.go | 5 +- x/auction/abci_test.go | 5 +- x/auction/keeper/auctions.go | 33 +++++++++++-- x/auction/keeper/auctions_test.go | 24 ++++++---- x/auction/keeper/integration_test.go | 7 +++ x/auction/keeper/math.go | 69 ++++++++++++++++++++++++++++ x/auction/keeper/math_test.go | 37 +++++++++++++++ x/auction/types/auctions.go | 34 +++++++++++--- 8 files changed, 191 insertions(+), 23 deletions(-) create mode 100644 x/auction/keeper/math.go create mode 100644 x/auction/keeper/math_test.go diff --git a/app/test_common.go b/app/test_common.go index f6a99036..f25c1c23 100644 --- a/app/test_common.go +++ b/app/test_common.go @@ -97,8 +97,9 @@ func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) T } func (tApp TestApp) CheckBalance(t *testing.T, ctx sdk.Context, owner sdk.AccAddress, expectedCoins sdk.Coins) { - actualCoins := tApp.GetAccountKeeper().GetAccount(ctx, owner).GetCoins() - require.Equal(t, expectedCoins, actualCoins) + acc := tApp.GetAccountKeeper().GetAccount(ctx, owner) + require.NotNilf(t, acc, "account with address '%s' doesn't exist", owner) + require.Equal(t, expectedCoins, acc.GetCoins()) } // Create a new auth genesis state from some addresses and coins. The state is returned marshalled into a map. diff --git a/x/auction/abci_test.go b/x/auction/abci_test.go index bb6a6585..79f526b5 100644 --- a/x/auction/abci_test.go +++ b/x/auction/abci_test.go @@ -19,7 +19,8 @@ func TestKeeper_EndBlocker(t *testing.T) { // Setup _, addrs := app.GeneratePrivKeyAddressPairs(2) buyer := addrs[0] - recipient := addrs[1] + returnAddrs := addrs[1:] + returnWeights := []sdk.Int{sdk.NewInt(1)} sellerModName := liquidator.ModuleName //sellerAddr := supply.NewModuleAddress(sellerModName) @@ -36,7 +37,7 @@ func TestKeeper_EndBlocker(t *testing.T) { ctx := tApp.NewContext(true, abci.Header{}) keeper := tApp.GetAuctionKeeper() - auctionID, err := keeper.StartForwardReverseAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), recipient) + auctionID, err := keeper.StartForwardReverseAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights) require.NoError(t, err) require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 30), c("token1", 20))) diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index a1215624..96d305bd 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -46,12 +46,16 @@ func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer string, bid sdk.Coin, } // StartForwardReverseAuction starts an auction where bidders bid up to a maxBid, then switch to bidding down on price. -func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, otherPerson sdk.AccAddress) (types.ID, sdk.Error) { +func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int) (types.ID, sdk.Error) { // create auction - auction := types.NewForwardReverseAuction(seller, lot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), maxBid, otherPerson) + weightedAddresses, err := types.NewWeightedAddresses(lotReturnAddrs, lotReturnWeights) + if err != nil { + return 0, err + } + auction := types.NewForwardReverseAuction(seller, lot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), maxBid, weightedAddresses) // take coins from module account - err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) + err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) if err != nil { return 0, err } @@ -206,10 +210,17 @@ func (k Keeper) PlaceBidForwardReverse(ctx sdk.Context, a types.ForwardReverseAu if err != nil { return a, err } - err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.OtherPerson, sdk.NewCoins(lotDecrement)) + // FIXME paying out rateably to cdp depositors is vulnerable to errors compounding over multiple bids + lotPayouts, err := splitCoinIntoWeightedBuckets(lotDecrement, a.LotReturns.Weights) if err != nil { return a, err } + for i, payout := range lotPayouts { + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.LotReturns.Addresses[i], sdk.NewCoins(payout)) + if err != nil { + return a, err + } + } // Update Auction a.Bidder = bidder @@ -318,3 +329,17 @@ func earliestTime(t1, t2 time.Time) time.Time { return t2 // also returned if times are equal } } + +func splitCoinIntoWeightedBuckets(coin sdk.Coin, buckets []sdk.Int) ([]sdk.Coin, sdk.Error) { + for _, bucket := range buckets { + if bucket.IsNegative() { + return nil, sdk.ErrInternal("cannot split coin into bucket with negative weight") + } + } + amounts := splitIntIntoWeightedBuckets(coin.Amount, buckets) + result := make([]sdk.Coin, len(amounts)) + for i, a := range amounts { + result[i] = sdk.NewCoin(coin.Denom, a) + } + return result, nil +} diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index 77f174e7..1bc48650 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -3,6 +3,7 @@ package keeper_test 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/supply" @@ -94,9 +95,10 @@ func TestReverseAuctionBasic(t *testing.T) { func TestForwardReverseAuctionBasic(t *testing.T) { // Setup - _, addrs := app.GeneratePrivKeyAddressPairs(2) + _, addrs := app.GeneratePrivKeyAddressPairs(4) buyer := addrs[0] - recipient := addrs[1] + returnAddrs := addrs[1:] + returnWeights := []sdk.Int{i(30), i(20), i(10)} sellerModName := liquidator.ModuleName sellerAddr := supply.NewModuleAddress(sellerModName) @@ -106,7 +108,9 @@ func TestForwardReverseAuctionBasic(t *testing.T) { tApp.InitializeFromGenesisStates( NewAuthGenStateFromAccs(authexported.GenesisAccounts{ auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), - auth.NewBaseAccount(recipient, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + auth.NewBaseAccount(returnAddrs[0], cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + auth.NewBaseAccount(returnAddrs[1], cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + auth.NewBaseAccount(returnAddrs[2], cs(c("token1", 100), c("token2", 100)), nil, 0, 0), sellerAcc, }), ) @@ -114,7 +118,7 @@ func TestForwardReverseAuctionBasic(t *testing.T) { keeper := tApp.GetAuctionKeeper() // Start auction - auctionID, err := keeper.StartForwardReverseAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), recipient) // seller, lot, maxBid, otherPerson + auctionID, err := keeper.StartForwardReverseAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights) // seller, lot, maxBid, otherPerson require.NoError(t, err) // Check seller's coins have decreased tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) @@ -125,8 +129,10 @@ func TestForwardReverseAuctionBasic(t *testing.T) { tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) // Check seller's coins have increased tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 110))) - // Check recipient has not received coins - tApp.CheckBalance(t, ctx, recipient, cs(c("token1", 100), c("token2", 100))) + // Check return addresses have not received coins + for _, ra := range returnAddrs { + tApp.CheckBalance(t, ctx, ra, cs(c("token1", 100), c("token2", 100))) + } // Place a reverse bid require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50), c("token1", 15))) // bid, lot @@ -134,8 +140,10 @@ func TestForwardReverseAuctionBasic(t *testing.T) { tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50))) // Check seller's coins have increased tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 150))) - // Check "recipient" has received coins - tApp.CheckBalance(t, ctx, recipient, cs(c("token1", 105), c("token2", 100))) + // Check return addresses have received coins + tApp.CheckBalance(t, ctx, returnAddrs[0], cs(c("token1", 102), c("token2", 100))) + tApp.CheckBalance(t, ctx, returnAddrs[1], cs(c("token1", 102), c("token2", 100))) + tApp.CheckBalance(t, ctx, returnAddrs[2], cs(c("token1", 101), c("token2", 100))) // Close auction at just after auction expiry ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration)) diff --git a/x/auction/keeper/integration_test.go b/x/auction/keeper/integration_test.go index 3ad2500e..38d9e4ce 100644 --- a/x/auction/keeper/integration_test.go +++ b/x/auction/keeper/integration_test.go @@ -10,6 +10,13 @@ import ( 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 i(n int64) sdk.Int { return sdk.NewInt(n) } +func is(ns ...int64) (is []sdk.Int) { + for _, n := range ns { + is = append(is, sdk.NewInt(n)) + } + return +} func NewAuthGenStateFromAccs(accounts authexported.GenesisAccounts) app.GenesisState { authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts) diff --git a/x/auction/keeper/math.go b/x/auction/keeper/math.go new file mode 100644 index 00000000..437b4f22 --- /dev/null +++ b/x/auction/keeper/math.go @@ -0,0 +1,69 @@ +package keeper + +import ( + "sort" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// splitIntIntoWeightedBuckets divides an initial +ve integer among several buckets in proportion to the buckets' weights +// It uses the largest remainder method: +// https://en.wikipedia.org/wiki/Largest_remainder_method +// see also: https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100 +func splitIntIntoWeightedBuckets(amount sdk.Int, buckets []sdk.Int) []sdk.Int { + // TODO ideally change algorithm to work with -ve numbers. Limiting to +ve numbers until them + if amount.IsNegative() { + panic("negative amount") + } + for _, bucket := range buckets { + if bucket.IsNegative() { + panic("negative bucket") + } + } + + totalWeights := totalInts(buckets...) + + // split amount by weights, recording whole number part and remainder + quotients := make([]quoRem, len(buckets)) + for i := range buckets { + q := amount.Mul(buckets[i]).Quo(totalWeights) + r := amount.Mul(buckets[i]).Mod(totalWeights) + quotients[i] = quoRem{index: i, quo: q, rem: r} + } + + // apportion left over to buckets with the highest remainder (to minimize error) + sort.Slice(quotients, func(i, j int) bool { + return quotients[i].rem.GT(quotients[j].rem) // decreasing remainder order + }) + + allocated := sdk.ZeroInt() + for _, qr := range quotients { + allocated = allocated.Add(qr.quo) + } + leftToAllocate := amount.Sub(allocated) + + results := make([]sdk.Int, len(quotients)) + for _, qr := range quotients { + results[qr.index] = qr.quo + if !leftToAllocate.IsZero() { + results[qr.index] = results[qr.index].Add(sdk.OneInt()) + leftToAllocate = leftToAllocate.Sub(sdk.OneInt()) + } + } + return results +} + +type quoRem struct { + index int + quo sdk.Int + rem sdk.Int +} + +// totalInts adds together sdk.Ints +func totalInts(is ...sdk.Int) sdk.Int { + total := sdk.ZeroInt() + for _, i := range is { + total = total.Add(i) + } + return total +} diff --git a/x/auction/keeper/math_test.go b/x/auction/keeper/math_test.go new file mode 100644 index 00000000..58e33e7e --- /dev/null +++ b/x/auction/keeper/math_test.go @@ -0,0 +1,37 @@ +package keeper + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestSplitIntIntoWeightedBuckets(t *testing.T) { + testCases := []struct { + name string + amount sdk.Int + buckets []sdk.Int + want []sdk.Int + }{ + {"2split1,1", i(2), is(1, 1), is(1, 1)}, + {"100split1,9", i(100), is(1, 9), is(10, 90)}, + {"7split1,2", i(7), is(1, 2), is(2, 5)}, + {"17split1,1,1", i(17), is(1, 1, 1), is(6, 6, 5)}, + // TODO more tests + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := splitIntIntoWeightedBuckets(tc.amount, tc.buckets) + require.Equal(t, tc.want, got) + }) + } +} + +func i(n int64) sdk.Int { return sdk.NewInt(n) } +func is(ns ...int64) (is []sdk.Int) { + for _, n := range ns { + is = append(is, sdk.NewInt(n)) + } + return +} diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 9e45ee5c..b293c5ee 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -133,8 +133,8 @@ func NewReverseAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin // 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, will have to be updated to account for deposits + MaxBid sdk.Coin + LotReturns WeightedAddresses // return addresses to pay out reductions in the lot amount to. Lot is bid down during reverse phase. } // WithID returns an auction with the ID set @@ -149,15 +149,15 @@ func (a ForwardReverseAuction) String() string { End Time: %s Max End Time: %s Max Bid %s - Other Person %s`, + LotReturns %s`, a.GetID(), a.Initiator, a.Lot, a.Bidder, a.Bid, a.GetEndTime().String(), - a.MaxEndTime.String(), a.MaxBid, a.OtherPerson, + a.MaxEndTime.String(), a.MaxBid, a.LotReturns, ) } // NewForwardReverseAuction creates a new forward reverse auction -func NewForwardReverseAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, otherPerson sdk.AccAddress) ForwardReverseAuction { +func NewForwardReverseAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses) ForwardReverseAuction { auction := ForwardReverseAuction{ BaseAuction: BaseAuction{ // no ID @@ -167,8 +167,28 @@ func NewForwardReverseAuction(seller string, lot sdk.Coin, EndTime time.Time, ma Bid: sdk.NewInt64Coin(maxBid.Denom, 0), EndTime: EndTime, MaxEndTime: EndTime}, - MaxBid: maxBid, - OtherPerson: otherPerson, + MaxBid: maxBid, + LotReturns: lotReturns, } return auction } + +type WeightedAddresses struct { + Addresses []sdk.AccAddress + Weights []sdk.Int +} + +func NewWeightedAddresses(addrs []sdk.AccAddress, weights []sdk.Int) (WeightedAddresses, sdk.Error) { + if len(addrs) != len(weights) { + return WeightedAddresses{}, sdk.ErrInternal("number of addresses doesn't match number of weights") + } + for _, w := range weights { + if w.IsNegative() { + return WeightedAddresses{}, sdk.ErrInternal("weights contain a negative amount") + } + } + return WeightedAddresses{ + Addresses: addrs, + Weights: weights, + }, nil +} From 4e7f18313a14b07c14f397d4afee68d986d6a2cb Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 31 Dec 2019 11:10:58 +0000 Subject: [PATCH 33/99] add more tests --- x/auction/keeper/auctions_test.go | 88 +++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index 1bc48650..aad049a3 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" @@ -151,3 +152,90 @@ func TestForwardReverseAuctionBasic(t *testing.T) { // Check buyer's coins increased tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 115), c("token2", 50))) } + +func TestStartForwardAuction(t *testing.T) { + someTime := time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC) + type args struct { + seller string + lot sdk.Coin + bidDenom string + } + testCases := []struct { + name string + blockTime time.Time + args args + expectPass bool + }{ + { + "normal", + someTime, + args{liquidator.ModuleName, c("stable", 10), "gov"}, + true, + }, + { + "no module account", + someTime, + args{"nonExistentModule", c("stable", 10), "gov"}, + false, + }, + { + "not enough coins", + someTime, + args{liquidator.ModuleName, c("stable", 101), "gov"}, + false, + }, + { + "incorrect denom", + someTime, + args{liquidator.ModuleName, c("notacoin", 10), "gov"}, + false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // setup + initialLiquidatorCoins := cs(c("stable", 100)) + tApp := app.NewTestApp() + + liqAcc := supply.NewEmptyModuleAccount(liquidator.ModuleName, supply.Burner) // TODO could add test to check for burner permissions + require.NoError(t, liqAcc.SetCoins(initialLiquidatorCoins)) + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{liqAcc}), + ) + ctx := tApp.NewContext(false, abci.Header{}).WithBlockTime(tc.blockTime) + keeper := tApp.GetAuctionKeeper() + + // run function under test + id, err := keeper.StartForwardAuction(ctx, tc.args.seller, tc.args.lot, tc.args.bidDenom) + + // check + sk := tApp.GetSupplyKeeper() + liquidatorCoins := sk.GetModuleAccount(ctx, liquidator.ModuleName).GetCoins() + actualAuc, found := keeper.GetAuction(ctx, id) + + if tc.expectPass { + require.NoError(t, err) + // check coins moved + require.Equal(t, initialLiquidatorCoins.Sub(cs(tc.args.lot)), liquidatorCoins) + // check auction in store and is correct + require.True(t, found) + expectedAuction := types.Auction(types.ForwardAuction{BaseAuction: types.BaseAuction{ + ID: types.ID(0), + Initiator: tc.args.seller, + Lot: tc.args.lot, + Bidder: nil, + Bid: c(tc.args.bidDenom, 0), + EndTime: tc.blockTime.Add(types.DefaultMaxAuctionDuration), + MaxEndTime: tc.blockTime.Add(types.DefaultMaxAuctionDuration), + }}) + require.Equal(t, expectedAuction, actualAuc) + } else { + require.Error(t, err) + // check coins not moved + require.Equal(t, initialLiquidatorCoins, liquidatorCoins) + // check auction not in store + require.False(t, found) + } + }) + } +} From db3c39aaa525fa40f000daea8bb4edddbd45eed6 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Tue, 31 Dec 2019 11:56:39 +0000 Subject: [PATCH 34/99] move index updates to Get/Set for more safety --- x/auction/keeper/auctions.go | 14 ++---- x/auction/keeper/auctions_test.go | 2 +- x/auction/keeper/keeper.go | 47 ++++++++++---------- x/auction/keeper/keeper_test.go | 74 ++++++++++++++----------------- 4 files changed, 61 insertions(+), 76 deletions(-) diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index 96d305bd..0ece2e5c 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -20,7 +20,7 @@ func (k Keeper) StartForwardAuction(ctx sdk.Context, seller string, lot sdk.Coin return 0, err } // store the auction - auctionID, err := k.storeNewAuction(ctx, auction) + auctionID, err := k.StoreNewAuction(ctx, auction) if err != nil { return 0, err } @@ -38,7 +38,7 @@ func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer string, bid sdk.Coin, return 0, sdk.ErrInternal("module does not have minting permissions") } // store the auction - auctionID, err := k.storeNewAuction(ctx, auction) + auctionID, err := k.StoreNewAuction(ctx, auction) if err != nil { return 0, err } @@ -60,7 +60,7 @@ func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller string, lot s return 0, err } // store the auction - auctionID, err := k.storeNewAuction(ctx, auction) + auctionID, err := k.StoreNewAuction(ctx, auction) if err != nil { return 0, err } @@ -112,13 +112,7 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID types.ID, bidder sdk.AccAddr } // store updated auction - existing, found := k.GetAuction(ctx, a.GetID()) - if found { - k.RemoveFromQueue(ctx, existing.GetEndTime(), existing.GetID()) - } k.SetAuction(ctx, a) - k.InsertIntoQueue(ctx, a.GetEndTime(), a.GetID()) - return nil } @@ -298,8 +292,6 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID types.ID) sdk.Error { // Delete auction from store (and queue) k.DeleteAuction(ctx, auctionID) - k.RemoveFromQueue(ctx, auction.GetEndTime(), auction.GetID()) - return nil } func (k Keeper) MintAndPayoutAuctionLot(ctx sdk.Context, a types.ReverseAuction) sdk.Error { diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index aad049a3..e3ea8028 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -99,7 +99,7 @@ func TestForwardReverseAuctionBasic(t *testing.T) { _, addrs := app.GeneratePrivKeyAddressPairs(4) buyer := addrs[0] returnAddrs := addrs[1:] - returnWeights := []sdk.Int{i(30), i(20), i(10)} + returnWeights := is(30, 20, 10) sellerModName := liquidator.ModuleName sellerAddr := supply.NewModuleAddress(sellerModName) diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index dfb7dd49..d665599b 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -57,8 +57,8 @@ func (k Keeper) IncrementNextAuctionID(ctx sdk.Context) sdk.Error { return nil } -// storeNewAuction stores an auction, adding a new ID, and setting indexes -func (k Keeper) storeNewAuction(ctx sdk.Context, auction types.Auction) (types.ID, sdk.Error) { +// StoreNewAuction stores an auction, adding a new ID +func (k Keeper) StoreNewAuction(ctx sdk.Context, auction types.Auction) (types.ID, sdk.Error) { newAuctionID, err := k.GetNextAuctionID(ctx) if err != nil { return 0, err @@ -66,7 +66,6 @@ func (k Keeper) storeNewAuction(ctx sdk.Context, auction types.Auction) (types.I auction = auction.WithID(newAuctionID) k.SetAuction(ctx, auction) - k.InsertIntoQueue(ctx, auction.GetEndTime(), auction.GetID()) err = k.IncrementNextAuctionID(ctx) if err != nil { @@ -75,24 +74,22 @@ func (k Keeper) storeNewAuction(ctx sdk.Context, auction types.Auction) (types.I return newAuctionID, nil } -// TODO should get/set/delete be responsible for updating auctionByTime index? - // 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()) - // } + // remove the auction from the byTime index if it is already in there + existingAuction, found := k.GetAuction(ctx, auction.GetID()) + if found { + k.RemoveFromIndex(ctx, existingAuction.GetEndTime(), existingAuction.GetID()) + } // store auction store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) bz := k.cdc.MustMarshalBinaryLengthPrefixed(auction) store.Set(types.GetAuctionKey(auction.GetID()), bz) - // add to the queue - //k.InsertIntoQueue(ctx, auction.GetEndTime(), auction.GetID()) + // add to index + k.InsertIntoIndex(ctx, auction.GetEndTime(), auction.GetID()) } // getAuction gets an auction from the store by auctionID @@ -111,29 +108,32 @@ func (k Keeper) GetAuction(ctx sdk.Context, auctionID types.ID) (types.Auction, // 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) - // } + // remove from index + auction, found := k.GetAuction(ctx, auctionID) + if found { + k.RemoveFromIndex(ctx, auction.GetEndTime(), auctionID) + } // delete auction store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) store.Delete(types.GetAuctionKey(auctionID)) } -// Inserts a AuctionID into the queue at endTime -func (k Keeper) InsertIntoQueue(ctx sdk.Context, endTime time.Time, auctionID types.ID) { +// InsertIntoIndex adds an auction ID and end time into the byTime index +func (k Keeper) InsertIntoIndex(ctx sdk.Context, endTime time.Time, auctionID types.ID) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) store.Set(types.GetAuctionByTimeKey(endTime, auctionID), auctionID.Bytes()) } -// removes an auctionID from the queue -func (k Keeper) RemoveFromQueue(ctx sdk.Context, endTime time.Time, auctionID types.ID) { +// RemoveFromIndex removes an auction ID and end time from the byTime index +func (k Keeper) RemoveFromIndex(ctx sdk.Context, endTime time.Time, auctionID types.ID) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) store.Delete(types.GetAuctionByTimeKey(endTime, auctionID)) } +// IterateAuctionByTime provides an iterator over auctions ordered by auction.EndTime. +// For each auction cb will be callled. If cb returns true the iterator will close and stop. +// TODO can the cutoff time be removed in favour of caller specifying cutoffs in the callback? func (k Keeper) IterateAuctionsByTime(ctx sdk.Context, inclusiveCutoffTime time.Time, cb func(auctionID types.ID) (stop bool)) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) iterator := store.Iterator( @@ -152,9 +152,8 @@ func (k Keeper) IterateAuctionsByTime(ctx sdk.Context, inclusiveCutoffTime time. } } -// IterateAuctions provides an iterator over all stored auctions. For -// each auction, cb will be called. If the cb returns true, the iterator -// will close and stop. +// IterateAuctions provides an iterator over all stored auctions. +// For each auction, cb will be called. If cb returns true, the iterator will close and stop. func (k Keeper) IterateAuctions(ctx sdk.Context, cb func(auction types.Auction) (stop bool)) { iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go index f6e9a1d9..3667aeb8 100644 --- a/x/auction/keeper/keeper_test.go +++ b/x/auction/keeper/keeper_test.go @@ -4,12 +4,10 @@ import ( "testing" "time" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/app" - "github.com/kava-labs/kava/x/auction/keeper" "github.com/kava-labs/kava/x/auction/types" ) @@ -29,10 +27,11 @@ func SetGetDeleteAuction(t *testing.T) { // check before and after match require.True(t, found) require.Equal(t, auction, readAuction) - // check auction is in queue - // iter := keeper.GetQueueIterator(ctx, 100000) - // require.Equal(t, 1, len(convertIteratorToSlice(keeper, iter))) - // iter.Close() + // check auction is in the index + keeper.IterateAuctionsByTime(ctx, auction.GetEndTime(), func(readID types.ID) bool { + require.Equal(t, auction.GetID(), readID) + return false + }) // delete auction keeper.DeleteAuction(ctx, id) @@ -40,11 +39,11 @@ func SetGetDeleteAuction(t *testing.T) { // 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() - + // check auction not in index + keeper.IterateAuctionsByTime(ctx, time.Unix(999999999, 0), func(readID types.ID) bool { + require.Fail(t, "index should be empty", " found auction ID '%s", readID) + return false + }) } func TestIncrementNextAuctionID(t *testing.T) { @@ -66,27 +65,32 @@ func TestIncrementNextAuctionID(t *testing.T) { } -// func TestIterateAuctions(t *testing.T) { -// // setup keeper -// tApp := app.NewTestApp() -// keeper := tApp.GetAuctionKeeper() -// ctx := tApp.NewContext(true, abci.Header{}) +func TestIterateAuctions(t *testing.T) { + // setup + tApp := app.NewTestApp() + tApp.InitializeFromGenesisStates() + keeper := tApp.GetAuctionKeeper() + ctx := tApp.NewContext(true, abci.Header{}) -// auctions := []types.Auction{ -// &types.ForwardAuction{}, -// } -// for _, a := range auctions { -// keeper.SetAuction(ctx, a) -// } + auctions := []types.Auction{ + types.NewForwardAuction("sellerMod", c("denom", 12345678), "anotherdenom", time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)).WithID(0), + types.NewReverseAuction("buyerMod", c("denom", 12345678), c("anotherdenom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)).WithID(1), + types.NewForwardReverseAuction("sellerMod", c("denom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC), c("anotherdenom", 12345678), types.WeightedAddresses{}).WithID(2), + } + for _, a := range auctions { + keeper.SetAuction(ctx, a) + } -// var readAuctions []types.Auction -// keeper.IterateAuctions(ctx, func(a types.Auction) bool { -// readAuctions = append(readAuctions, a) -// return false -// }) + // run + var readAuctions []types.Auction + keeper.IterateAuctions(ctx, func(a types.Auction) bool { + readAuctions = append(readAuctions, a) + return false + }) -// require.Equal(t, auctions, readAuctions) -// } + // check + require.Equal(t, auctions, readAuctions) +} func TestIterateAuctionsByTime(t *testing.T) { // setup keeper @@ -109,7 +113,7 @@ func TestIterateAuctionsByTime(t *testing.T) { {time.Date(9999, time.January, 1, 0, 0, 0, 0, time.UTC), 0}, // distant future } for _, v := range byTimeIndex { - keeper.InsertIntoQueue(ctx, v.endTime, v.auctionID) + keeper.InsertIntoIndex(ctx, v.endTime, v.auctionID) } // read out values from index up to a cutoff time and check they are as expected @@ -129,13 +133,3 @@ func TestIterateAuctionsByTime(t *testing.T) { require.Equal(t, expectedIndex, readIndex) } - -func convertIteratorToSlice(keeper keeper.Keeper, iterator sdk.Iterator) []types.ID { - var queue []types.ID - for ; iterator.Valid(); iterator.Next() { - var auctionID types.ID - types.ModuleCdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &auctionID) - queue = append(queue, auctionID) - } - return queue -} From 983de010df54e0bc8a2582a1bc66ac9259676dae Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Wed, 1 Jan 2020 14:11:19 +0000 Subject: [PATCH 35/99] remove slightly unecessary ID type --- x/auction/abci.go | 4 +-- x/auction/alias.go | 2 -- x/auction/client/cli/tx.go | 3 +- x/auction/client/rest/tx.go | 3 +- x/auction/keeper/auctions.go | 10 +++---- x/auction/keeper/auctions_test.go | 2 +- x/auction/keeper/keeper.go | 24 ++++++++-------- x/auction/keeper/keeper_test.go | 16 +++++------ x/auction/types/auctions.go | 38 +++++--------------------- x/auction/types/genesis.go | 6 ++-- x/auction/types/keys.go | 19 ++++++++++--- x/auction/types/msg.go | 4 +-- x/liquidator/keeper/keeper.go | 7 ++--- x/liquidator/types/expected_keepers.go | 7 ++--- 14 files changed, 65 insertions(+), 80 deletions(-) diff --git a/x/auction/abci.go b/x/auction/abci.go index f05a518a..8ce80f90 100644 --- a/x/auction/abci.go +++ b/x/auction/abci.go @@ -7,8 +7,8 @@ import ( // EndBlocker runs at the end of every block. func EndBlocker(ctx sdk.Context, k Keeper) { - var expiredAuctions []ID - k.IterateAuctionsByTime(ctx, ctx.BlockTime(), func(id ID) bool { + var expiredAuctions []uint64 + k.IterateAuctionsByTime(ctx, ctx.BlockTime(), func(id uint64) bool { expiredAuctions = append(expiredAuctions, id) return false }) diff --git a/x/auction/alias.go b/x/auction/alias.go index acf20129..a3262826 100644 --- a/x/auction/alias.go +++ b/x/auction/alias.go @@ -22,7 +22,6 @@ const ( var ( // functions aliases - NewIDFromString = types.NewIDFromString NewForwardAuction = types.NewForwardAuction NewReverseAuction = types.NewReverseAuction NewForwardReverseAuction = types.NewForwardReverseAuction @@ -46,7 +45,6 @@ var ( type ( Auction = types.Auction BaseAuction = types.BaseAuction - ID = types.ID ForwardAuction = types.ForwardAuction ReverseAuction = types.ReverseAuction ForwardReverseAuction = types.ForwardReverseAuction diff --git a/x/auction/client/cli/tx.go b/x/auction/client/cli/tx.go index c663658b..2ced6610 100644 --- a/x/auction/client/cli/tx.go +++ b/x/auction/client/cli/tx.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "strconv" "github.com/kava-labs/kava/x/auction/types" "github.com/spf13/cobra" @@ -39,7 +40,7 @@ func GetCmdPlaceBid(cdc *codec.Codec) *cobra.Command { cliCtx := context.NewCLIContext().WithCodec(cdc) txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) - id, err := types.NewIDFromString(args[0]) + id, err := strconv.ParseUint(args[0], 10, 64) if err != nil { fmt.Printf("invalid auction id - %s \n", string(args[0])) return err diff --git a/x/auction/client/rest/tx.go b/x/auction/client/rest/tx.go index 61c9e18f..5773f24a 100644 --- a/x/auction/client/rest/tx.go +++ b/x/auction/client/rest/tx.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "net/http" + "strconv" "github.com/gorilla/mux" @@ -45,7 +46,7 @@ func bidHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { strBid := vars[restBid] strLot := vars[restLot] - auctionID, err := types.NewIDFromString(strAuctionID) + auctionID, err := strconv.ParseUint(strAuctionID, 10, 64) if err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index 0ece2e5c..51c7c804 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -10,7 +10,7 @@ import ( ) // StartForwardAuction starts a normal auction that mints the sold coins. -func (k Keeper) StartForwardAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (types.ID, sdk.Error) { +func (k Keeper) StartForwardAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (uint64, sdk.Error) { // create auction auction := types.NewForwardAuction(seller, lot, bidDenom, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) @@ -28,7 +28,7 @@ func (k Keeper) StartForwardAuction(ctx sdk.Context, seller string, lot sdk.Coin } // StartReverseAuction starts an auction where sellers compete by offering decreasing prices. -func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin) (types.ID, sdk.Error) { +func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin) (uint64, sdk.Error) { // create auction auction := types.NewReverseAuction(buyer, bid, initialLot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) @@ -46,7 +46,7 @@ func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer string, bid sdk.Coin, } // StartForwardReverseAuction starts an auction where bidders bid up to a maxBid, then switch to bidding down on price. -func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int) (types.ID, sdk.Error) { +func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int) (uint64, sdk.Error) { // create auction weightedAddresses, err := types.NewWeightedAddresses(lotReturnAddrs, lotReturnWeights) if err != nil { @@ -69,7 +69,7 @@ func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller string, lot s // PlaceBid places a bid on any auction. // TODO passing bid and lot is weird when only one needed -func (k Keeper) PlaceBid(ctx sdk.Context, auctionID types.ID, bidder sdk.AccAddress, bid sdk.Coin, lot sdk.Coin) sdk.Error { +func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddress, bid sdk.Coin, lot sdk.Coin) sdk.Error { // get auction from store auction, found := k.GetAuction(ctx, auctionID) @@ -261,7 +261,7 @@ func (k Keeper) PlaceBidReverse(ctx sdk.Context, a types.ReverseAuction, bidder } // CloseAuction closes an auction and distributes funds to the highest bidder. -func (k Keeper) CloseAuction(ctx sdk.Context, auctionID types.ID) sdk.Error { +func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error { // get the auction from the store auction, found := k.GetAuction(ctx, auctionID) diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index e3ea8028..c5545476 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -220,7 +220,7 @@ func TestStartForwardAuction(t *testing.T) { // check auction in store and is correct require.True(t, found) expectedAuction := types.Auction(types.ForwardAuction{BaseAuction: types.BaseAuction{ - ID: types.ID(0), + ID: 0, Initiator: tc.args.seller, Lot: tc.args.lot, Bidder: nil, diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index d665599b..28e41540 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -30,21 +30,21 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, supplyKeeper types.Suppl } // SetNextAuctionID stores an ID to be used for the next created auction -func (k Keeper) SetNextAuctionID(ctx sdk.Context, id types.ID) { +func (k Keeper) SetNextAuctionID(ctx sdk.Context, id uint64) { store := ctx.KVStore(k.storeKey) - store.Set(types.NextAuctionIDKey, id.Bytes()) + store.Set(types.NextAuctionIDKey, types.Uint64ToBytes(id)) } // GetNextAuctionID reads the next available global ID from store // TODO might be nicer to convert not found error to a panic, it's not an error that can be recovered from -func (k Keeper) GetNextAuctionID(ctx sdk.Context) (types.ID, sdk.Error) { +func (k Keeper) GetNextAuctionID(ctx sdk.Context) (uint64, sdk.Error) { store := ctx.KVStore(k.storeKey) bz := store.Get(types.NextAuctionIDKey) if bz == nil { //return 0, types.ErrInvalidGenesis(k.codespace, "initial auction ID hasn't been set") // TODO create error return 0, sdk.ErrInternal("initial auction ID hasn't been set") } - return types.NewIDFromBytes(bz), nil + return types.Uint64FromBytes(bz), nil } // incrementNextAuctionID increments the global ID in the store by 1 @@ -58,7 +58,7 @@ func (k Keeper) IncrementNextAuctionID(ctx sdk.Context) sdk.Error { } // StoreNewAuction stores an auction, adding a new ID -func (k Keeper) StoreNewAuction(ctx sdk.Context, auction types.Auction) (types.ID, sdk.Error) { +func (k Keeper) StoreNewAuction(ctx sdk.Context, auction types.Auction) (uint64, sdk.Error) { newAuctionID, err := k.GetNextAuctionID(ctx) if err != nil { return 0, err @@ -93,7 +93,7 @@ func (k Keeper) SetAuction(ctx sdk.Context, auction types.Auction) { } // getAuction gets an auction from the store by auctionID -func (k Keeper) GetAuction(ctx sdk.Context, auctionID types.ID) (types.Auction, bool) { +func (k Keeper) GetAuction(ctx sdk.Context, auctionID uint64) (types.Auction, bool) { var auction types.Auction store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) @@ -107,7 +107,7 @@ func (k Keeper) GetAuction(ctx sdk.Context, auctionID types.ID) (types.Auction, } // DeleteAuction removes an auction from the store without any validation -func (k Keeper) DeleteAuction(ctx sdk.Context, auctionID types.ID) { +func (k Keeper) DeleteAuction(ctx sdk.Context, auctionID uint64) { // remove from index auction, found := k.GetAuction(ctx, auctionID) if found { @@ -120,13 +120,13 @@ func (k Keeper) DeleteAuction(ctx sdk.Context, auctionID types.ID) { } // InsertIntoIndex adds an auction ID and end time into the byTime index -func (k Keeper) InsertIntoIndex(ctx sdk.Context, endTime time.Time, auctionID types.ID) { +func (k Keeper) InsertIntoIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) - store.Set(types.GetAuctionByTimeKey(endTime, auctionID), auctionID.Bytes()) + store.Set(types.GetAuctionByTimeKey(endTime, auctionID), types.Uint64ToBytes(auctionID)) // TODO } // RemoveFromIndex removes an auction ID and end time from the byTime index -func (k Keeper) RemoveFromIndex(ctx sdk.Context, endTime time.Time, auctionID types.ID) { +func (k Keeper) RemoveFromIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) store.Delete(types.GetAuctionByTimeKey(endTime, auctionID)) } @@ -134,7 +134,7 @@ func (k Keeper) RemoveFromIndex(ctx sdk.Context, endTime time.Time, auctionID ty // IterateAuctionByTime provides an iterator over auctions ordered by auction.EndTime. // For each auction cb will be callled. If cb returns true the iterator will close and stop. // TODO can the cutoff time be removed in favour of caller specifying cutoffs in the callback? -func (k Keeper) IterateAuctionsByTime(ctx sdk.Context, inclusiveCutoffTime time.Time, cb func(auctionID types.ID) (stop bool)) { +func (k Keeper) IterateAuctionsByTime(ctx sdk.Context, inclusiveCutoffTime time.Time, cb func(auctionID uint64) (stop bool)) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) iterator := store.Iterator( nil, // start at the very start of the prefix store @@ -144,7 +144,7 @@ func (k Keeper) IterateAuctionsByTime(ctx sdk.Context, inclusiveCutoffTime time. defer iterator.Close() for ; iterator.Valid(); iterator.Next() { // TODO get the auction ID - either read from store, or extract from key - auctionID := types.NewIDFromBytes(iterator.Value()) + auctionID := types.Uint64FromBytes(iterator.Value()) if cb(auctionID) { break diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go index 3667aeb8..cc6fbce1 100644 --- a/x/auction/keeper/keeper_test.go +++ b/x/auction/keeper/keeper_test.go @@ -17,7 +17,7 @@ func SetGetDeleteAuction(t *testing.T) { keeper := tApp.GetAuctionKeeper() ctx := tApp.NewContext(true, abci.Header{}) someTime := time.Date(43, time.January, 1, 0, 0, 0, 0, time.UTC) // need to specify UTC as tz info is lost on unmarshal - id := types.ID(5) + var id uint64 = 5 auction := types.NewForwardAuction("some_module", c("usdx", 100), "kava", someTime).WithID(id) // write and read from store @@ -28,7 +28,7 @@ func SetGetDeleteAuction(t *testing.T) { require.True(t, found) require.Equal(t, auction, readAuction) // check auction is in the index - keeper.IterateAuctionsByTime(ctx, auction.GetEndTime(), func(readID types.ID) bool { + keeper.IterateAuctionsByTime(ctx, auction.GetEndTime(), func(readID uint64) bool { require.Equal(t, auction.GetID(), readID) return false }) @@ -40,7 +40,7 @@ func SetGetDeleteAuction(t *testing.T) { _, found = keeper.GetAuction(ctx, id) require.False(t, found) // check auction not in index - keeper.IterateAuctionsByTime(ctx, time.Unix(999999999, 0), func(readID types.ID) bool { + keeper.IterateAuctionsByTime(ctx, time.Unix(999999999, 0), func(readID uint64) bool { require.Fail(t, "index should be empty", " found auction ID '%s", readID) return false }) @@ -53,7 +53,7 @@ func TestIncrementNextAuctionID(t *testing.T) { ctx := tApp.NewContext(true, abci.Header{}) // store id - id := types.ID(123456) + var id uint64 = 123456 keeper.SetNextAuctionID(ctx, id) require.NoError(t, keeper.IncrementNextAuctionID(ctx)) @@ -101,7 +101,7 @@ func TestIterateAuctionsByTime(t *testing.T) { // setup byTime index byTimeIndex := []struct { endTime time.Time - auctionID types.ID + auctionID uint64 }{ {time.Date(0, time.January, 1, 0, 0, 0, 0, time.UTC), 9999}, // distant past {time.Date(1998, time.January, 1, 11, 59, 59, 999999999, time.UTC), 1}, // just before cutoff @@ -118,15 +118,15 @@ func TestIterateAuctionsByTime(t *testing.T) { // read out values from index up to a cutoff time and check they are as expected cutoffTime := time.Date(1998, time.January, 1, 12, 0, 0, 0, time.UTC) - var expectedIndex []types.ID + var expectedIndex []uint64 for _, v := range byTimeIndex { if v.endTime.Before(cutoffTime) || v.endTime.Equal(cutoffTime) { // endTime ≤ cutoffTime expectedIndex = append(expectedIndex, v.auctionID) } } - var readIndex []types.ID - keeper.IterateAuctionsByTime(ctx, cutoffTime, func(id types.ID) bool { + var readIndex []uint64 + keeper.IterateAuctionsByTime(ctx, cutoffTime, func(id uint64) bool { readIndex = append(readIndex, id) return false }) diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index b293c5ee..5b9b079f 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -1,41 +1,17 @@ package types import ( - "encoding/binary" "fmt" - "strconv" "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/supply" ) -// ID type for auction IDs -type ID uint64 - -// TODO can this be removed? -// 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 -} -func NewIDFromBytes(bz []byte) ID { - return ID(binary.BigEndian.Uint64(bz)) - -} -func (id ID) Bytes() []byte { - bz := make([]byte, 8) - binary.BigEndian.PutUint64(bz, uint64(id)) - return bz -} - // Auction is an interface to several types of auction. type Auction interface { - GetID() ID - WithID(ID) Auction + GetID() uint64 + WithID(uint64) Auction GetBidder() sdk.AccAddress GetBid() sdk.Coin GetLot() sdk.Coin @@ -44,7 +20,7 @@ type Auction interface { // BaseAuction type shared by all Auctions type BaseAuction struct { - ID ID + ID uint64 Initiator string // Module that starts the auction. Giving away Lot (aka seller in a forward auction). Restricted to being a module account name rather than any account. 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) @@ -54,7 +30,7 @@ type BaseAuction struct { } // GetID getter for auction ID -func (a BaseAuction) GetID() ID { return a.ID } +func (a BaseAuction) GetID() uint64 { return a.ID } // GetBid getter for auction bid func (a BaseAuction) GetBidder() sdk.AccAddress { return a.Bidder } @@ -88,7 +64,7 @@ type ForwardAuction struct { } // WithID returns an auction with the ID set -func (a ForwardAuction) WithID(id ID) Auction { a.ID = id; return a } +func (a ForwardAuction) WithID(id uint64) Auction { a.ID = id; return a } // NewForwardAuction creates a new forward auction func NewForwardAuction(seller string, lot sdk.Coin, bidDenom string, endTime time.Time) ForwardAuction { @@ -110,7 +86,7 @@ type ReverseAuction struct { } // WithID returns an auction with the ID set -func (a ReverseAuction) WithID(id ID) Auction { a.ID = id; return a } +func (a ReverseAuction) WithID(id uint64) Auction { a.ID = id; return a } // NewReverseAuction creates a new reverse auction func NewReverseAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime time.Time) ReverseAuction { @@ -138,7 +114,7 @@ type ForwardReverseAuction struct { } // WithID returns an auction with the ID set -func (a ForwardReverseAuction) WithID(id ID) Auction { a.ID = id; return a } +func (a ForwardReverseAuction) WithID(id uint64) Auction { a.ID = id; return a } func (a ForwardReverseAuction) String() string { return fmt.Sprintf(`Auction %d: diff --git a/x/auction/types/genesis.go b/x/auction/types/genesis.go index 15530961..27f9eb52 100644 --- a/x/auction/types/genesis.go +++ b/x/auction/types/genesis.go @@ -9,13 +9,13 @@ type GenesisAuctions []Auction // GenesisState - auction state that must be provided at genesis type GenesisState struct { - NextAuctionID ID + NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` 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(nextID ID, ap AuctionParams, ga GenesisAuctions) GenesisState { +func NewGenesisState(nextID uint64, ap AuctionParams, ga GenesisAuctions) GenesisState { return GenesisState{ NextAuctionID: nextID, AuctionParams: ap, @@ -25,7 +25,7 @@ func NewGenesisState(nextID ID, ap AuctionParams, ga GenesisAuctions) GenesisSta // DefaultGenesisState defines default genesis state for auction module func DefaultGenesisState() GenesisState { - return NewGenesisState(ID(0), DefaultAuctionParams(), GenesisAuctions{}) + return NewGenesisState(0, DefaultAuctionParams(), GenesisAuctions{}) } // Equal checks whether two GenesisState structs are equivalent diff --git a/x/auction/types/keys.go b/x/auction/types/keys.go index fc20ef14..e63bdf54 100644 --- a/x/auction/types/keys.go +++ b/x/auction/types/keys.go @@ -1,6 +1,7 @@ package types import ( + "encoding/binary" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -28,10 +29,20 @@ var ( NextAuctionIDKey = []byte{0x02} ) -func GetAuctionKey(auctionID ID) []byte { - return auctionID.Bytes() +func GetAuctionKey(auctionID uint64) []byte { + return Uint64ToBytes(auctionID) } -func GetAuctionByTimeKey(endTime time.Time, auctionID ID) []byte { - return append(sdk.FormatTimeBytes(endTime), auctionID.Bytes()...) +func GetAuctionByTimeKey(endTime time.Time, auctionID uint64) []byte { + return append(sdk.FormatTimeBytes(endTime), Uint64ToBytes(auctionID)...) } + +func Uint64FromBytes(bz []byte) uint64 { + return binary.BigEndian.Uint64(bz) +} + +func Uint64ToBytes(id uint64) []byte { + bz := make([]byte, 8) + binary.BigEndian.PutUint64(bz, uint64(id)) + return bz +} \ No newline at end of file diff --git a/x/auction/types/msg.go b/x/auction/types/msg.go index 5cdc60e3..0014d01e 100644 --- a/x/auction/types/msg.go +++ b/x/auction/types/msg.go @@ -4,14 +4,14 @@ 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 + AuctionID uint64 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 { +func NewMsgPlaceBid(auctionID uint64, bidder sdk.AccAddress, bid sdk.Coin, lot sdk.Coin) MsgPlaceBid { return MsgPlaceBid{ AuctionID: auctionID, Bidder: bidder, diff --git a/x/liquidator/keeper/keeper.go b/x/liquidator/keeper/keeper.go index 59354270..5134d47e 100644 --- a/x/liquidator/keeper/keeper.go +++ b/x/liquidator/keeper/keeper.go @@ -5,7 +5,6 @@ import ( 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" ) @@ -33,7 +32,7 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, paramstore subspace.Subs // 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) { +func (k Keeper) SeizeAndStartCollateralAuction(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string) (uint64, sdk.Error) { // Get CDP cdp, found := k.cdpKeeper.GetCDP(ctx, owner, collateralDenom) if !found { @@ -73,7 +72,7 @@ func (k Keeper) SeizeAndStartCollateralAuction(ctx sdk.Context, owner sdk.AccAdd // 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) { +func (k Keeper) StartDebtAuction(ctx sdk.Context) (uint64, 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()) @@ -107,7 +106,7 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context) (auction.ID, sdk.Error) { // 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) { +// func (k Keeper) StartSurplusAuction(ctx sdk.Context) (uint64, sdk.Error) { // // TODO ensure seized debt is 0 diff --git a/x/liquidator/types/expected_keepers.go b/x/liquidator/types/expected_keepers.go index deea2bd4..39bede95 100644 --- a/x/liquidator/types/expected_keepers.go +++ b/x/liquidator/types/expected_keepers.go @@ -3,7 +3,6 @@ package types import ( sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/cdp" ) @@ -26,7 +25,7 @@ type BankKeeper interface { // 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) + StartForwardAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin) (uint64, sdk.Error) + StartReverseAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin) (uint64, sdk.Error) + StartForwardReverseAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin, sdk.AccAddress) (uint64, sdk.Error) } From b2fa8d81eecc2951bfb0d53cc421d0b28912553c Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Mon, 6 Jan 2020 16:35:50 +0000 Subject: [PATCH 36/99] remove unused message types --- x/auction/types/genesis.go | 2 +- x/auction/types/msg.go | 47 ++++---------------------------------- 2 files changed, 6 insertions(+), 43 deletions(-) diff --git a/x/auction/types/genesis.go b/x/auction/types/genesis.go index 27f9eb52..34514250 100644 --- a/x/auction/types/genesis.go +++ b/x/auction/types/genesis.go @@ -9,7 +9,7 @@ type GenesisAuctions []Auction // GenesisState - auction state that must be provided at genesis type GenesisState struct { - NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` + NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` AuctionParams AuctionParams `json:"auction_params" yaml:"auction_params"` Auctions GenesisAuctions `json:"genesis_auctions" yaml:"genesis_auctions"` } diff --git a/x/auction/types/msg.go b/x/auction/types/msg.go index 0014d01e..45c10472 100644 --- a/x/auction/types/msg.go +++ b/x/auction/types/msg.go @@ -2,6 +2,9 @@ package types import sdk "github.com/cosmos/cosmos-sdk/types" +// ensure Msg interface compliance at compile time +var _ sdk.Msg = &MsgPlaceBid{} + // MsgPlaceBid is the message type used to place a bid on any type of auction. type MsgPlaceBid struct { AuctionID uint64 @@ -21,7 +24,7 @@ func NewMsgPlaceBid(auctionID uint64, bidder sdk.AccAddress, bid sdk.Coin, lot s } // Route return the message type used for routing the message. -func (msg MsgPlaceBid) Route() string { return "auction" } +func (msg MsgPlaceBid) Route() string { return RouterKey } // Type returns a human-readable string for the message, intended for utilization within tags. func (msg MsgPlaceBid) Type() string { return "place_bid" } @@ -50,44 +53,4 @@ func (msg MsgPlaceBid) GetSignBytes() []byte { // 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} -// } +} \ No newline at end of file From de4f55ea20e41d5b39887dca3a8b3929e4e4655f Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Tue, 7 Jan 2020 12:17:27 -0500 Subject: [PATCH 37/99] feat: add spec, update redundant type names --- x/auction/alias.go | 24 +++++++--- x/auction/genesis.go | 4 +- x/auction/keeper/auctions_test.go | 6 ++- x/auction/keeper/params.go | 6 +-- x/auction/spec/01_concepts.md | 13 ++++++ x/auction/spec/02_state.md | 76 +++++++++++++++++++++++++++++++ x/auction/spec/03_messages.md | 37 +++++++++++++++ x/auction/spec/04_events.md | 9 ++++ x/auction/spec/05_params.md | 12 +++++ x/auction/spec/06_begin_block.md | 20 ++++++++ x/auction/spec/README.md | 20 ++++++++ x/auction/types/auctions.go | 1 + x/auction/types/genesis.go | 16 +++---- x/auction/types/params.go | 36 +++++++-------- 14 files changed, 241 insertions(+), 39 deletions(-) create mode 100644 x/auction/spec/01_concepts.md create mode 100644 x/auction/spec/02_state.md create mode 100644 x/auction/spec/03_messages.md create mode 100644 x/auction/spec/04_events.md create mode 100644 x/auction/spec/05_params.md create mode 100644 x/auction/spec/06_begin_block.md create mode 100644 x/auction/spec/README.md diff --git a/x/auction/alias.go b/x/auction/alias.go index a3262826..ec4ad50f 100644 --- a/x/auction/alias.go +++ b/x/auction/alias.go @@ -25,21 +25,29 @@ var ( NewForwardAuction = types.NewForwardAuction NewReverseAuction = types.NewReverseAuction NewForwardReverseAuction = types.NewForwardReverseAuction + NewWeightedAddresses = types.NewWeightedAddresses RegisterCodec = types.RegisterCodec NewGenesisState = types.NewGenesisState DefaultGenesisState = types.DefaultGenesisState ValidateGenesis = types.ValidateGenesis + GetAuctionKey = types.GetAuctionKey + GetAuctionByTimeKey = types.GetAuctionByTimeKey + Uint64FromBytes = types.Uint64FromBytes + Uint64ToBytes = types.Uint64ToBytes NewMsgPlaceBid = types.NewMsgPlaceBid - NewAuctionParams = types.NewAuctionParams - DefaultAuctionParams = types.DefaultAuctionParams + NewParams = types.NewParams + DefaultParams = types.DefaultParams ParamKeyTable = types.ParamKeyTable NewKeeper = keeper.NewKeeper NewQuerier = keeper.NewQuerier // variable aliases - ModuleCdc = types.ModuleCdc - KeyAuctionBidDuration = types.KeyAuctionBidDuration - KeyAuctionDuration = types.KeyAuctionDuration + ModuleCdc = types.ModuleCdc + AuctionKeyPrefix = types.AuctionKeyPrefix + AuctionByTimeKeyPrefix = types.AuctionByTimeKeyPrefix + NextAuctionIDKey = types.NextAuctionIDKey + KeyAuctionBidDuration = types.KeyAuctionBidDuration + KeyAuctionDuration = types.KeyAuctionDuration ) type ( @@ -48,10 +56,12 @@ type ( ForwardAuction = types.ForwardAuction ReverseAuction = types.ReverseAuction ForwardReverseAuction = types.ForwardReverseAuction - GenesisAuctions = types.GenesisAuctions + WeightedAddresses = types.WeightedAddresses + SupplyKeeper = types.SupplyKeeper + Auctions = types.Auctions GenesisState = types.GenesisState MsgPlaceBid = types.MsgPlaceBid - AuctionParams = types.AuctionParams + Params = types.Params QueryResAuctions = types.QueryResAuctions Keeper = keeper.Keeper ) diff --git a/x/auction/genesis.go b/x/auction/genesis.go index 6136df9e..8b4a5f1e 100644 --- a/x/auction/genesis.go +++ b/x/auction/genesis.go @@ -8,7 +8,7 @@ import ( func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { keeper.SetNextAuctionID(ctx, data.NextAuctionID) - keeper.SetParams(ctx, data.AuctionParams) + keeper.SetParams(ctx, data.Params) for _, a := range data.Auctions { keeper.SetAuction(ctx, a) @@ -24,7 +24,7 @@ func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { params := keeper.GetParams(ctx) - var genAuctions GenesisAuctions + var genAuctions Auctions keeper.IterateAuctions(ctx, func(a Auction) bool { genAuctions = append(genAuctions, a) return false diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index c5545476..696c3382 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -49,11 +49,15 @@ func TestForwardAuctionBasic(t *testing.T) { // Check seller's coins have not increased (because proceeds are burned) tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) + // increment bid same bidder + err = keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 20), c("token1", 20)) + require.NoError(t, err) + // Close auction at just at auction expiry time ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration)) require.NoError(t, keeper.CloseAuction(ctx, auctionID)) // Check buyer's coins increased - tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90))) + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 80))) } func TestReverseAuctionBasic(t *testing.T) { diff --git a/x/auction/keeper/params.go b/x/auction/keeper/params.go index c832caf5..bd2d67de 100644 --- a/x/auction/keeper/params.go +++ b/x/auction/keeper/params.go @@ -6,12 +6,12 @@ import ( ) // SetParams sets the auth module's parameters. -func (k Keeper) SetParams(ctx sdk.Context, params types.AuctionParams) { +func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { k.paramSubspace.SetParamSet(ctx, ¶ms) } // GetParams gets the auth module's parameters. -func (k Keeper) GetParams(ctx sdk.Context) (params types.AuctionParams) { +func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) { k.paramSubspace.GetParamSet(ctx, ¶ms) return -} \ No newline at end of file +} diff --git a/x/auction/spec/01_concepts.md b/x/auction/spec/01_concepts.md new file mode 100644 index 00000000..f656b615 --- /dev/null +++ b/x/auction/spec/01_concepts.md @@ -0,0 +1,13 @@ + + +# Concepts + +Auctions are broken down into three distinct types, which correspond to three specific functionalities within the CDP system. + +* **Forward Auction:** An auction in which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 they are willing to pay for the lot of c1. After the completion of a forward auction, the winning bid of c2 is burned, and the bidder receives the lot of c1. As a concrete example, forward auction are used to sell a fixed amount of USDX stable coins in exchange for increasing bids of KAVA governance tokens. The governance tokens are then burned and the winner receives USDX. +* **Reverse Auction:** An auction in which a fixed amount of coins (c1) is bid for a decreasing lot of other coins (c2). Bidders decrement the lot of c2 they are willing to receive for the fixed amount of c1. As a concrete example, reverse auctions are used to raise a certain amount of USDX stable coins in exchange for decreasing lots of KAVA governance tokens. The USDX tokens are used to recapitalize the cdp system and the winner receives KAVA. +* **Forward Reverse Auction:** An two phase auction is which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 until a specific `maxBid` is reached. Once `maxBid` is reached, a fixed amount of c2 is bid for a decreasing lot of c1. In the second phase, bidders decrement the lot of c1 they are willing to receive for a fixed amount of c2. As a concrete example. forward reverse auctions are used to sell collateral (ATOM, for example) for up to a `maxBid` amount of USDX. The USDX tokens are used to recapitalize the cdp system and the winner receives the specified lot of ATOM. In the event that the winning lot is smaller than the total lot, the excess ATOM is ratably returned to the original owners of the liquidated CDPs that were collateralized with that ATOM. + +Auctions are always initiated by another module, and not directly by users. Auctions start with an expiry, the time at which the auction is guaranteed to end, even if there have been no bidders. After each bid, the auction is extended by a specific amount of time, `BidDuration`. In the case that increasing the auction time by `BidDuration` would cause the auction to go past its expiry, the expiry is chosen as the ending time. diff --git a/x/auction/spec/02_state.md b/x/auction/spec/02_state.md new file mode 100644 index 00000000..a1c39f84 --- /dev/null +++ b/x/auction/spec/02_state.md @@ -0,0 +1,76 @@ + + +# State + +## Parameters and genesis state + +`Paramaters` define the rules according to which auctions are run. There is only one active parameter set at any given time. Updates to the parameter set can be made via on-chain parameter update proposals. + +```go +// Params governance parameters for auction module +type Params struct { + MaxAuctionDuration time.Duration `json:"max_auction_duration" yaml:"max_auction_duration"` // max length of auction + MaxBidDuration time.Duration `json:"max_bid_duration" yaml:"max_bid_duration"` // additional time added to the auction end time after each bid, capped by the expiry. +} +``` + +`GenesisState` defines the state that must be persisted when the blockchain stops/restarts in order for normal function of the auction module to resume. + +```go +// GenesisState - auction state that must be provided at genesis +type GenesisState struct { + NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` // auctionID that will be used for the next created auction + Params Params `json:"auction_params" yaml:"auction_params"` // auction params + Auctions Auctions `json:"genesis_auctions" yaml:"genesis_auctions"` // auctions currently in the store +} +``` + +## Base types + +```go +// Auction is an interface to several types of auction. +type Auction interface { + GetID() uint64 + WithID(uint64) Auction + GetBidder() sdk.AccAddress + GetBid() sdk.Coin + GetLot() sdk.Coin + GetEndTime() time.Time +} + +// BaseAuction type shared by all Auctions +type BaseAuction struct { + ID uint64 + Initiator string // Module that starts the auction. Giving away Lot (aka seller in a forward auction). Restricted to being a module account name rather than any account. + 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 time.Time // Auction closing time. Triggers at the end of the block with time ≥ endTime (bids placed in that block are valid) // TODO ensure everything is consistent with this + MaxEndTime time.Time // Maximum closing time. Auctions can close before this but never after. +} + +// ForwardAuction type for forward auctions +type ForwardAuction struct { + BaseAuction +} + +// ReverseAuction type for reverse auctions +type ReverseAuction struct { + BaseAuction +} + +// WeightedAddresses type for storing an address and its associated weight +type WeightedAddresses struct { + Addresses []sdk.AccAddress + Weights []sdk.Int +} + +// ForwardReverseAuction type for forward reverse auction +type ForwardReverseAuction struct { + BaseAuction + MaxBid sdk.Coin + LotReturns WeightedAddresses // return addresses to pay out reductions in the lot amount to. Lot is bid down during reverse phase. +} +``` diff --git a/x/auction/spec/03_messages.md b/x/auction/spec/03_messages.md new file mode 100644 index 00000000..5c8ece91 --- /dev/null +++ b/x/auction/spec/03_messages.md @@ -0,0 +1,37 @@ + + +# Messages + +## Bidding + +Users can bid on auctions using the `MsgPlaceBid` message type. All auction types can be bid on using the same message type. + +```go +// MsgPlaceBid is the message type used to place a bid on any type of auction. +type MsgPlaceBid struct { + AuctionID uint64 + Bidder sdk.AccAddress + Bid sdk.Coin + Lot sdk.Coin +} +``` + +**State Modifications:** + +* Update bidder if different than previous bidder +* For forward auctions: + * Update Bid Amount + * Return bid coins to previous bidder + * Burn coins equal to the increment in the bid (CurrentBid - PreviousBid) +* For Reverse auctions: + * Update lot amount + * Return bid coins to previous bidder +* For Forward Reverse auctions: + * Return bid coins to previous bidder + * If in forward phase: + * Update bid amount + * If in reverse phase: + * Update lot amount +* Extend auction by `BidDuration`, or `MaxEndTime` diff --git a/x/auction/spec/04_events.md b/x/auction/spec/04_events.md new file mode 100644 index 00000000..3bc15051 --- /dev/null +++ b/x/auction/spec/04_events.md @@ -0,0 +1,9 @@ + + +# Events + + diff --git a/x/auction/spec/05_params.md b/x/auction/spec/05_params.md new file mode 100644 index 00000000..621d26aa --- /dev/null +++ b/x/auction/spec/05_params.md @@ -0,0 +1,12 @@ + + +# Parameters + +The auction module contains the following parameters: + +| Key | Type | Example | +| ------------------ | ---------------------- | -----------| +| MaxAuctionDuration | string (time.Duration) | "48h0m0s" | +| MaxBidDuration | string (time.Duration) | "3h0m0s" | diff --git a/x/auction/spec/06_begin_block.md b/x/auction/spec/06_begin_block.md new file mode 100644 index 00000000..2ecfcd54 --- /dev/null +++ b/x/auction/spec/06_begin_block.md @@ -0,0 +1,20 @@ + + +At the beginning of each block, auctions that have reached `EndTime` are closed. The logic to close auctions is as follows: + +```go +var expiredAuctions []uint64 + k.IterateAuctionsByTime(ctx, ctx.BlockTime(), func(id uint64) bool { + expiredAuctions = append(expiredAuctions, id) + return false + }) + + for _, id := range expiredAuctions { + err := k.CloseAuction(ctx, id) + if err != nil { + panic(err) + } + } +``` diff --git a/x/auction/spec/README.md b/x/auction/spec/README.md new file mode 100644 index 00000000..c68ab587 --- /dev/null +++ b/x/auction/spec/README.md @@ -0,0 +1,20 @@ + + +# `auction` + + +1. **[Concepts](01_concepts.md)** +2. **[State](02_state.md)** +3. **[Messages](03_messages.md)** +4. **[Events](04_events.md)** +5. **[Params](05_params.md)** +6. **[BeginBlock](06_begin_block.md)** + +## Abstract + +`x/auction` is an implementation of a Cosmos SDK Module that handles the creation, bidding, and payout of 3 distinct auction types. All auction types implement the `Auction` interface. Each auction type is used at different points during the normal functioning of the CDP system. diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 5b9b079f..1aafaa36 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -149,6 +149,7 @@ func NewForwardReverseAuction(seller string, lot sdk.Coin, EndTime time.Time, ma return auction } +// WeightedAddresses type for storing an address and its associated weight type WeightedAddresses struct { Addresses []sdk.AccAddress Weights []sdk.Int diff --git a/x/auction/types/genesis.go b/x/auction/types/genesis.go index 34514250..423d12fd 100644 --- a/x/auction/types/genesis.go +++ b/x/auction/types/genesis.go @@ -4,28 +4,28 @@ import ( "bytes" ) -// GenesisAuctions type for an array of auctions -type GenesisAuctions []Auction +// Auctions type for an array of auctions +type Auctions []Auction // GenesisState - auction state that must be provided at genesis type GenesisState struct { NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` - AuctionParams AuctionParams `json:"auction_params" yaml:"auction_params"` - Auctions GenesisAuctions `json:"genesis_auctions" yaml:"genesis_auctions"` + Params Params `json:"auction_params" yaml:"auction_params"` + Auctions Auctions `json:"genesis_auctions" yaml:"genesis_auctions"` } // NewGenesisState returns a new genesis state object for auctions module -func NewGenesisState(nextID uint64, ap AuctionParams, ga GenesisAuctions) GenesisState { +func NewGenesisState(nextID uint64, ap Params, ga Auctions) GenesisState { return GenesisState{ NextAuctionID: nextID, - AuctionParams: ap, + Params: ap, Auctions: ga, } } // DefaultGenesisState defines default genesis state for auction module func DefaultGenesisState() GenesisState { - return NewGenesisState(0, DefaultAuctionParams(), GenesisAuctions{}) + return NewGenesisState(0, DefaultParams(), Auctions{}) } // Equal checks whether two GenesisState structs are equivalent @@ -42,7 +42,7 @@ func (data GenesisState) IsEmpty() bool { // ValidateGenesis validates genesis inputs. Returns error if validation of any input fails. func ValidateGenesis(data GenesisState) error { - if err := data.AuctionParams.Validate(); err != nil { + if err := data.Params.Validate(); err != nil { return err } return nil diff --git a/x/auction/types/params.go b/x/auction/types/params.go index 5b7cfdf0..1aa0276d 100644 --- a/x/auction/types/params.go +++ b/x/auction/types/params.go @@ -18,30 +18,30 @@ const ( // Parameter keys var ( - // ParamStoreKeyAuctionParams Param store key for auction params + // ParamStoreKeyParams Param store key for auction params KeyAuctionBidDuration = []byte("MaxBidDuration") KeyAuctionDuration = []byte("MaxAuctionDuration") ) -var _ subspace.ParamSet = &AuctionParams{} +var _ subspace.ParamSet = &Params{} -// AuctionParams governance parameters for auction module -type AuctionParams struct { - MaxAuctionDuration time.Duration `json:"max_auction_duration" yaml:"max_auction_duration"` // max length of auction, in blocks - MaxBidDuration time.Duration `json:"max_bid_duration" yaml:"max_bid_duration"` +// Params governance parameters for auction module +type Params struct { + MaxAuctionDuration time.Duration `json:"max_auction_duration" yaml:"max_auction_duration"` // max length of auction + MaxBidDuration time.Duration `json:"max_bid_duration" yaml:"max_bid_duration"` // additional time added to the auction end time after each bid, capped by the expiry. } -// NewAuctionParams creates a new AuctionParams object -func NewAuctionParams(maxAuctionDuration time.Duration, bidDuration time.Duration) AuctionParams { - return AuctionParams{ +// NewParams creates a new Params object +func NewParams(maxAuctionDuration time.Duration, bidDuration time.Duration) Params { + return Params{ MaxAuctionDuration: maxAuctionDuration, MaxBidDuration: bidDuration, } } -// DefaultAuctionParams default parameters for auctions -func DefaultAuctionParams() AuctionParams { - return NewAuctionParams( +// DefaultParams default parameters for auctions +func DefaultParams() Params { + return NewParams( DefaultMaxAuctionDuration, DefaultBidDuration, ) @@ -49,35 +49,35 @@ func DefaultAuctionParams() AuctionParams { // ParamKeyTable Key declaration for parameters func ParamKeyTable() subspace.KeyTable { - return subspace.NewKeyTable().RegisterParamSet(&AuctionParams{}) + return subspace.NewKeyTable().RegisterParamSet(&Params{}) } // 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 { +func (ap *Params) ParamSetPairs() subspace.ParamSetPairs { return subspace.ParamSetPairs{ {KeyAuctionBidDuration, &ap.MaxBidDuration}, {KeyAuctionDuration, &ap.MaxAuctionDuration}, } } -// Equal returns a boolean determining if two AuctionParams types are identical. -func (ap AuctionParams) Equal(ap2 AuctionParams) bool { +// Equal returns a boolean determining if two Params types are identical. +func (ap Params) Equal(ap2 Params) bool { bz1 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&ap) bz2 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&ap2) return bytes.Equal(bz1, bz2) } // String implements stringer interface -func (ap AuctionParams) String() string { +func (ap Params) String() string { return fmt.Sprintf(`Auction Params: Max Auction Duration: %s Max Bid Duration: %s`, ap.MaxAuctionDuration, ap.MaxBidDuration) } // Validate checks that the parameters have valid values. -func (ap AuctionParams) Validate() error { +func (ap Params) Validate() error { // TODO check durations are within acceptable limits, if needed return nil } From 3a7cb7e4f6de01a1cadea8992309113f2bfd8d74 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 9 Jan 2020 13:55:45 +0000 Subject: [PATCH 38/99] stop sending zero coins --- x/auction/keeper/auctions.go | 158 ++++++++++++++++-------------- x/auction/keeper/auctions_test.go | 3 +- x/auction/types/auctions.go | 15 +-- x/auction/types/params.go | 4 +- 4 files changed, 100 insertions(+), 80 deletions(-) diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index 51c7c804..38108079 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -34,7 +34,7 @@ func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer string, bid sdk.Coin, // This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker. macc := k.supplyKeeper.GetModuleAccount(ctx, buyer) - if !macc.HasPermission(supply.Minter) { // TODO ideally don't want to import supply + if !macc.HasPermission(supply.Minter) { return 0, sdk.ErrInternal("module does not have minting permissions") } // store the auction @@ -103,7 +103,11 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddres return err } case types.ForwardReverseAuction: - a, err = k.PlaceBidForwardReverse(ctx, auc, bidder, bid, lot) + if !auc.IsReversePhase() { + a, err = k.PlaceBidForwardReverseForward(ctx, auc, bidder, bid) + } else { + a, err = k.PlaceBidForwardReverseReverse(ctx, auc, bidder, lot) + } if err != nil { return err } @@ -126,24 +130,23 @@ func (k Keeper) PlaceBidForward(ctx sdk.Context, a types.ForwardAuction, bidder } // Move Coins - increment := bid.Sub(a.Bid) - bidAmtToReturn := a.Bid - if bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance - bidAmtToReturn = sdk.NewInt64Coin(a.Bid.Denom, 0) + if !bidder.Equals(a.Bidder) && !a.Bid.IsZero() { // catch edge case of someone updating their bid with a low balance, also don't send if amt is zero + // pay back previous bidder + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err + } } - err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(bidAmtToReturn.Add(increment))) + // burn increase in bid + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, a.Initiator, sdk.NewCoins(bid.Sub(a.Bid))) if err != nil { return a, err } - err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(bidAmtToReturn)) - if err != nil { - return a, err - } - err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(increment)) // increase in bid size is burned - if err != nil { - return a, err - } - err = k.supplyKeeper.BurnCoins(ctx, a.Initiator, sdk.NewCoins(increment)) + err = k.supplyKeeper.BurnCoins(ctx, a.Initiator, sdk.NewCoins(bid.Sub(a.Bid))) if err != nil { return a, err } @@ -156,56 +159,71 @@ func (k Keeper) PlaceBidForward(ctx sdk.Context, a types.ForwardAuction, bidder return a, nil } -func (k Keeper) PlaceBidForwardReverse(ctx sdk.Context, a types.ForwardReverseAuction, bidder sdk.AccAddress, bid sdk.Coin, lot sdk.Coin) (types.ForwardReverseAuction, sdk.Error) { - // Validate New Bid // TODO min bid increments, make validation code less confusing - if !a.Bid.IsEqual(a.MaxBid) { - // Auction is in forward phase, a bid here can put the auction into forward or reverse phases - if !a.Bid.IsLT(bid) { - return a, sdk.ErrInternal("auction in forward phase, new bid not higher than last bid") + +// TODO naming +func (k Keeper) PlaceBidForwardReverseForward(ctx sdk.Context, a types.ForwardReverseAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.ForwardReverseAuction, sdk.Error) { + // Validate bid + if a.IsReversePhase() { + return a, sdk.ErrInternal("auction is not in forward phase") + } + if !a.Bid.IsLT(bid) { + return a, sdk.ErrInternal("auction in forward phase, new bid not higher than last bid") + } + if a.MaxBid.IsLT(bid) { + return a, sdk.ErrInternal("bid higher than max bid") + } + // Move Coins + // pay back previous bidder + if !bidder.Equals(a.Bidder) && !a.Bid.IsZero() { // catch edge case of someone updating their bid with a low balance, also don't send if amt is zero + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err } - if a.MaxBid.IsLT(bid) { - return a, sdk.ErrInternal("bid higher than max bid") - } - if lot.IsNegative() || a.Lot.IsLT(lot) { - return a, sdk.ErrInternal("lot out of bounds") - } - if lot.IsLT(a.Lot) && !bid.IsEqual(a.MaxBid) { - return a, sdk.ErrInternal("auction cannot enter reverse phase without bidding max bid") - } - } else { - // Auction is in reverse phase, it can never leave reverse phase - if !bid.IsEqual(a.MaxBid) { - return a, sdk.ErrInternal("") // not necessary - } - if lot.IsNegative() { - return a, sdk.ErrInternal("can't bid negative amount") - } - if !lot.IsLT(a.Lot) { - return a, sdk.ErrInternal("auction in reverse phase, new bid not less than previous amount") + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err } } + // pay increase in bid to auction initiator + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, a.Initiator, sdk.NewCoins(bid.Sub(a.Bid))) + if err != nil { + return a, err + } + + // Update Auction + a.Bidder = bidder + a.Bid = bid + // increment timeout + a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultBidDuration), a.MaxEndTime) + + return a, nil +} + +func (k Keeper) PlaceBidForwardReverseReverse(ctx sdk.Context, a types.ForwardReverseAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.ForwardReverseAuction, sdk.Error) { + // Validate bid + if !a.IsReversePhase() { + return a, sdk.ErrInternal("auction not in reverse phase") + } + if lot.IsNegative() { + return a, sdk.ErrInternal("can't bid negative amount") + } + if !lot.IsLT(a.Lot) { + return a, sdk.ErrInternal("auction in reverse phase, new bid not less than previous amount") + } // Move Coins - bidIncrement := bid.Sub(a.Bid) - bidAmtToReturn := a.Bid - lotDecrement := a.Lot.Sub(lot) - if bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance - bidAmtToReturn = sdk.NewInt64Coin(a.Bid.Denom, 0) - } - err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(bidAmtToReturn.Add(bidIncrement))) - if err != nil { - return a, err - } - err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(bidAmtToReturn)) - if err != nil { - return a, err - } - err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(bidIncrement)) - if err != nil { - return a, err + if !bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err + } } // FIXME paying out rateably to cdp depositors is vulnerable to errors compounding over multiple bids - lotPayouts, err := splitCoinIntoWeightedBuckets(lotDecrement, a.LotReturns.Weights) + lotPayouts, err := splitCoinIntoWeightedBuckets(a.Lot.Sub(lot), a.LotReturns.Weights) if err != nil { return a, err } @@ -219,12 +237,12 @@ func (k Keeper) PlaceBidForwardReverse(ctx sdk.Context, a types.ForwardReverseAu // Update Auction a.Bidder = bidder a.Lot = lot - a.Bid = bid // increment timeout a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultBidDuration), a.MaxEndTime) return a, nil } + func (k Keeper) PlaceBidReverse(ctx sdk.Context, a types.ReverseAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.ReverseAuction, sdk.Error) { // Validate New Bid if lot.Denom != a.Lot.Denom { @@ -238,17 +256,15 @@ func (k Keeper) PlaceBidReverse(ctx sdk.Context, a types.ReverseAuction, bidder } // Move Coins - bidAmtToReturn := a.Bid - if bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance - bidAmtToReturn = sdk.NewInt64Coin(a.Bid.Denom, 0) - } - err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(bidAmtToReturn)) - if err != nil { - return a, err - } - err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(bidAmtToReturn)) - if err != nil { - return a, err + if !bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err + } } // Update Auction diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index 696c3382..1f10a28c 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -140,7 +140,8 @@ func TestForwardReverseAuctionBasic(t *testing.T) { } // Place a reverse bid - require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50), c("token1", 15))) // bid, lot + require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50), c("token1", 15))) // first bid up to max bid to switch phases + require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50), c("token1", 15))) // Check bidder's coins have decreased tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50))) // Check seller's coins have increased diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 1aafaa36..fa5e1163 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -72,7 +72,7 @@ func NewForwardAuction(seller string, lot sdk.Coin, bidDenom string, endTime tim // no ID Initiator: seller, Lot: lot, - Bidder: nil, // TODO on the first place bid, 0 coins will be sent to this address, check if this causes problems or can be avoided + Bidder: nil, Bid: sdk.NewInt64Coin(bidDenom, 0), EndTime: endTime, MaxEndTime: endTime, @@ -90,10 +90,9 @@ func (a ReverseAuction) WithID(id uint64) Auction { a.ID = id; return a } // NewReverseAuction creates a new reverse auction func NewReverseAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime time.Time) ReverseAuction { - // TODO setting the bidder here is a bit hacky - // Needs to be set so that when the first bid is placed, it is paid out to the initiator. - // Setting to the module account address bypasses calling supply.SendCoinsFromModuleToModule, instead calls SendCoinsFromModuleToModule. Not a problem currently but if checks/logic regarding modules accounts where added to those methods they would be bypassed. - // Alternative: set address to nil, and catch it in an if statement in place bid + // Note: Bidder is set to the initiator's module account address instead of module name. (when the first bid is placed, it is paid out to the initiator) + // Setting to the module account address bypasses calling supply.SendCoinsFromModuleToModule, instead calls SendCoinsFromModuleToAccount. + // This isn't a problem currently, but if additional logic/validation was added for sending to coins to Module Accounts, it would be bypassed. auction := ReverseAuction{BaseAuction{ // no ID Initiator: buyerModAccName, @@ -116,6 +115,10 @@ type ForwardReverseAuction struct { // WithID returns an auction with the ID set func (a ForwardReverseAuction) WithID(id uint64) Auction { a.ID = id; return a } +func (a ForwardReverseAuction) IsReversePhase() bool { + return a.Bid.IsEqual(a.MaxBid) +} + func (a ForwardReverseAuction) String() string { return fmt.Sprintf(`Auction %d: Initiator: %s @@ -139,7 +142,7 @@ func NewForwardReverseAuction(seller string, lot sdk.Coin, EndTime time.Time, ma // no ID Initiator: seller, Lot: lot, - Bidder: nil, // TODO on the first place bid, 0 coins will be sent to this address, check if this causes problems or can be avoided + Bidder: nil, Bid: sdk.NewInt64Coin(maxBid.Denom, 0), EndTime: EndTime, MaxEndTime: EndTime}, diff --git a/x/auction/types/params.go b/x/auction/types/params.go index 1aa0276d..0bbf075d 100644 --- a/x/auction/types/params.go +++ b/x/auction/types/params.go @@ -12,8 +12,8 @@ import ( const ( // DefaultMaxAuctionDuration max length of auction DefaultMaxAuctionDuration time.Duration = 2 * 24 * time.Hour - // DefaultBidDuration how long an auction gets extended when someone bids, roughly 3 hours in blocks - DefaultBidDuration time.Duration = 3 * time.Hour + // DefaultBidDuration how long an auction gets extended when someone bids + DefaultBidDuration time.Duration = 1 * time.Hour ) // Parameter keys From 48a2d5b6dcc5a71f85b9cd69ed7f3d31324498b6 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 9 Jan 2020 14:58:47 +0000 Subject: [PATCH 39/99] use only one coins field in MsgPlaceBid --- x/auction/abci_test.go | 2 +- x/auction/client/cli/tx.go | 15 +++++---------- x/auction/client/rest/tx.go | 11 ++--------- x/auction/handler.go | 2 +- x/auction/keeper/auctions.go | 23 +++++++++++------------ x/auction/keeper/auctions_test.go | 12 ++++++------ x/auction/types/msg.go | 20 +++++++------------- x/auction/types/msg_test.go | 24 +++++++++++++++++------- 8 files changed, 50 insertions(+), 59 deletions(-) diff --git a/x/auction/abci_test.go b/x/auction/abci_test.go index 79f526b5..19bc1792 100644 --- a/x/auction/abci_test.go +++ b/x/auction/abci_test.go @@ -39,7 +39,7 @@ func TestKeeper_EndBlocker(t *testing.T) { auctionID, err := keeper.StartForwardReverseAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights) require.NoError(t, err) - require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 30), c("token1", 20))) + require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 30))) // Run the endblocker, simulating a block time 1ns before auction expiry preExpiryTime := ctx.BlockTime().Add(auction.DefaultBidDuration - 1) diff --git a/x/auction/client/cli/tx.go b/x/auction/client/cli/tx.go index 2ced6610..e705eb74 100644 --- a/x/auction/client/cli/tx.go +++ b/x/auction/client/cli/tx.go @@ -33,9 +33,9 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { // GetCmdPlaceBid cli command for creating and modifying cdps. func GetCmdPlaceBid(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "placebid [AuctionID] [Bidder] [Bid] [Lot]", + Use: "placebid [auctionID] [amount]", Short: "place a bid on an auction", - Args: cobra.ExactArgs(4), + Args: cobra.MinimumNArgs(2), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) @@ -46,18 +46,13 @@ func GetCmdPlaceBid(cdc *codec.Codec) *cobra.Command { return err } - bid, err := sdk.ParseCoin(args[2]) + amt, err := sdk.ParseCoin(args[2]) if err != nil { - fmt.Printf("invalid bid amount - %s \n", string(args[2])) + fmt.Printf("invalid 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) + msg := types.NewMsgPlaceBid(id, cliCtx.GetFromAddress(), amt) err = msg.ValidateBasic() if err != nil { return err diff --git a/x/auction/client/rest/tx.go b/x/auction/client/rest/tx.go index 5773f24a..910714ca 100644 --- a/x/auction/client/rest/tx.go +++ b/x/auction/client/rest/tx.go @@ -33,7 +33,7 @@ const ( 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") + fmt.Sprintf("/auction/bid/{%s}/{%s}/{%s}", restAuctionID, restBidder, restBid), bidHandlerFn(cliCtx)).Methods("PUT") } func bidHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { @@ -44,7 +44,6 @@ func bidHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { strAuctionID := vars[restAuctionID] bechBidder := vars[restBidder] strBid := vars[restBid] - strLot := vars[restLot] auctionID, err := strconv.ParseUint(strAuctionID, 10, 64) if err != nil { @@ -64,13 +63,7 @@ func bidHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return } - lot, err := sdk.ParseCoin(strLot) - if err != nil { - rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } - - msg := types.NewMsgPlaceBid(auctionID, bidder, bid, lot) + msg := types.NewMsgPlaceBid(auctionID, bidder, bid) if err := msg.ValidateBasic(); err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return diff --git a/x/auction/handler.go b/x/auction/handler.go index ec238957..f9b7c137 100644 --- a/x/auction/handler.go +++ b/x/auction/handler.go @@ -21,7 +21,7 @@ func NewHandler(keeper Keeper) sdk.Handler { func handleMsgPlaceBid(ctx sdk.Context, keeper Keeper, msg MsgPlaceBid) sdk.Result { - err := keeper.PlaceBid(ctx, msg.AuctionID, msg.Bidder, msg.Bid, msg.Lot) + err := keeper.PlaceBid(ctx, msg.AuctionID, msg.Bidder, msg.Amount) if err != nil { return err.Result() } diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index 38108079..6aa27fd6 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -68,8 +68,7 @@ func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller string, lot s } // PlaceBid places a bid on any auction. -// TODO passing bid and lot is weird when only one needed -func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddress, bid sdk.Coin, lot sdk.Coin) sdk.Error { +func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddress, newAmount sdk.Coin) sdk.Error { // get auction from store auction, found := k.GetAuction(ctx, auctionID) @@ -81,32 +80,26 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddres if ctx.BlockTime().After(auction.GetEndTime()) { return sdk.ErrInternal("auction has closed") } - if auction.GetBid().Denom != bid.Denom { - return sdk.ErrInternal("bid has incorrect denom") - } - if auction.GetLot().Denom != lot.Denom { - return sdk.ErrInternal("lot has incorrect denom") - } // place bid var err sdk.Error var a types.Auction switch auc := auction.(type) { case types.ForwardAuction: - a, err = k.PlaceBidForward(ctx, auc, bidder, bid) + a, err = k.PlaceBidForward(ctx, auc, bidder, newAmount) if err != nil { return err } case types.ReverseAuction: - a, err = k.PlaceBidReverse(ctx, auc, bidder, lot) + a, err = k.PlaceBidReverse(ctx, auc, bidder, newAmount) if err != nil { return err } case types.ForwardReverseAuction: if !auc.IsReversePhase() { - a, err = k.PlaceBidForwardReverseForward(ctx, auc, bidder, bid) + a, err = k.PlaceBidForwardReverseForward(ctx, auc, bidder, newAmount) } else { - a, err = k.PlaceBidForwardReverseReverse(ctx, auc, bidder, lot) + a, err = k.PlaceBidForwardReverseReverse(ctx, auc, bidder, newAmount) } if err != nil { return err @@ -163,6 +156,9 @@ func (k Keeper) PlaceBidForward(ctx sdk.Context, a types.ForwardAuction, bidder // TODO naming func (k Keeper) PlaceBidForwardReverseForward(ctx sdk.Context, a types.ForwardReverseAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.ForwardReverseAuction, sdk.Error) { // Validate bid + if bid.Denom != a.Bid.Denom { + return a, sdk.ErrInternal("bid denom doesn't match auction") + } if a.IsReversePhase() { return a, sdk.ErrInternal("auction is not in forward phase") } @@ -201,6 +197,9 @@ func (k Keeper) PlaceBidForwardReverseForward(ctx sdk.Context, a types.ForwardRe func (k Keeper) PlaceBidForwardReverseReverse(ctx sdk.Context, a types.ForwardReverseAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.ForwardReverseAuction, sdk.Error) { // Validate bid + if lot.Denom != a.Lot.Denom { + return a, sdk.ErrInternal("lot denom doesn't match auction") + } if !a.IsReversePhase() { return a, sdk.ErrInternal("auction not in reverse phase") } diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index 1f10a28c..7d7f3073 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -43,14 +43,14 @@ func TestForwardAuctionBasic(t *testing.T) { tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) // PlaceBid (bid: 10 token, lot: same as starting) - require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 10), c("token1", 20))) // bid, lot + require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 10))) // Check buyer's coins have decreased tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) // Check seller's coins have not increased (because proceeds are burned) tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) // increment bid same bidder - err = keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 20), c("token1", 20)) + err = keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 20)) require.NoError(t, err) // Close auction at just at auction expiry time @@ -85,7 +85,7 @@ func TestReverseAuctionBasic(t *testing.T) { tApp.CheckBalance(t, ctx, buyerAddr, nil) // zero coins // Place a bid - require.NoError(t, keeper.PlaceBid(ctx, 0, seller, c("token1", 20), c("token2", 10))) // bid, lot + require.NoError(t, keeper.PlaceBid(ctx, 0, seller, c("token2", 10))) // Check seller's coins have decreased tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) // Check buyer's coins have increased @@ -129,7 +129,7 @@ func TestForwardReverseAuctionBasic(t *testing.T) { tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) // Place a forward bid - require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 10), c("token1", 20))) // bid, lot + require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 10))) // Check bidder's coins have decreased tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) // Check seller's coins have increased @@ -140,8 +140,8 @@ func TestForwardReverseAuctionBasic(t *testing.T) { } // Place a reverse bid - require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50), c("token1", 15))) // first bid up to max bid to switch phases - require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50), c("token1", 15))) + require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50))) // first bid up to max bid to switch phases + require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token1", 15))) // Check bidder's coins have decreased tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50))) // Check seller's coins have increased diff --git a/x/auction/types/msg.go b/x/auction/types/msg.go index 45c10472..dbaf43b2 100644 --- a/x/auction/types/msg.go +++ b/x/auction/types/msg.go @@ -9,17 +9,15 @@ var _ sdk.Msg = &MsgPlaceBid{} type MsgPlaceBid struct { AuctionID uint64 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 + Amount sdk.Coin // The new bid or lot to set on the auction } // NewMsgPlaceBid returns a new MsgPlaceBid. -func NewMsgPlaceBid(auctionID uint64, bidder sdk.AccAddress, bid sdk.Coin, lot sdk.Coin) MsgPlaceBid { +func NewMsgPlaceBid(auctionID uint64, bidder sdk.AccAddress, amt sdk.Coin) MsgPlaceBid { return MsgPlaceBid{ AuctionID: auctionID, Bidder: bidder, - Bid: bid, - Lot: lot, + Amount: amt, } } @@ -29,18 +27,14 @@ func (msg MsgPlaceBid) Route() string { return RouterKey } // 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. +// ValidateBasic does a simple validation check that doesn't require access to state. 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.Amount.IsValid() { + return sdk.ErrInternal("invalid bid amount") } - if msg.Lot.Amount.LT(sdk.ZeroInt()) { - return sdk.ErrInternal("invalid (negative) lot amount") - } - // TODO check coin denoms return nil } @@ -53,4 +47,4 @@ func (msg MsgPlaceBid) GetSignBytes() []byte { // GetSigners returns the addresses of signers that must sign. func (msg MsgPlaceBid) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Bidder} -} \ No newline at end of file +} diff --git a/x/auction/types/msg_test.go b/x/auction/types/msg_test.go index cfb51b5c..f78e7102 100644 --- a/x/auction/types/msg_test.go +++ b/x/auction/types/msg_test.go @@ -14,19 +14,29 @@ func TestMsgPlaceBid_ValidateBasic(t *testing.T) { 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}, + {"normal", + NewMsgPlaceBid(0, addr, c("token", 10)), + true}, + {"emptyAddr", + NewMsgPlaceBid(0, sdk.AccAddress{}, c("token", 10)), + false}, + {"negativeAmount", + NewMsgPlaceBid(0, addr, sdk.Coin{Denom: "token", Amount: sdk.NewInt(-10)}), + false}, + {"zeroAmount", + NewMsgPlaceBid(0, addr, c("token", 0)), + true}, } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if tc.expectPass { - require.Nil(t, tc.msg.ValidateBasic()) + require.NoError(t, tc.msg.ValidateBasic()) } else { - require.NotNil(t, tc.msg.ValidateBasic()) + require.Error(t, tc.msg.ValidateBasic()) } }) } } + +func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } From 08689305cc53a72598db0775db249b1905045b95 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 9 Jan 2020 15:43:42 +0000 Subject: [PATCH 40/99] remove uncessary Auction interface methods --- x/auction/keeper/auctions.go | 52 +++++++++++++++++++++--------------- x/auction/types/auctions.go | 12 --------- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index 6aa27fd6..51e05a43 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -83,23 +83,21 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddres // place bid var err sdk.Error - var a types.Auction - switch auc := auction.(type) { + var updatedAuction types.Auction + switch a := auction.(type) { case types.ForwardAuction: - a, err = k.PlaceBidForward(ctx, auc, bidder, newAmount) - if err != nil { + if updatedAuction, err = k.PlaceBidForward(ctx, a, bidder, newAmount); err != nil { return err } case types.ReverseAuction: - a, err = k.PlaceBidReverse(ctx, auc, bidder, newAmount) - if err != nil { + if updatedAuction, err = k.PlaceBidReverse(ctx, a, bidder, newAmount); err != nil { return err } case types.ForwardReverseAuction: - if !auc.IsReversePhase() { - a, err = k.PlaceBidForwardReverseForward(ctx, auc, bidder, newAmount) + if !a.IsReversePhase() { + updatedAuction, err = k.PlaceBidForwardReverseForward(ctx, a, bidder, newAmount) } else { - a, err = k.PlaceBidForwardReverseReverse(ctx, auc, bidder, newAmount) + updatedAuction, err = k.PlaceBidForwardReverseReverse(ctx, a, bidder, newAmount) } if err != nil { return err @@ -109,12 +107,12 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddres } // store updated auction - k.SetAuction(ctx, a) + k.SetAuction(ctx, updatedAuction) return nil } func (k Keeper) PlaceBidForward(ctx sdk.Context, a types.ForwardAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.ForwardAuction, sdk.Error) { - // Valid New Bid + // Validate New Bid if bid.Denom != a.Bid.Denom { return a, sdk.ErrInternal("bid denom doesn't match auction") } @@ -155,7 +153,7 @@ func (k Keeper) PlaceBidForward(ctx sdk.Context, a types.ForwardAuction, bidder // TODO naming func (k Keeper) PlaceBidForwardReverseForward(ctx sdk.Context, a types.ForwardReverseAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.ForwardReverseAuction, sdk.Error) { - // Validate bid + // Validate new bid if bid.Denom != a.Bid.Denom { return a, sdk.ErrInternal("bid denom doesn't match auction") } @@ -289,27 +287,28 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error { } // payout to the last bidder - var err sdk.Error switch auc := auction.(type) { - case types.ForwardAuction, types.ForwardReverseAuction: - err = k.PayoutAuctionLot(ctx, auc) - if err != nil { + case types.ForwardAuction: + if err := k.PayoutForwardAuction(ctx, auc); err != nil { return err } case types.ReverseAuction: - err = k.MintAndPayoutAuctionLot(ctx, auc) - if err != nil { + if err := k.PayoutReverseAuction(ctx, auc); err != nil { + return err + } + case types.ForwardReverseAuction: + if err := k.PayoutForwardReverseAuction(ctx, auc); err != nil { return err } default: panic("unrecognized auction type") } - // Delete auction from store (and queue) k.DeleteAuction(ctx, auctionID) return nil } -func (k Keeper) MintAndPayoutAuctionLot(ctx sdk.Context, a types.ReverseAuction) sdk.Error { + +func (k Keeper) PayoutReverseAuction(ctx sdk.Context, a types.ReverseAuction) sdk.Error { err := k.supplyKeeper.MintCoins(ctx, a.Initiator, sdk.NewCoins(a.Lot)) if err != nil { return err @@ -320,8 +319,17 @@ func (k Keeper) MintAndPayoutAuctionLot(ctx sdk.Context, a types.ReverseAuction) } return nil } -func (k Keeper) PayoutAuctionLot(ctx sdk.Context, a types.Auction) sdk.Error { - err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.GetBidder(), sdk.NewCoins(a.GetLot())) + +func (k Keeper) PayoutForwardAuction(ctx sdk.Context, a types.ForwardAuction) sdk.Error { + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot)) + if err != nil { + return err + } + return nil +} + +func (k Keeper) PayoutForwardReverseAuction(ctx sdk.Context, a types.ForwardReverseAuction) sdk.Error { + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot)) if err != nil { return err } diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index fa5e1163..53857084 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -12,9 +12,6 @@ import ( type Auction interface { GetID() uint64 WithID(uint64) Auction - GetBidder() sdk.AccAddress - GetBid() sdk.Coin - GetLot() sdk.Coin GetEndTime() time.Time } @@ -32,15 +29,6 @@ type BaseAuction struct { // GetID getter for auction ID func (a BaseAuction) GetID() uint64 { return a.ID } -// GetBid getter for auction bid -func (a BaseAuction) GetBidder() sdk.AccAddress { return a.Bidder } - -// GetBid getter for auction lot -func (a BaseAuction) GetBid() sdk.Coin { return a.Bid } - -// GetLot getter for auction lot -func (a BaseAuction) GetLot() sdk.Coin { return a.Lot } - // GetEndTime getter for auction end time func (a BaseAuction) GetEndTime() time.Time { return a.EndTime } From c239932297b748d3f7fa73facf6cd1b9dc0b8745 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 9 Jan 2020 16:09:19 +0000 Subject: [PATCH 41/99] give auction types more accurate names --- x/auction/abci_test.go | 2 +- x/auction/alias.go | 62 +++++++++++++++---------------- x/auction/keeper/auctions.go | 59 +++++++++++++++-------------- x/auction/keeper/auctions_test.go | 18 ++++----- x/auction/keeper/keeper_test.go | 8 ++-- x/auction/spec/01_concepts.md | 6 +-- x/auction/spec/02_state.md | 12 +++--- x/auction/spec/03_messages.md | 6 +-- x/auction/types/auctions.go | 40 ++++++++++---------- x/auction/types/auctions_test.go | 40 ++++++++++---------- x/auction/types/codec.go | 6 +-- 11 files changed, 129 insertions(+), 130 deletions(-) diff --git a/x/auction/abci_test.go b/x/auction/abci_test.go index 19bc1792..418ca49a 100644 --- a/x/auction/abci_test.go +++ b/x/auction/abci_test.go @@ -37,7 +37,7 @@ func TestKeeper_EndBlocker(t *testing.T) { ctx := tApp.NewContext(true, abci.Header{}) keeper := tApp.GetAuctionKeeper() - auctionID, err := keeper.StartForwardReverseAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights) + auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights) require.NoError(t, err) require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 30))) diff --git a/x/auction/alias.go b/x/auction/alias.go index ec4ad50f..99b63c4f 100644 --- a/x/auction/alias.go +++ b/x/auction/alias.go @@ -22,24 +22,24 @@ const ( var ( // functions aliases - NewForwardAuction = types.NewForwardAuction - NewReverseAuction = types.NewReverseAuction - NewForwardReverseAuction = types.NewForwardReverseAuction - NewWeightedAddresses = types.NewWeightedAddresses - RegisterCodec = types.RegisterCodec - NewGenesisState = types.NewGenesisState - DefaultGenesisState = types.DefaultGenesisState - ValidateGenesis = types.ValidateGenesis - GetAuctionKey = types.GetAuctionKey - GetAuctionByTimeKey = types.GetAuctionByTimeKey - Uint64FromBytes = types.Uint64FromBytes - Uint64ToBytes = types.Uint64ToBytes - NewMsgPlaceBid = types.NewMsgPlaceBid - NewParams = types.NewParams - DefaultParams = types.DefaultParams - ParamKeyTable = types.ParamKeyTable - NewKeeper = keeper.NewKeeper - NewQuerier = keeper.NewQuerier + NewSurplusAuction = types.NewSurplusAuction + NewDebtAuction = types.NewDebtAuction + NewCollateralAuction = types.NewCollateralAuction + NewWeightedAddresses = types.NewWeightedAddresses + RegisterCodec = types.RegisterCodec + NewGenesisState = types.NewGenesisState + DefaultGenesisState = types.DefaultGenesisState + ValidateGenesis = types.ValidateGenesis + GetAuctionKey = types.GetAuctionKey + GetAuctionByTimeKey = types.GetAuctionByTimeKey + Uint64FromBytes = types.Uint64FromBytes + Uint64ToBytes = types.Uint64ToBytes + NewMsgPlaceBid = types.NewMsgPlaceBid + NewParams = types.NewParams + DefaultParams = types.DefaultParams + ParamKeyTable = types.ParamKeyTable + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier // variable aliases ModuleCdc = types.ModuleCdc @@ -51,17 +51,17 @@ var ( ) type ( - Auction = types.Auction - BaseAuction = types.BaseAuction - ForwardAuction = types.ForwardAuction - ReverseAuction = types.ReverseAuction - ForwardReverseAuction = types.ForwardReverseAuction - WeightedAddresses = types.WeightedAddresses - SupplyKeeper = types.SupplyKeeper - Auctions = types.Auctions - GenesisState = types.GenesisState - MsgPlaceBid = types.MsgPlaceBid - Params = types.Params - QueryResAuctions = types.QueryResAuctions - Keeper = keeper.Keeper + Auction = types.Auction + BaseAuction = types.BaseAuction + SurplusAuction = types.SurplusAuction + DebtAuction = types.DebtAuction + CollateralAuction = types.CollateralAuction + WeightedAddresses = types.WeightedAddresses + SupplyKeeper = types.SupplyKeeper + Auctions = types.Auctions + GenesisState = types.GenesisState + MsgPlaceBid = types.MsgPlaceBid + Params = types.Params + QueryResAuctions = types.QueryResAuctions + Keeper = keeper.Keeper ) diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index 51e05a43..debac160 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -9,10 +9,10 @@ import ( "github.com/kava-labs/kava/x/auction/types" ) -// StartForwardAuction starts a normal auction that mints the sold coins. -func (k Keeper) StartForwardAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (uint64, sdk.Error) { +// StartSurplusAuction starts a normal auction that mints the sold coins. +func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (uint64, sdk.Error) { // create auction - auction := types.NewForwardAuction(seller, lot, bidDenom, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) + auction := types.NewSurplusAuction(seller, lot, bidDenom, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) // take coins from module account err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) @@ -27,10 +27,10 @@ func (k Keeper) StartForwardAuction(ctx sdk.Context, seller string, lot sdk.Coin return auctionID, nil } -// StartReverseAuction starts an auction where sellers compete by offering decreasing prices. -func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin) (uint64, sdk.Error) { +// StartDebtAuction starts an auction where sellers compete by offering decreasing prices. +func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin) (uint64, sdk.Error) { // create auction - auction := types.NewReverseAuction(buyer, bid, initialLot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) + auction := types.NewDebtAuction(buyer, bid, initialLot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) // This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker. macc := k.supplyKeeper.GetModuleAccount(ctx, buyer) @@ -45,14 +45,14 @@ func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer string, bid sdk.Coin, return auctionID, nil } -// StartForwardReverseAuction starts an auction where bidders bid up to a maxBid, then switch to bidding down on price. -func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int) (uint64, sdk.Error) { +// StartCollateralAuction starts an auction where bidders bid up to a maxBid, then switch to bidding down on price. +func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int) (uint64, sdk.Error) { // create auction weightedAddresses, err := types.NewWeightedAddresses(lotReturnAddrs, lotReturnWeights) if err != nil { return 0, err } - auction := types.NewForwardReverseAuction(seller, lot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), maxBid, weightedAddresses) + auction := types.NewCollateralAuction(seller, lot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), maxBid, weightedAddresses) // take coins from module account err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) @@ -85,19 +85,19 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddres var err sdk.Error var updatedAuction types.Auction switch a := auction.(type) { - case types.ForwardAuction: - if updatedAuction, err = k.PlaceBidForward(ctx, a, bidder, newAmount); err != nil { + case types.SurplusAuction: + if updatedAuction, err = k.PlaceBidSurplus(ctx, a, bidder, newAmount); err != nil { return err } - case types.ReverseAuction: - if updatedAuction, err = k.PlaceBidReverse(ctx, a, bidder, newAmount); err != nil { + case types.DebtAuction: + if updatedAuction, err = k.PlaceBidDebt(ctx, a, bidder, newAmount); err != nil { return err } - case types.ForwardReverseAuction: + case types.CollateralAuction: if !a.IsReversePhase() { - updatedAuction, err = k.PlaceBidForwardReverseForward(ctx, a, bidder, newAmount) + updatedAuction, err = k.PlaceForwardBidCollateral(ctx, a, bidder, newAmount) } else { - updatedAuction, err = k.PlaceBidForwardReverseReverse(ctx, a, bidder, newAmount) + updatedAuction, err = k.PlaceReverseBidCollateral(ctx, a, bidder, newAmount) } if err != nil { return err @@ -111,7 +111,7 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddres return nil } -func (k Keeper) PlaceBidForward(ctx sdk.Context, a types.ForwardAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.ForwardAuction, sdk.Error) { +func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.SurplusAuction, sdk.Error) { // Validate New Bid if bid.Denom != a.Bid.Denom { return a, sdk.ErrInternal("bid denom doesn't match auction") @@ -151,8 +151,7 @@ func (k Keeper) PlaceBidForward(ctx sdk.Context, a types.ForwardAuction, bidder return a, nil } -// TODO naming -func (k Keeper) PlaceBidForwardReverseForward(ctx sdk.Context, a types.ForwardReverseAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.ForwardReverseAuction, sdk.Error) { +func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.CollateralAuction, sdk.Error) { // Validate new bid if bid.Denom != a.Bid.Denom { return a, sdk.ErrInternal("bid denom doesn't match auction") @@ -193,7 +192,7 @@ func (k Keeper) PlaceBidForwardReverseForward(ctx sdk.Context, a types.ForwardRe return a, nil } -func (k Keeper) PlaceBidForwardReverseReverse(ctx sdk.Context, a types.ForwardReverseAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.ForwardReverseAuction, sdk.Error) { +func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.CollateralAuction, sdk.Error) { // Validate bid if lot.Denom != a.Lot.Denom { return a, sdk.ErrInternal("lot denom doesn't match auction") @@ -240,7 +239,7 @@ func (k Keeper) PlaceBidForwardReverseReverse(ctx sdk.Context, a types.ForwardRe return a, nil } -func (k Keeper) PlaceBidReverse(ctx sdk.Context, a types.ReverseAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.ReverseAuction, sdk.Error) { +func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.DebtAuction, sdk.Error) { // Validate New Bid if lot.Denom != a.Lot.Denom { return a, sdk.ErrInternal("lot denom doesn't match auction") @@ -288,16 +287,16 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error { // payout to the last bidder switch auc := auction.(type) { - case types.ForwardAuction: - if err := k.PayoutForwardAuction(ctx, auc); err != nil { + case types.SurplusAuction: + if err := k.PayoutSurplusAuction(ctx, auc); err != nil { return err } - case types.ReverseAuction: - if err := k.PayoutReverseAuction(ctx, auc); err != nil { + case types.DebtAuction: + if err := k.PayoutDebtAuction(ctx, auc); err != nil { return err } - case types.ForwardReverseAuction: - if err := k.PayoutForwardReverseAuction(ctx, auc); err != nil { + case types.CollateralAuction: + if err := k.PayoutCollateralAuction(ctx, auc); err != nil { return err } default: @@ -308,7 +307,7 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error { return nil } -func (k Keeper) PayoutReverseAuction(ctx sdk.Context, a types.ReverseAuction) sdk.Error { +func (k Keeper) PayoutDebtAuction(ctx sdk.Context, a types.DebtAuction) sdk.Error { err := k.supplyKeeper.MintCoins(ctx, a.Initiator, sdk.NewCoins(a.Lot)) if err != nil { return err @@ -320,7 +319,7 @@ func (k Keeper) PayoutReverseAuction(ctx sdk.Context, a types.ReverseAuction) sd return nil } -func (k Keeper) PayoutForwardAuction(ctx sdk.Context, a types.ForwardAuction) sdk.Error { +func (k Keeper) PayoutSurplusAuction(ctx sdk.Context, a types.SurplusAuction) sdk.Error { err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot)) if err != nil { return err @@ -328,7 +327,7 @@ func (k Keeper) PayoutForwardAuction(ctx sdk.Context, a types.ForwardAuction) sd return nil } -func (k Keeper) PayoutForwardReverseAuction(ctx sdk.Context, a types.ForwardReverseAuction) sdk.Error { +func (k Keeper) PayoutCollateralAuction(ctx sdk.Context, a types.CollateralAuction) sdk.Error { err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot)) if err != nil { return err diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index 7d7f3073..6e4ffeb4 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -16,7 +16,7 @@ import ( "github.com/kava-labs/kava/x/liquidator" ) -func TestForwardAuctionBasic(t *testing.T) { +func TestSurplusAuctionBasic(t *testing.T) { // Setup _, addrs := app.GeneratePrivKeyAddressPairs(1) buyer := addrs[0] @@ -37,7 +37,7 @@ func TestForwardAuctionBasic(t *testing.T) { keeper := tApp.GetAuctionKeeper() // Create an auction (lot: 20 token1, initialBid: 0 token2) - auctionID, err := keeper.StartForwardAuction(ctx, sellerModName, c("token1", 20), "token2") // lot, bid denom + auctionID, err := keeper.StartSurplusAuction(ctx, sellerModName, c("token1", 20), "token2") // lot, bid denom require.NoError(t, err) // Check seller's coins have decreased tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) @@ -60,7 +60,7 @@ func TestForwardAuctionBasic(t *testing.T) { tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 80))) } -func TestReverseAuctionBasic(t *testing.T) { +func TestDebtAuctionBasic(t *testing.T) { // Setup _, addrs := app.GeneratePrivKeyAddressPairs(1) seller := addrs[0] @@ -79,7 +79,7 @@ func TestReverseAuctionBasic(t *testing.T) { keeper := tApp.GetAuctionKeeper() // Start auction - auctionID, err := keeper.StartReverseAuction(ctx, buyerModName, c("token1", 20), c("token2", 99999)) // buyer, bid, initialLot + auctionID, err := keeper.StartDebtAuction(ctx, buyerModName, c("token1", 20), c("token2", 99999)) // buyer, bid, initialLot require.NoError(t, err) // Check buyer's coins have not decreased, as lot is minted at the end tApp.CheckBalance(t, ctx, buyerAddr, nil) // zero coins @@ -98,7 +98,7 @@ func TestReverseAuctionBasic(t *testing.T) { tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) } -func TestForwardReverseAuctionBasic(t *testing.T) { +func TestCollateralAuctionBasic(t *testing.T) { // Setup _, addrs := app.GeneratePrivKeyAddressPairs(4) buyer := addrs[0] @@ -123,7 +123,7 @@ func TestForwardReverseAuctionBasic(t *testing.T) { keeper := tApp.GetAuctionKeeper() // Start auction - auctionID, err := keeper.StartForwardReverseAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights) // seller, lot, maxBid, otherPerson + auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights) // seller, lot, maxBid, otherPerson require.NoError(t, err) // Check seller's coins have decreased tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) @@ -158,7 +158,7 @@ func TestForwardReverseAuctionBasic(t *testing.T) { tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 115), c("token2", 50))) } -func TestStartForwardAuction(t *testing.T) { +func TestStartSurplusAuction(t *testing.T) { someTime := time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC) type args struct { seller string @@ -211,7 +211,7 @@ func TestStartForwardAuction(t *testing.T) { keeper := tApp.GetAuctionKeeper() // run function under test - id, err := keeper.StartForwardAuction(ctx, tc.args.seller, tc.args.lot, tc.args.bidDenom) + id, err := keeper.StartSurplusAuction(ctx, tc.args.seller, tc.args.lot, tc.args.bidDenom) // check sk := tApp.GetSupplyKeeper() @@ -224,7 +224,7 @@ func TestStartForwardAuction(t *testing.T) { require.Equal(t, initialLiquidatorCoins.Sub(cs(tc.args.lot)), liquidatorCoins) // check auction in store and is correct require.True(t, found) - expectedAuction := types.Auction(types.ForwardAuction{BaseAuction: types.BaseAuction{ + expectedAuction := types.Auction(types.SurplusAuction{BaseAuction: types.BaseAuction{ ID: 0, Initiator: tc.args.seller, Lot: tc.args.lot, diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go index cc6fbce1..3c59d9ae 100644 --- a/x/auction/keeper/keeper_test.go +++ b/x/auction/keeper/keeper_test.go @@ -18,7 +18,7 @@ func SetGetDeleteAuction(t *testing.T) { ctx := tApp.NewContext(true, abci.Header{}) someTime := time.Date(43, time.January, 1, 0, 0, 0, 0, time.UTC) // need to specify UTC as tz info is lost on unmarshal var id uint64 = 5 - auction := types.NewForwardAuction("some_module", c("usdx", 100), "kava", someTime).WithID(id) + auction := types.NewSurplusAuction("some_module", c("usdx", 100), "kava", someTime).WithID(id) // write and read from store keeper.SetAuction(ctx, auction) @@ -73,9 +73,9 @@ func TestIterateAuctions(t *testing.T) { ctx := tApp.NewContext(true, abci.Header{}) auctions := []types.Auction{ - types.NewForwardAuction("sellerMod", c("denom", 12345678), "anotherdenom", time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)).WithID(0), - types.NewReverseAuction("buyerMod", c("denom", 12345678), c("anotherdenom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)).WithID(1), - types.NewForwardReverseAuction("sellerMod", c("denom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC), c("anotherdenom", 12345678), types.WeightedAddresses{}).WithID(2), + types.NewSurplusAuction("sellerMod", c("denom", 12345678), "anotherdenom", time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)).WithID(0), + types.NewDebtAuction("buyerMod", c("denom", 12345678), c("anotherdenom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)).WithID(1), + types.NewCollateralAuction("sellerMod", c("denom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC), c("anotherdenom", 12345678), types.WeightedAddresses{}).WithID(2), } for _, a := range auctions { keeper.SetAuction(ctx, a) diff --git a/x/auction/spec/01_concepts.md b/x/auction/spec/01_concepts.md index f656b615..5a4196ac 100644 --- a/x/auction/spec/01_concepts.md +++ b/x/auction/spec/01_concepts.md @@ -6,8 +6,8 @@ order: 1 Auctions are broken down into three distinct types, which correspond to three specific functionalities within the CDP system. -* **Forward Auction:** An auction in which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 they are willing to pay for the lot of c1. After the completion of a forward auction, the winning bid of c2 is burned, and the bidder receives the lot of c1. As a concrete example, forward auction are used to sell a fixed amount of USDX stable coins in exchange for increasing bids of KAVA governance tokens. The governance tokens are then burned and the winner receives USDX. -* **Reverse Auction:** An auction in which a fixed amount of coins (c1) is bid for a decreasing lot of other coins (c2). Bidders decrement the lot of c2 they are willing to receive for the fixed amount of c1. As a concrete example, reverse auctions are used to raise a certain amount of USDX stable coins in exchange for decreasing lots of KAVA governance tokens. The USDX tokens are used to recapitalize the cdp system and the winner receives KAVA. -* **Forward Reverse Auction:** An two phase auction is which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 until a specific `maxBid` is reached. Once `maxBid` is reached, a fixed amount of c2 is bid for a decreasing lot of c1. In the second phase, bidders decrement the lot of c1 they are willing to receive for a fixed amount of c2. As a concrete example. forward reverse auctions are used to sell collateral (ATOM, for example) for up to a `maxBid` amount of USDX. The USDX tokens are used to recapitalize the cdp system and the winner receives the specified lot of ATOM. In the event that the winning lot is smaller than the total lot, the excess ATOM is ratably returned to the original owners of the liquidated CDPs that were collateralized with that ATOM. +* **Surplus Auction:** An auction in which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 they are willing to pay for the lot of c1. After the completion of a forward auction, the winning bid of c2 is burned, and the bidder receives the lot of c1. As a concrete example, forward auction are used to sell a fixed amount of USDX stable coins in exchange for increasing bids of KAVA governance tokens. The governance tokens are then burned and the winner receives USDX. +* **Debt Auction:** An auction in which a fixed amount of coins (c1) is bid for a decreasing lot of other coins (c2). Bidders decrement the lot of c2 they are willing to receive for the fixed amount of c1. As a concrete example, reverse auctions are used to raise a certain amount of USDX stable coins in exchange for decreasing lots of KAVA governance tokens. The USDX tokens are used to recapitalize the cdp system and the winner receives KAVA. +* **Surplus Reverse Auction:** An two phase auction is which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 until a specific `maxBid` is reached. Once `maxBid` is reached, a fixed amount of c2 is bid for a decreasing lot of c1. In the second phase, bidders decrement the lot of c1 they are willing to receive for a fixed amount of c2. As a concrete example. forward reverse auctions are used to sell collateral (ATOM, for example) for up to a `maxBid` amount of USDX. The USDX tokens are used to recapitalize the cdp system and the winner receives the specified lot of ATOM. In the event that the winning lot is smaller than the total lot, the excess ATOM is ratably returned to the original owners of the liquidated CDPs that were collateralized with that ATOM. Auctions are always initiated by another module, and not directly by users. Auctions start with an expiry, the time at which the auction is guaranteed to end, even if there have been no bidders. After each bid, the auction is extended by a specific amount of time, `BidDuration`. In the case that increasing the auction time by `BidDuration` would cause the auction to go past its expiry, the expiry is chosen as the ending time. diff --git a/x/auction/spec/02_state.md b/x/auction/spec/02_state.md index a1c39f84..e24f1c02 100644 --- a/x/auction/spec/02_state.md +++ b/x/auction/spec/02_state.md @@ -51,13 +51,13 @@ type BaseAuction struct { MaxEndTime time.Time // Maximum closing time. Auctions can close before this but never after. } -// ForwardAuction type for forward auctions -type ForwardAuction struct { +//SurplusAuction type for forward auctions +typeSurplusAuction struct { BaseAuction } -// ReverseAuction type for reverse auctions -type ReverseAuction struct { +// DebtAuction type for reverse auctions +type DebtAuction struct { BaseAuction } @@ -67,8 +67,8 @@ type WeightedAddresses struct { Weights []sdk.Int } -// ForwardReverseAuction type for forward reverse auction -type ForwardReverseAuction struct { +// CollateralAuction type for forward reverse auction +type CollateralAuction struct { BaseAuction MaxBid sdk.Coin LotReturns WeightedAddresses // return addresses to pay out reductions in the lot amount to. Lot is bid down during reverse phase. diff --git a/x/auction/spec/03_messages.md b/x/auction/spec/03_messages.md index 5c8ece91..6b59d9c2 100644 --- a/x/auction/spec/03_messages.md +++ b/x/auction/spec/03_messages.md @@ -21,14 +21,14 @@ type MsgPlaceBid struct { **State Modifications:** * Update bidder if different than previous bidder -* For forward auctions: +* For Surplus auctions: * Update Bid Amount * Return bid coins to previous bidder * Burn coins equal to the increment in the bid (CurrentBid - PreviousBid) -* For Reverse auctions: +* For Debt auctions: * Update lot amount * Return bid coins to previous bidder -* For Forward Reverse auctions: +* For Collateral auctions: * Return bid coins to previous bidder * If in forward phase: * Update bid amount diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 53857084..ec685006 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -46,17 +46,17 @@ func (a BaseAuction) String() string { ) } -// ForwardAuction type for forward auctions -type ForwardAuction struct { +// SurplusAuction type for forward auctions +type SurplusAuction struct { BaseAuction } // WithID returns an auction with the ID set -func (a ForwardAuction) WithID(id uint64) Auction { a.ID = id; return a } +func (a SurplusAuction) WithID(id uint64) Auction { a.ID = id; return a } -// NewForwardAuction creates a new forward auction -func NewForwardAuction(seller string, lot sdk.Coin, bidDenom string, endTime time.Time) ForwardAuction { - auction := ForwardAuction{BaseAuction{ +// NewSurplusAuction creates a new forward auction +func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime time.Time) SurplusAuction { + auction := SurplusAuction{BaseAuction{ // no ID Initiator: seller, Lot: lot, @@ -68,20 +68,20 @@ func NewForwardAuction(seller string, lot sdk.Coin, bidDenom string, endTime tim return auction } -// ReverseAuction type for reverse auctions -type ReverseAuction struct { +// DebtAuction type for reverse auctions +type DebtAuction struct { BaseAuction } // WithID returns an auction with the ID set -func (a ReverseAuction) WithID(id uint64) Auction { a.ID = id; return a } +func (a DebtAuction) WithID(id uint64) Auction { a.ID = id; return a } -// NewReverseAuction creates a new reverse auction -func NewReverseAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime time.Time) ReverseAuction { +// NewDebtAuction creates a new reverse auction +func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime time.Time) DebtAuction { // Note: Bidder is set to the initiator's module account address instead of module name. (when the first bid is placed, it is paid out to the initiator) // Setting to the module account address bypasses calling supply.SendCoinsFromModuleToModule, instead calls SendCoinsFromModuleToAccount. // This isn't a problem currently, but if additional logic/validation was added for sending to coins to Module Accounts, it would be bypassed. - auction := ReverseAuction{BaseAuction{ + auction := DebtAuction{BaseAuction{ // no ID Initiator: buyerModAccName, Lot: initialLot, @@ -93,21 +93,21 @@ func NewReverseAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin return auction } -// ForwardReverseAuction type for forward reverse auction -type ForwardReverseAuction struct { +// CollateralAuction type for forward reverse auction +type CollateralAuction struct { BaseAuction MaxBid sdk.Coin LotReturns WeightedAddresses // return addresses to pay out reductions in the lot amount to. Lot is bid down during reverse phase. } // WithID returns an auction with the ID set -func (a ForwardReverseAuction) WithID(id uint64) Auction { a.ID = id; return a } +func (a CollateralAuction) WithID(id uint64) Auction { a.ID = id; return a } -func (a ForwardReverseAuction) IsReversePhase() bool { +func (a CollateralAuction) IsReversePhase() bool { return a.Bid.IsEqual(a.MaxBid) } -func (a ForwardReverseAuction) String() string { +func (a CollateralAuction) String() string { return fmt.Sprintf(`Auction %d: Initiator: %s Lot: %s @@ -123,9 +123,9 @@ func (a ForwardReverseAuction) String() string { ) } -// NewForwardReverseAuction creates a new forward reverse auction -func NewForwardReverseAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses) ForwardReverseAuction { - auction := ForwardReverseAuction{ +// NewCollateralAuction creates a new forward reverse auction +func NewCollateralAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses) CollateralAuction { + auction := CollateralAuction{ BaseAuction: BaseAuction{ // no ID Initiator: seller, diff --git a/x/auction/types/auctions_test.go b/x/auction/types/auctions_test.go index bd0db597..a4d059d6 100644 --- a/x/auction/types/auctions_test.go +++ b/x/auction/types/auctions_test.go @@ -2,7 +2,7 @@ package types // // 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) { +// func TesSurplusAuction_PlaceBid(t *testing.T) { // seller := sdk.AccAddress([]byte("a_seller")) // buyer1 := sdk.AccAddress([]byte("buyer1")) // buyer2 := sdk.AccAddress([]byte("buyer2")) @@ -17,7 +17,7 @@ package types // } // tests := []struct { // name string -// auction ForwardAuction +// auction SurplusAuction // args args // expectedOutputs []BankOutput // expectedInputs []BankInput @@ -28,7 +28,7 @@ package types // }{ // { // "normal", -// ForwardAuction{BaseAuction{ +// SurplusAuction{BaseAuction{ // Initiator: seller, // Lot: c("usdx", 100), // Bidder: buyer1, @@ -46,7 +46,7 @@ package types // }, // { // "lowBid", -// ForwardAuction{BaseAuction{ +// SurplusAuction{BaseAuction{ // Initiator: seller, // Lot: c("usdx", 100), // Bidder: buyer1, @@ -64,7 +64,7 @@ package types // }, // { // "equalBid", -// ForwardAuction{BaseAuction{ +// SurplusAuction{BaseAuction{ // Initiator: seller, // Lot: c("usdx", 100), // Bidder: buyer1, @@ -82,7 +82,7 @@ package types // }, // { // "timeout", -// ForwardAuction{BaseAuction{ +// SurplusAuction{BaseAuction{ // Initiator: seller, // Lot: c("usdx", 100), // Bidder: buyer1, @@ -100,7 +100,7 @@ package types // }, // { // "hitMaxEndTime", -// ForwardAuction{BaseAuction{ +// SurplusAuction{BaseAuction{ // Initiator: seller, // Lot: c("usdx", 100), // Bidder: buyer1, @@ -139,7 +139,7 @@ package types // } // } -// func TestReverseAuction_PlaceBid(t *testing.T) { +// func TestDebtAuction_PlaceBid(t *testing.T) { // buyer := sdk.AccAddress([]byte("a_buyer")) // seller1 := sdk.AccAddress([]byte("seller1")) // seller2 := sdk.AccAddress([]byte("seller2")) @@ -154,7 +154,7 @@ package types // } // tests := []struct { // name string -// auction ReverseAuction +// auction DebtAuction // args args // expectedOutputs []BankOutput // expectedInputs []BankInput @@ -165,7 +165,7 @@ package types // }{ // { // "normal", -// ReverseAuction{BaseAuction{ +// DebtAuction{BaseAuction{ // Initiator: buyer, // Lot: c("kava", 10), // Bidder: seller1, @@ -183,7 +183,7 @@ package types // }, // { // "highBid", -// ReverseAuction{BaseAuction{ +// DebtAuction{BaseAuction{ // Initiator: buyer, // Lot: c("kava", 10), // Bidder: seller1, @@ -201,7 +201,7 @@ package types // }, // { // "equalBid", -// ReverseAuction{BaseAuction{ +// DebtAuction{BaseAuction{ // Initiator: buyer, // Lot: c("kava", 10), // Bidder: seller1, @@ -219,7 +219,7 @@ package types // }, // { // "timeout", -// ReverseAuction{BaseAuction{ +// DebtAuction{BaseAuction{ // Initiator: buyer, // Lot: c("kava", 10), // Bidder: seller1, @@ -237,7 +237,7 @@ package types // }, // { // "hitMaxEndTime", -// ReverseAuction{BaseAuction{ +// DebtAuction{BaseAuction{ // Initiator: buyer, // Lot: c("kava", 10), // Bidder: seller1, @@ -276,7 +276,7 @@ package types // } // } -// func TestForwardReverseAuction_PlaceBid(t *testing.T) { +// func TestCollateralAuction_PlaceBid(t *testing.T) { // cdpOwner := sdk.AccAddress([]byte("a_cdp_owner")) // seller := sdk.AccAddress([]byte("a_seller")) // buyer1 := sdk.AccAddress([]byte("buyer1")) @@ -292,7 +292,7 @@ package types // } // tests := []struct { // name string -// auction ForwardReverseAuction +// auction CollateralAuction // args args // expectedOutputs []BankOutput // expectedInputs []BankInput @@ -304,7 +304,7 @@ package types // }{ // { // "normalForwardBid", -// ForwardReverseAuction{BaseAuction: BaseAuction{ +// CollateralAuction{BaseAuction: BaseAuction{ // Initiator: seller, // Lot: c("xrp", 100), // Bidder: buyer1, @@ -325,7 +325,7 @@ package types // }, // { // "normalSwitchOverBid", -// ForwardReverseAuction{BaseAuction: BaseAuction{ +// CollateralAuction{BaseAuction: BaseAuction{ // Initiator: seller, // Lot: c("xrp", 100), // Bidder: buyer1, @@ -345,8 +345,8 @@ package types // true, // }, // { -// "normalReverseBid", -// ForwardReverseAuction{BaseAuction: BaseAuction{ +// "normalDebtBid", +// CollateralAuction{BaseAuction: BaseAuction{ // Initiator: seller, // Lot: c("xrp", 99), // Bidder: buyer1, diff --git a/x/auction/types/codec.go b/x/auction/types/codec.go index 75ede175..12f8f9eb 100644 --- a/x/auction/types/codec.go +++ b/x/auction/types/codec.go @@ -17,7 +17,7 @@ func RegisterCodec(cdc *codec.Codec) { // 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) + cdc.RegisterConcrete(SurplusAuction{}, "auction/SurplusAuction", nil) + cdc.RegisterConcrete(DebtAuction{}, "auction/DebtAuction", nil) + cdc.RegisterConcrete(CollateralAuction{}, "auction/CollateralAuction", nil) } From 08d6bc2284503ad7bbda90a63b79321d4b6999d9 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 9 Jan 2020 16:14:55 +0000 Subject: [PATCH 42/99] remove vuepress comments from spec --- x/auction/spec/01_concepts.md | 4 ---- x/auction/spec/02_state.md | 4 ---- x/auction/spec/03_messages.md | 4 ---- x/auction/spec/04_events.md | 4 ---- x/auction/spec/05_params.md | 4 ---- x/auction/spec/{06_begin_block.md => 06_end_block.md} | 6 ++---- x/auction/spec/README.md | 7 ------- 7 files changed, 2 insertions(+), 31 deletions(-) rename x/auction/spec/{06_begin_block.md => 06_end_block.md} (67%) diff --git a/x/auction/spec/01_concepts.md b/x/auction/spec/01_concepts.md index 5a4196ac..08943870 100644 --- a/x/auction/spec/01_concepts.md +++ b/x/auction/spec/01_concepts.md @@ -1,7 +1,3 @@ - - # Concepts Auctions are broken down into three distinct types, which correspond to three specific functionalities within the CDP system. diff --git a/x/auction/spec/02_state.md b/x/auction/spec/02_state.md index e24f1c02..fe383ed6 100644 --- a/x/auction/spec/02_state.md +++ b/x/auction/spec/02_state.md @@ -1,7 +1,3 @@ - - # State ## Parameters and genesis state diff --git a/x/auction/spec/03_messages.md b/x/auction/spec/03_messages.md index 6b59d9c2..b97d9745 100644 --- a/x/auction/spec/03_messages.md +++ b/x/auction/spec/03_messages.md @@ -1,7 +1,3 @@ - - # Messages ## Bidding diff --git a/x/auction/spec/04_events.md b/x/auction/spec/04_events.md index 3bc15051..0c6b3238 100644 --- a/x/auction/spec/04_events.md +++ b/x/auction/spec/04_events.md @@ -1,7 +1,3 @@ - - # Events - # Parameters The auction module contains the following parameters: diff --git a/x/auction/spec/06_begin_block.md b/x/auction/spec/06_end_block.md similarity index 67% rename from x/auction/spec/06_begin_block.md rename to x/auction/spec/06_end_block.md index 2ecfcd54..43694df9 100644 --- a/x/auction/spec/06_begin_block.md +++ b/x/auction/spec/06_end_block.md @@ -1,8 +1,6 @@ - +# End Block -At the beginning of each block, auctions that have reached `EndTime` are closed. The logic to close auctions is as follows: +At the end of each block, auctions that have reached `EndTime` are closed. The logic to close auctions is as follows: ```go var expiredAuctions []uint64 diff --git a/x/auction/spec/README.md b/x/auction/spec/README.md index c68ab587..32ae71d1 100644 --- a/x/auction/spec/README.md +++ b/x/auction/spec/README.md @@ -1,10 +1,3 @@ - - # `auction` From fecfee5077cd4032a2b8cdd485265c559f526d92 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 9 Jan 2020 16:21:42 +0000 Subject: [PATCH 43/99] minor spec updates --- x/auction/spec/01_concepts.md | 6 +++--- x/auction/spec/02_state.md | 7 ++----- x/auction/spec/03_messages.md | 13 ++++++------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/x/auction/spec/01_concepts.md b/x/auction/spec/01_concepts.md index 08943870..a7365d0c 100644 --- a/x/auction/spec/01_concepts.md +++ b/x/auction/spec/01_concepts.md @@ -2,8 +2,8 @@ Auctions are broken down into three distinct types, which correspond to three specific functionalities within the CDP system. -* **Surplus Auction:** An auction in which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 they are willing to pay for the lot of c1. After the completion of a forward auction, the winning bid of c2 is burned, and the bidder receives the lot of c1. As a concrete example, forward auction are used to sell a fixed amount of USDX stable coins in exchange for increasing bids of KAVA governance tokens. The governance tokens are then burned and the winner receives USDX. -* **Debt Auction:** An auction in which a fixed amount of coins (c1) is bid for a decreasing lot of other coins (c2). Bidders decrement the lot of c2 they are willing to receive for the fixed amount of c1. As a concrete example, reverse auctions are used to raise a certain amount of USDX stable coins in exchange for decreasing lots of KAVA governance tokens. The USDX tokens are used to recapitalize the cdp system and the winner receives KAVA. -* **Surplus Reverse Auction:** An two phase auction is which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 until a specific `maxBid` is reached. Once `maxBid` is reached, a fixed amount of c2 is bid for a decreasing lot of c1. In the second phase, bidders decrement the lot of c1 they are willing to receive for a fixed amount of c2. As a concrete example. forward reverse auctions are used to sell collateral (ATOM, for example) for up to a `maxBid` amount of USDX. The USDX tokens are used to recapitalize the cdp system and the winner receives the specified lot of ATOM. In the event that the winning lot is smaller than the total lot, the excess ATOM is ratably returned to the original owners of the liquidated CDPs that were collateralized with that ATOM. +* **Surplus Auction:** An auction in which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 they are willing to pay for the lot of c1. After the completion of a surplus auction, the winning bid of c2 is burned, and the bidder receives the lot of c1. As a concrete example, surplus auction are used to sell a fixed amount of USDX stable coins in exchange for increasing bids of KAVA governance tokens. The governance tokens are then burned and the winner receives USDX. +* **Debt Auction:** An auction in which a fixed amount of coins (c1) is bid for a decreasing lot of other coins (c2). Bidders decrement the lot of c2 they are willing to receive for the fixed amount of c1. As a concrete example, debt auctions are used to raise a certain amount of USDX stable coins in exchange for decreasing lots of KAVA governance tokens. The USDX tokens are used to recapitalize the cdp system and the winner receives KAVA. +* **Surplus Reverse Auction:** Are two phase auction is which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 until a specific `maxBid` is reached. Once `maxBid` is reached, a fixed amount of c2 is bid for a decreasing lot of c1. In the second phase, bidders decrement the lot of c1 they are willing to receive for a fixed amount of c2. As a concrete example, collateral auctions are used to sell collateral (ATOM, for example) for up to a `maxBid` amount of USDX. The USDX tokens are used to recapitalize the cdp system and the winner receives the specified lot of ATOM. In the event that the winning lot is smaller than the total lot, the excess ATOM is ratably returned to the original owners of the liquidated CDPs that were collateralized with that ATOM. Auctions are always initiated by another module, and not directly by users. Auctions start with an expiry, the time at which the auction is guaranteed to end, even if there have been no bidders. After each bid, the auction is extended by a specific amount of time, `BidDuration`. In the case that increasing the auction time by `BidDuration` would cause the auction to go past its expiry, the expiry is chosen as the ending time. diff --git a/x/auction/spec/02_state.md b/x/auction/spec/02_state.md index fe383ed6..075cf08c 100644 --- a/x/auction/spec/02_state.md +++ b/x/auction/spec/02_state.md @@ -30,9 +30,6 @@ type GenesisState struct { type Auction interface { GetID() uint64 WithID(uint64) Auction - GetBidder() sdk.AccAddress - GetBid() sdk.Coin - GetLot() sdk.Coin GetEndTime() time.Time } @@ -47,8 +44,8 @@ type BaseAuction struct { MaxEndTime time.Time // Maximum closing time. Auctions can close before this but never after. } -//SurplusAuction type for forward auctions -typeSurplusAuction struct { +// SurplusAuction type for forward auctions +type SurplusAuction struct { BaseAuction } diff --git a/x/auction/spec/03_messages.md b/x/auction/spec/03_messages.md index b97d9745..3a26a722 100644 --- a/x/auction/spec/03_messages.md +++ b/x/auction/spec/03_messages.md @@ -9,8 +9,7 @@ Users can bid on auctions using the `MsgPlaceBid` message type. All auction type type MsgPlaceBid struct { AuctionID uint64 Bidder sdk.AccAddress - Bid sdk.Coin - Lot sdk.Coin + Amount sdk.Coin } ``` @@ -18,16 +17,16 @@ type MsgPlaceBid struct { * Update bidder if different than previous bidder * For Surplus auctions: - * Update Bid Amount + * Update Bid to msg.Amount * Return bid coins to previous bidder * Burn coins equal to the increment in the bid (CurrentBid - PreviousBid) * For Debt auctions: - * Update lot amount + * Update Lot amount to msg.Amount * Return bid coins to previous bidder * For Collateral auctions: * Return bid coins to previous bidder * If in forward phase: - * Update bid amount + * Update Bid amount to msg.Amount * If in reverse phase: - * Update lot amount -* Extend auction by `BidDuration`, or `MaxEndTime` + * Update Lot amount to msg.Amount +* Extend auction by `BidDuration`, up to `MaxEndTime` From 2537928ee73e7ff5a63cbdc8c9624f9b48f9a381 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Thu, 9 Jan 2020 17:25:16 +0000 Subject: [PATCH 44/99] update doc comments --- x/auction/genesis.go | 2 +- x/auction/keeper/auctions.go | 14 +++++-- x/auction/keeper/keeper.go | 27 ++++++------ x/auction/keeper/params.go | 2 - x/auction/module.go | 15 +++---- x/auction/types/auctions.go | 46 ++++++++++++--------- x/auction/types/codec.go | 1 - x/auction/types/expected_keepers.go | 3 -- x/auction/types/genesis.go | 18 ++++---- x/auction/types/keys.go | 6 ++- x/auction/types/msg.go | 4 +- x/auction/types/params.go | 9 ++-- x/auction/types/{quierier.go => querier.go} | 0 x/auction/types/utils.go | 9 ---- 14 files changed, 75 insertions(+), 81 deletions(-) rename x/auction/types/{quierier.go => querier.go} (100%) delete mode 100644 x/auction/types/utils.go diff --git a/x/auction/genesis.go b/x/auction/genesis.go index 8b4a5f1e..b5fad1ab 100644 --- a/x/auction/genesis.go +++ b/x/auction/genesis.go @@ -4,7 +4,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// InitGenesis - initializes the store state from genesis data +// InitGenesis initializes the store state from genesis data. func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { keeper.SetNextAuctionID(ctx, data.NextAuctionID) diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index debac160..8c92f3ba 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -9,7 +9,7 @@ import ( "github.com/kava-labs/kava/x/auction/types" ) -// StartSurplusAuction starts a normal auction that mints the sold coins. +// StartSurplusAuction starts a new surplus (forward) auction. func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (uint64, sdk.Error) { // create auction auction := types.NewSurplusAuction(seller, lot, bidDenom, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) @@ -27,7 +27,7 @@ func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin return auctionID, nil } -// StartDebtAuction starts an auction where sellers compete by offering decreasing prices. +// StartDebtAuction starts a new debt (reverse) auction. func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin) (uint64, sdk.Error) { // create auction auction := types.NewDebtAuction(buyer, bid, initialLot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) @@ -45,7 +45,7 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in return auctionID, nil } -// StartCollateralAuction starts an auction where bidders bid up to a maxBid, then switch to bidding down on price. +// StartCollateralAuction starts a new collateral (2-phase) auction where bidders bid up to a maxBid, then switch to bidding down on the Lot. func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int) (uint64, sdk.Error) { // create auction weightedAddresses, err := types.NewWeightedAddresses(lotReturnAddrs, lotReturnWeights) @@ -111,6 +111,7 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddres return nil } +// PlaceBidSurplus places a forward bid on a surplus auction, moving coins and returning the updated auction. func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.SurplusAuction, sdk.Error) { // Validate New Bid if bid.Denom != a.Bid.Denom { @@ -151,6 +152,7 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder return a, nil } +// PlaceForwardBidCollateral places a forward bid on a collateral auction, moving coins and returning the updated auction. func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.CollateralAuction, sdk.Error) { // Validate new bid if bid.Denom != a.Bid.Denom { @@ -192,6 +194,7 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc return a, nil } +// PlaceReverseBidCollateral places a reverse bid on a collateral auction, moving coins and returning the updated auction. func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.CollateralAuction, sdk.Error) { // Validate bid if lot.Denom != a.Lot.Denom { @@ -239,6 +242,7 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc return a, nil } +// PlaceBidDebt places a reverse bid on a debt auction, moving coins and returning the updated auction. func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.DebtAuction, sdk.Error) { // Validate New Bid if lot.Denom != a.Lot.Denom { @@ -307,6 +311,7 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error { return nil } +// PayoutDebtAuction pays out the proceeds for a debt auction, first minting the coins. func (k Keeper) PayoutDebtAuction(ctx sdk.Context, a types.DebtAuction) sdk.Error { err := k.supplyKeeper.MintCoins(ctx, a.Initiator, sdk.NewCoins(a.Lot)) if err != nil { @@ -319,6 +324,7 @@ func (k Keeper) PayoutDebtAuction(ctx sdk.Context, a types.DebtAuction) sdk.Erro return nil } +// PayoutSurplusAuction pays out the proceeds for a surplus auction. func (k Keeper) PayoutSurplusAuction(ctx sdk.Context, a types.SurplusAuction) sdk.Error { err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot)) if err != nil { @@ -327,6 +333,7 @@ func (k Keeper) PayoutSurplusAuction(ctx sdk.Context, a types.SurplusAuction) sd return nil } +// PayoutCollateralAuction pays out the proceeds for a collateral auction. func (k Keeper) PayoutCollateralAuction(ctx sdk.Context, a types.CollateralAuction) sdk.Error { err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot)) if err != nil { @@ -344,6 +351,7 @@ func earliestTime(t1, t2 time.Time) time.Time { } } +// splitCoinIntoWeightedBuckets divides up some amount of coins according to some weights. func splitCoinIntoWeightedBuckets(coin sdk.Coin, buckets []sdk.Int) ([]sdk.Coin, sdk.Error) { for _, bucket := range buckets { if bucket.IsNegative() { diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index 28e41540..d756463f 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -47,7 +47,7 @@ func (k Keeper) GetNextAuctionID(ctx sdk.Context) (uint64, sdk.Error) { return types.Uint64FromBytes(bz), nil } -// incrementNextAuctionID increments the global ID in the store by 1 +// IncrementNextAuctionID increments the next auction ID in the store by 1. func (k Keeper) IncrementNextAuctionID(ctx sdk.Context) sdk.Error { id, err := k.GetNextAuctionID(ctx) if err != nil { @@ -74,13 +74,12 @@ func (k Keeper) StoreNewAuction(ctx sdk.Context, auction types.Auction) (uint64, return newAuctionID, nil } -// SetAuction puts the auction into the database and adds it to the queue -// it overwrites any pre-existing auction with same ID +// SetAuction puts the auction into the store, and updates any indexes. func (k Keeper) SetAuction(ctx sdk.Context, auction types.Auction) { // remove the auction from the byTime index if it is already in there existingAuction, found := k.GetAuction(ctx, auction.GetID()) if found { - k.RemoveFromIndex(ctx, existingAuction.GetEndTime(), existingAuction.GetID()) + k.removeFromIndex(ctx, existingAuction.GetEndTime(), existingAuction.GetID()) } // store auction @@ -89,10 +88,10 @@ func (k Keeper) SetAuction(ctx sdk.Context, auction types.Auction) { store.Set(types.GetAuctionKey(auction.GetID()), bz) // add to index - k.InsertIntoIndex(ctx, auction.GetEndTime(), auction.GetID()) + k.insertIntoIndex(ctx, auction.GetEndTime(), auction.GetID()) } -// getAuction gets an auction from the store by auctionID +// GetAuction gets an auction from the store. func (k Keeper) GetAuction(ctx sdk.Context, auctionID uint64) (types.Auction, bool) { var auction types.Auction @@ -106,12 +105,12 @@ func (k Keeper) GetAuction(ctx sdk.Context, auctionID uint64) (types.Auction, bo return auction, true } -// DeleteAuction removes an auction from the store without any validation +// DeleteAuction removes an auction from the store, and any indexes. func (k Keeper) DeleteAuction(ctx sdk.Context, auctionID uint64) { // remove from index auction, found := k.GetAuction(ctx, auctionID) if found { - k.RemoveFromIndex(ctx, auction.GetEndTime(), auctionID) + k.removeFromIndex(ctx, auction.GetEndTime(), auctionID) } // delete auction @@ -119,14 +118,14 @@ func (k Keeper) DeleteAuction(ctx sdk.Context, auctionID uint64) { store.Delete(types.GetAuctionKey(auctionID)) } -// InsertIntoIndex adds an auction ID and end time into the byTime index -func (k Keeper) InsertIntoIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) { +// insertIntoIndex adds an auction ID and end time into the byTime index. +func (k Keeper) insertIntoIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) - store.Set(types.GetAuctionByTimeKey(endTime, auctionID), types.Uint64ToBytes(auctionID)) // TODO + store.Set(types.GetAuctionByTimeKey(endTime, auctionID), types.Uint64ToBytes(auctionID)) } -// RemoveFromIndex removes an auction ID and end time from the byTime index -func (k Keeper) RemoveFromIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) { +// removeFromIndex removes an auction ID and end time from the byTime index. +func (k Keeper) removeFromIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) store.Delete(types.GetAuctionByTimeKey(endTime, auctionID)) } @@ -143,7 +142,7 @@ func (k Keeper) IterateAuctionsByTime(ctx sdk.Context, inclusiveCutoffTime time. defer iterator.Close() for ; iterator.Valid(); iterator.Next() { - // TODO get the auction ID - either read from store, or extract from key + auctionID := types.Uint64FromBytes(iterator.Value()) if cb(auctionID) { diff --git a/x/auction/keeper/params.go b/x/auction/keeper/params.go index bd2d67de..6d7ddfd5 100644 --- a/x/auction/keeper/params.go +++ b/x/auction/keeper/params.go @@ -5,12 +5,10 @@ import ( "github.com/kava-labs/kava/x/auction/types" ) -// SetParams sets the auth module's parameters. func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { k.paramSubspace.SetParamSet(ctx, ¶ms) } -// GetParams gets the auth module's parameters. func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) { k.paramSubspace.GetParamSet(ctx, ¶ms) return diff --git a/x/auction/module.go b/x/auction/module.go index 7e0f1bd7..14c2d023 100644 --- a/x/auction/module.go +++ b/x/auction/module.go @@ -21,20 +21,20 @@ var ( _ module.AppModuleBasic = AppModuleBasic{} ) -// AppModuleBasic app module basics object +// AppModuleBasic implements the sdk.AppModuleBasic interface. type AppModuleBasic struct{} -// Name get module name +// Name returns the module name. func (AppModuleBasic) Name() string { return ModuleName } -// RegisterCodec register module codec +// RegisterCodec registers the module codec. func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { RegisterCodec(cdc) } -// DefaultGenesis default genesis state +// DefaultGenesis returns the default genesis state. func (AppModuleBasic) DefaultGenesis() json.RawMessage { return ModuleCdc.MustMarshalJSON(DefaultGenesisState()) } @@ -64,7 +64,7 @@ func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { return cli.GetQueryCmd(StoreKey, cdc) } -// AppModule app module type +// AppModule implements the sdk.AppModule interface. type AppModule struct { AppModuleBasic keeper Keeper @@ -78,11 +78,6 @@ func NewAppModule(keeper Keeper) AppModule { } } -// Name module name -func (AppModule) Name() string { - return ModuleName -} - // RegisterInvariants performs a no-op. func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index ec685006..9fbd01cd 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -8,28 +8,28 @@ import ( "github.com/cosmos/cosmos-sdk/x/supply" ) -// Auction is an interface to several types of auction. +// Auction is an interface for handling common actions on auctions. type Auction interface { GetID() uint64 WithID(uint64) Auction GetEndTime() time.Time } -// BaseAuction type shared by all Auctions +// BaseAuction is a common type shared by all Auctions. type BaseAuction struct { ID uint64 - Initiator string // Module that starts the auction. Giving away Lot (aka seller in a forward auction). Restricted to being a module account name rather than any account. - 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 time.Time // Auction closing time. Triggers at the end of the block with time ≥ endTime (bids placed in that block are valid) // TODO ensure everything is consistent with this + Initiator string // Module name that starts the auction. Pays out Lot. + Lot sdk.Coin // Coins that will paid out by Initiator to the winning bidder. + Bidder sdk.AccAddress // Latest bidder. Receiver of Lot. + Bid sdk.Coin // Coins paid into the auction the bidder. + EndTime time.Time // Current auction closing time. Triggers at the end of the block with time ≥ EndTime. MaxEndTime time.Time // Maximum closing time. Auctions can close before this but never after. } -// GetID getter for auction ID +// GetID is a getter for auction ID. func (a BaseAuction) GetID() uint64 { return a.ID } -// GetEndTime getter for auction end time +// GetEndTime is a getter for auction end time. func (a BaseAuction) GetEndTime() time.Time { return a.EndTime } func (a BaseAuction) String() string { @@ -46,15 +46,15 @@ func (a BaseAuction) String() string { ) } -// SurplusAuction type for forward auctions +// SurplusAuction is a forward auction that burns what it receives as bids. type SurplusAuction struct { BaseAuction } -// WithID returns an auction with the ID set +// WithID returns an auction with the ID set. func (a SurplusAuction) WithID(id uint64) Auction { a.ID = id; return a } -// NewSurplusAuction creates a new forward auction +// NewSurplusAuction returns a new surplus auction. func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime time.Time) SurplusAuction { auction := SurplusAuction{BaseAuction{ // no ID @@ -68,15 +68,15 @@ func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime tim return auction } -// DebtAuction type for reverse auctions +// DebtAuction is a reverse auction that mints what it pays out. type DebtAuction struct { BaseAuction } -// WithID returns an auction with the ID set +// WithID returns an auction with the ID set. func (a DebtAuction) WithID(id uint64) Auction { a.ID = id; return a } -// NewDebtAuction creates a new reverse auction +// NewDebtAuction returns a new debt auction. func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime time.Time) DebtAuction { // Note: Bidder is set to the initiator's module account address instead of module name. (when the first bid is placed, it is paid out to the initiator) // Setting to the module account address bypasses calling supply.SendCoinsFromModuleToModule, instead calls SendCoinsFromModuleToAccount. @@ -93,16 +93,21 @@ func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, E return auction } -// CollateralAuction type for forward reverse auction +// CollateralAuction is a two phase auction. +// Initially, in forward auction phase, bids can be placed up to a max bid. +// Then it switches to a reverse auction phase, where the initial amount up for auction is bidded down. +// Unsold Lot is sent to LotReturns, being divided among the addresses by weight. type CollateralAuction struct { BaseAuction MaxBid sdk.Coin - LotReturns WeightedAddresses // return addresses to pay out reductions in the lot amount to. Lot is bid down during reverse phase. + LotReturns WeightedAddresses } -// WithID returns an auction with the ID set +// WithID returns an auction with the ID set. func (a CollateralAuction) WithID(id uint64) Auction { a.ID = id; return a } +// IsReversePhase returns whether the auction has switched over to reverse phase or not. +// Auction initially start in forward phase. func (a CollateralAuction) IsReversePhase() bool { return a.Bid.IsEqual(a.MaxBid) } @@ -123,7 +128,7 @@ func (a CollateralAuction) String() string { ) } -// NewCollateralAuction creates a new forward reverse auction +// NewCollateralAuction returns a new collateral auction. func NewCollateralAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses) CollateralAuction { auction := CollateralAuction{ BaseAuction: BaseAuction{ @@ -140,12 +145,13 @@ func NewCollateralAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid return auction } -// WeightedAddresses type for storing an address and its associated weight +// WeightedAddresses is a type for storing some addresses and associated weights. type WeightedAddresses struct { Addresses []sdk.AccAddress Weights []sdk.Int } +// NewWeightedAddresses returns a new list addresses with weights. func NewWeightedAddresses(addrs []sdk.AccAddress, weights []sdk.Int) (WeightedAddresses, sdk.Error) { if len(addrs) != len(weights) { return WeightedAddresses{}, sdk.ErrInternal("number of addresses doesn't match number of weights") diff --git a/x/auction/types/codec.go b/x/auction/types/codec.go index 12f8f9eb..1904f432 100644 --- a/x/auction/types/codec.go +++ b/x/auction/types/codec.go @@ -15,7 +15,6 @@ func init() { 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(SurplusAuction{}, "auction/SurplusAuction", nil) cdc.RegisterConcrete(DebtAuction{}, "auction/DebtAuction", nil) diff --git a/x/auction/types/expected_keepers.go b/x/auction/types/expected_keepers.go index 85956576..021b8918 100644 --- a/x/auction/types/expected_keepers.go +++ b/x/auction/types/expected_keepers.go @@ -7,9 +7,6 @@ import ( // SupplyKeeper defines the expected supply Keeper type SupplyKeeper interface { - //GetSupply(ctx sdk.Context) supplyexported.SupplyI - - //GetModuleAddress(name string) sdk.AccAddress GetModuleAccount(ctx sdk.Context, moduleName string) supplyexported.ModuleAccountI SendCoinsFromModuleToModule(ctx sdk.Context, sender, recipient string, amt sdk.Coins) sdk.Error diff --git a/x/auction/types/genesis.go b/x/auction/types/genesis.go index 423d12fd..808ac050 100644 --- a/x/auction/types/genesis.go +++ b/x/auction/types/genesis.go @@ -4,17 +4,17 @@ import ( "bytes" ) -// Auctions type for an array of auctions +// Auctions is a slice of auctions. type Auctions []Auction -// GenesisState - auction state that must be provided at genesis +// GenesisState is auction state that must be provided at chain genesis. type GenesisState struct { - NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` - Params Params `json:"auction_params" yaml:"auction_params"` + NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` + Params Params `json:"auction_params" yaml:"auction_params"` Auctions Auctions `json:"genesis_auctions" yaml:"genesis_auctions"` } -// NewGenesisState returns a new genesis state object for auctions module +// NewGenesisState returns a new genesis state object for auctions module. func NewGenesisState(nextID uint64, ap Params, ga Auctions) GenesisState { return GenesisState{ NextAuctionID: nextID, @@ -23,24 +23,24 @@ func NewGenesisState(nextID uint64, ap Params, ga Auctions) GenesisState { } } -// DefaultGenesisState defines default genesis state for auction module +// DefaultGenesisState returns the default genesis state for auction module. func DefaultGenesisState() GenesisState { return NewGenesisState(0, DefaultParams(), Auctions{}) } -// Equal checks whether two GenesisState structs are equivalent +// 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 +// 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. +// ValidateGenesis validates genesis inputs. It returns error if validation of any input fails. func ValidateGenesis(data GenesisState) error { if err := data.Params.Validate(); err != nil { return err diff --git a/x/auction/types/keys.go b/x/auction/types/keys.go index e63bdf54..78ce6f34 100644 --- a/x/auction/types/keys.go +++ b/x/auction/types/keys.go @@ -26,7 +26,7 @@ var ( AuctionKeyPrefix = []byte{0x00} // prefix for keys that store auctions AuctionByTimeKeyPrefix = []byte{0x01} // prefix for keys that are part of the auctionsByTime index - NextAuctionIDKey = []byte{0x02} + NextAuctionIDKey = []byte{0x02} // key for the next auction id ) func GetAuctionKey(auctionID uint64) []byte { @@ -37,12 +37,14 @@ func GetAuctionByTimeKey(endTime time.Time, auctionID uint64) []byte { return append(sdk.FormatTimeBytes(endTime), Uint64ToBytes(auctionID)...) } +// Uint64FromBytes converts some fixed length bytes back into a uint64. func Uint64FromBytes(bz []byte) uint64 { return binary.BigEndian.Uint64(bz) } +// Uint64ToBytes converts a uint64 into fixed length bytes for use in store keys. func Uint64ToBytes(id uint64) []byte { bz := make([]byte, 8) binary.BigEndian.PutUint64(bz, uint64(id)) return bz -} \ No newline at end of file +} diff --git a/x/auction/types/msg.go b/x/auction/types/msg.go index dbaf43b2..976aedb1 100644 --- a/x/auction/types/msg.go +++ b/x/auction/types/msg.go @@ -8,8 +8,8 @@ var _ sdk.Msg = &MsgPlaceBid{} // MsgPlaceBid is the message type used to place a bid on any type of auction. type MsgPlaceBid struct { AuctionID uint64 - Bidder sdk.AccAddress // This can be a buyer (who increments bid), or a seller (who decrements lot) TODO rename to be clearer? - Amount sdk.Coin // The new bid or lot to set on the auction + Bidder sdk.AccAddress + Amount sdk.Coin // The new bid or lot to be set on the auction. } // NewMsgPlaceBid returns a new MsgPlaceBid. diff --git a/x/auction/types/params.go b/x/auction/types/params.go index 0bbf075d..22d01736 100644 --- a/x/auction/types/params.go +++ b/x/auction/types/params.go @@ -25,13 +25,13 @@ var ( var _ subspace.ParamSet = &Params{} -// Params governance parameters for auction module +// Params is the governance parameters for the auction module. type Params struct { MaxAuctionDuration time.Duration `json:"max_auction_duration" yaml:"max_auction_duration"` // max length of auction MaxBidDuration time.Duration `json:"max_bid_duration" yaml:"max_bid_duration"` // additional time added to the auction end time after each bid, capped by the expiry. } -// NewParams creates a new Params object +// NewParams returns a new Params object. func NewParams(maxAuctionDuration time.Duration, bidDuration time.Duration) Params { return Params{ MaxAuctionDuration: maxAuctionDuration, @@ -39,7 +39,7 @@ func NewParams(maxAuctionDuration time.Duration, bidDuration time.Duration) Para } } -// DefaultParams default parameters for auctions +// DefaultParams returns the default parameters for auctions. func DefaultParams() Params { return NewParams( DefaultMaxAuctionDuration, @@ -52,8 +52,7 @@ func ParamKeyTable() subspace.KeyTable { return subspace.NewKeyTable().RegisterParamSet(&Params{}) } -// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs -// pairs of auth module's parameters. +// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs. // nolint func (ap *Params) ParamSetPairs() subspace.ParamSetPairs { return subspace.ParamSetPairs{ diff --git a/x/auction/types/quierier.go b/x/auction/types/querier.go similarity index 100% rename from x/auction/types/quierier.go rename to x/auction/types/querier.go diff --git a/x/auction/types/utils.go b/x/auction/types/utils.go deleted file mode 100644 index b1aad11e..00000000 --- a/x/auction/types/utils.go +++ /dev/null @@ -1,9 +0,0 @@ -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 -} From 00c1a371d2860409b111a9ecfde163101cb304f8 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 10 Jan 2020 13:13:04 +0000 Subject: [PATCH 45/99] add params validation --- x/auction/types/params.go | 35 +++++++++++++++++++------------ x/auction/types/params_test.go | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 x/auction/types/params_test.go diff --git a/x/auction/types/params.go b/x/auction/types/params.go index 22d01736..5f52f258 100644 --- a/x/auction/types/params.go +++ b/x/auction/types/params.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/params/subspace" ) @@ -19,7 +20,7 @@ const ( // Parameter keys var ( // ParamStoreKeyParams Param store key for auction params - KeyAuctionBidDuration = []byte("MaxBidDuration") + KeyAuctionBidDuration = []byte("BidDuration") KeyAuctionDuration = []byte("MaxAuctionDuration") ) @@ -28,14 +29,14 @@ var _ subspace.ParamSet = &Params{} // Params is the governance parameters for the auction module. type Params struct { MaxAuctionDuration time.Duration `json:"max_auction_duration" yaml:"max_auction_duration"` // max length of auction - MaxBidDuration time.Duration `json:"max_bid_duration" yaml:"max_bid_duration"` // additional time added to the auction end time after each bid, capped by the expiry. + BidDuration time.Duration `json:"bid_duration" yaml:"bid_duration"` // additional time added to the auction end time after each bid, capped by the expiry. } // NewParams returns a new Params object. func NewParams(maxAuctionDuration time.Duration, bidDuration time.Duration) Params { return Params{ MaxAuctionDuration: maxAuctionDuration, - MaxBidDuration: bidDuration, + BidDuration: bidDuration, } } @@ -54,29 +55,37 @@ func ParamKeyTable() subspace.KeyTable { // ParamSetPairs implements the ParamSet interface and returns all the key/value pairs. // nolint -func (ap *Params) ParamSetPairs() subspace.ParamSetPairs { +func (p *Params) ParamSetPairs() subspace.ParamSetPairs { return subspace.ParamSetPairs{ - {KeyAuctionBidDuration, &ap.MaxBidDuration}, - {KeyAuctionDuration, &ap.MaxAuctionDuration}, + {KeyAuctionBidDuration, &p.BidDuration}, + {KeyAuctionDuration, &p.MaxAuctionDuration}, } } // Equal returns a boolean determining if two Params types are identical. -func (ap Params) Equal(ap2 Params) bool { - bz1 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&ap) - bz2 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&ap2) +func (p Params) Equal(p2 Params) bool { + bz1 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&p) + bz2 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&p2) return bytes.Equal(bz1, bz2) } // String implements stringer interface -func (ap Params) String() string { +func (p Params) String() string { return fmt.Sprintf(`Auction Params: Max Auction Duration: %s - Max Bid Duration: %s`, ap.MaxAuctionDuration, ap.MaxBidDuration) + Bid Duration: %s`, p.MaxAuctionDuration, p.BidDuration) } // Validate checks that the parameters have valid values. -func (ap Params) Validate() error { - // TODO check durations are within acceptable limits, if needed +func (p Params) Validate() error { + if p.BidDuration < 0 { + return sdk.ErrInternal("bid duration cannot be negative") + } + if p.MaxAuctionDuration < 0 { + return sdk.ErrInternal("max auction duration cannot be negative") + } + if p.BidDuration > p.MaxAuctionDuration { + return sdk.ErrInternal("bid duration param cannot be larger than max auction duration") + } return nil } diff --git a/x/auction/types/params_test.go b/x/auction/types/params_test.go new file mode 100644 index 00000000..0f642875 --- /dev/null +++ b/x/auction/types/params_test.go @@ -0,0 +1,38 @@ +package types + +import ( + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestParams_Validate(t *testing.T) { + type fields struct { + } + testCases := []struct { + name string + MaxAuctionDuration time.Duration + BidDuration time.Duration + expectErr bool + }{ + {"normal", 24 * time.Hour, 1 * time.Hour, false}, + {"negativeBid", 24 * time.Hour, -1 * time.Hour, true}, + {"negativeAuction", -24 * time.Hour, 1 * time.Hour, true}, + {"bid>auction", 1 * time.Hour, 24 * time.Hour, true}, + {"zeros", 0, 0, false}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + p := Params{ + MaxAuctionDuration: tc.MaxAuctionDuration, + BidDuration: tc.BidDuration, + } + err := p.Validate() + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} From d03509a17ae018feabddb240e33b0309813cbe88 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 10 Jan 2020 14:08:47 +0000 Subject: [PATCH 46/99] code cleanup, address review comments --- x/auction/abci.go | 15 +- x/auction/abci_test.go | 1 - x/auction/keeper/auctions.go | 101 ++++---- x/auction/keeper/keeper.go | 25 +- x/auction/keeper/keeper_test.go | 2 +- x/auction/types/auctions.go | 9 +- x/auction/types/auctions_test.go | 396 ------------------------------- x/auction/types/keys.go | 11 +- 8 files changed, 89 insertions(+), 471 deletions(-) delete mode 100644 x/auction/types/auctions_test.go diff --git a/x/auction/abci.go b/x/auction/abci.go index 8ce80f90..402c5c12 100644 --- a/x/auction/abci.go +++ b/x/auction/abci.go @@ -6,17 +6,8 @@ import ( // EndBlocker runs at the end of every block. func EndBlocker(ctx sdk.Context, k Keeper) { - - var expiredAuctions []uint64 - k.IterateAuctionsByTime(ctx, ctx.BlockTime(), func(id uint64) bool { - expiredAuctions = append(expiredAuctions, id) - return false - }) - // Note: iteration and auction closing are in separate loops as db should not be modified during iteration // TODO is this correct? gov modifies during iteration - for _, id := range expiredAuctions { - err := k.CloseAuction(ctx, id) - if err != nil { - panic(err) - } + err := k.CloseExpiredAuctions(ctx) + if err != nil { + panic(err) } } diff --git a/x/auction/abci_test.go b/x/auction/abci_test.go index 418ca49a..4ee5cd13 100644 --- a/x/auction/abci_test.go +++ b/x/auction/abci_test.go @@ -22,7 +22,6 @@ func TestKeeper_EndBlocker(t *testing.T) { returnAddrs := addrs[1:] returnWeights := []sdk.Int{sdk.NewInt(1)} sellerModName := liquidator.ModuleName - //sellerAddr := supply.NewModuleAddress(sellerModName) tApp := app.NewTestApp() sellerAcc := supply.NewEmptyModuleAccount(sellerModName) diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index 8c92f3ba..755027fe 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -11,15 +11,18 @@ import ( // StartSurplusAuction starts a new surplus (forward) auction. func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (uint64, sdk.Error) { - // create auction - auction := types.NewSurplusAuction(seller, lot, bidDenom, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) - // take coins from module account + auction := types.NewSurplusAuction( + seller, + lot, + bidDenom, + ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration)) + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) if err != nil { return 0, err } - // store the auction + auctionID, err := k.StoreNewAuction(ctx, auction) if err != nil { return 0, err @@ -29,15 +32,19 @@ func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin // StartDebtAuction starts a new debt (reverse) auction. func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin) (uint64, sdk.Error) { - // create auction - auction := types.NewDebtAuction(buyer, bid, initialLot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration)) + + auction := types.NewDebtAuction( + buyer, + bid, + initialLot, + ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration)) // This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker. macc := k.supplyKeeper.GetModuleAccount(ctx, buyer) if !macc.HasPermission(supply.Minter) { return 0, sdk.ErrInternal("module does not have minting permissions") } - // store the auction + auctionID, err := k.StoreNewAuction(ctx, auction) if err != nil { return 0, err @@ -45,21 +52,20 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in return auctionID, nil } -// StartCollateralAuction starts a new collateral (2-phase) auction where bidders bid up to a maxBid, then switch to bidding down on the Lot. +// StartCollateralAuction starts a new collateral (2-phase) auction. func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int) (uint64, sdk.Error) { - // create auction + weightedAddresses, err := types.NewWeightedAddresses(lotReturnAddrs, lotReturnWeights) if err != nil { return 0, err } auction := types.NewCollateralAuction(seller, lot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), maxBid, weightedAddresses) - // take coins from module account err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) if err != nil { return 0, err } - // store the auction + auctionID, err := k.StoreNewAuction(ctx, auction) if err != nil { return 0, err @@ -70,18 +76,17 @@ func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.C // PlaceBid places a bid on any auction. func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddress, newAmount sdk.Coin) sdk.Error { - // get auction from store auction, found := k.GetAuction(ctx, auctionID) if !found { return sdk.ErrInternal("auction doesn't exist") } - // validate + // validation common to all auctions if ctx.BlockTime().After(auction.GetEndTime()) { return sdk.ErrInternal("auction has closed") } - // place bid + // move coins and return updated auction var err sdk.Error var updatedAuction types.Auction switch a := auction.(type) { @@ -106,14 +111,13 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddres panic(fmt.Sprintf("unrecognized auction type: %T", auction)) } - // store updated auction k.SetAuction(ctx, updatedAuction) return nil } // PlaceBidSurplus places a forward bid on a surplus auction, moving coins and returning the updated auction. func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.SurplusAuction, sdk.Error) { - // Validate New Bid + // Validate new bid if bid.Denom != a.Bid.Denom { return a, sdk.ErrInternal("bid denom doesn't match auction") } @@ -121,9 +125,9 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder return a, sdk.ErrInternal("bid not greater than last bid") } - // Move Coins - if !bidder.Equals(a.Bidder) && !a.Bid.IsZero() { // catch edge case of someone updating their bid with a low balance, also don't send if amt is zero - // pay back previous bidder + // New bidder pays back old bidder + // Catch edge cases of a bidder replacing their own bid, and the amount being zero (sending zero coins produces meaningless send events). + if !bidder.Equals(a.Bidder) && !a.Bid.IsZero() { err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid)) if err != nil { return a, err @@ -133,7 +137,7 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder return a, err } } - // burn increase in bid + // Increase in bid is burned err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, a.Initiator, sdk.NewCoins(bid.Sub(a.Bid))) if err != nil { return a, err @@ -146,8 +150,7 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder // Update Auction a.Bidder = bidder a.Bid = bid - // increment timeout - a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultBidDuration), a.MaxEndTime) + a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout return a, nil } @@ -167,9 +170,10 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc if a.MaxBid.IsLT(bid) { return a, sdk.ErrInternal("bid higher than max bid") } - // Move Coins - // pay back previous bidder - if !bidder.Equals(a.Bidder) && !a.Bid.IsZero() { // catch edge case of someone updating their bid with a low balance, also don't send if amt is zero + + // New bidder pays back old bidder + // Catch edge cases of a bidder replacing their own bid, and the amount being zero (sending zero coins produces meaningless send events). + if !bidder.Equals(a.Bidder) && !a.Bid.IsZero() { err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid)) if err != nil { return a, err @@ -179,7 +183,7 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc return a, err } } - // pay increase in bid to auction initiator + // Increase in bid sent to auction initiator err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, a.Initiator, sdk.NewCoins(bid.Sub(a.Bid))) if err != nil { return a, err @@ -188,15 +192,14 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc // Update Auction a.Bidder = bidder a.Bid = bid - // increment timeout - a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultBidDuration), a.MaxEndTime) + a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout return a, nil } // PlaceReverseBidCollateral places a reverse bid on a collateral auction, moving coins and returning the updated auction. func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.CollateralAuction, sdk.Error) { - // Validate bid + // Validate new bid if lot.Denom != a.Lot.Denom { return a, sdk.ErrInternal("lot denom doesn't match auction") } @@ -210,8 +213,9 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc return a, sdk.ErrInternal("auction in reverse phase, new bid not less than previous amount") } - // Move Coins - if !bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance + // New bidder pays back old bidder + // Catch edge cases of a bidder replacing their own bid + if !bidder.Equals(a.Bidder) { err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid)) if err != nil { return a, err @@ -221,7 +225,8 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc return a, err } } - // FIXME paying out rateably to cdp depositors is vulnerable to errors compounding over multiple bids + // Decrease in lot is sent to weighted addresses (normally the CDP depositors) + // TODO paying out rateably to cdp depositors is vulnerable to errors compounding over multiple bids - check this can't be gamed. lotPayouts, err := splitCoinIntoWeightedBuckets(a.Lot.Sub(lot), a.LotReturns.Weights) if err != nil { return a, err @@ -236,15 +241,14 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc // Update Auction a.Bidder = bidder a.Lot = lot - // increment timeout - a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultBidDuration), a.MaxEndTime) + a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout return a, nil } // PlaceBidDebt places a reverse bid on a debt auction, moving coins and returning the updated auction. func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.DebtAuction, sdk.Error) { - // Validate New Bid + // Validate new bid if lot.Denom != a.Lot.Denom { return a, sdk.ErrInternal("lot denom doesn't match auction") } @@ -255,8 +259,9 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac return a, sdk.ErrInternal("lot not smaller than last lot") } - // Move Coins - if !bidder.Equals(a.Bidder) { // catch edge case of someone updating their bid with a low balance + // New bidder pays back old bidder + // Catch edge cases of a bidder replacing their own bid + if !bidder.Equals(a.Bidder) { err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid)) if err != nil { return a, err @@ -270,8 +275,7 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac // Update Auction a.Bidder = bidder a.Lot = lot - // increment timeout - a.EndTime = earliestTime(ctx.BlockTime().Add(types.DefaultBidDuration), a.MaxEndTime) + a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout return a, nil } @@ -279,12 +283,11 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac // CloseAuction closes an auction and distributes funds to the highest bidder. func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) 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.BlockTime().Before(auction.GetEndTime()) { return sdk.ErrInternal(fmt.Sprintf("auction can't be closed as curent block time (%v) is under auction end time (%v)", ctx.BlockTime(), auction.GetEndTime())) } @@ -342,6 +345,22 @@ func (k Keeper) PayoutCollateralAuction(ctx sdk.Context, a types.CollateralAucti return nil } +// CloseExpiredAuctions finds all auctions that are past (or at) their ending times and closes them, paying out to the highest bidder. +func (k Keeper) CloseExpiredAuctions(ctx sdk.Context) sdk.Error { + var expiredAuctions []uint64 + k.IterateAuctionsByTime(ctx, ctx.BlockTime(), func(id uint64) bool { + expiredAuctions = append(expiredAuctions, id) + return false + }) + // Note: iteration and auction closing are in separate loops as db should not be modified during iteration // TODO is this correct? gov modifies during iteration + for _, id := range expiredAuctions { + if err := k.CloseAuction(ctx, id); err != nil { + return err + } + } + return nil +} + // earliestTime returns the earliest of two times. func earliestTime(t1, t2 time.Time) time.Time { if t1.Before(t2) { diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index d756463f..c9ee241b 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -1,12 +1,14 @@ package keeper import ( + "fmt" "time" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/params/subspace" + "github.com/tendermint/tendermint/libs/log" "github.com/kava-labs/kava/x/auction/types" ) @@ -29,6 +31,11 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, supplyKeeper types.Suppl } } +// Logger returns a module-specific logger. +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) +} + // SetNextAuctionID stores an ID to be used for the next created auction func (k Keeper) SetNextAuctionID(ctx sdk.Context, id uint64) { store := ctx.KVStore(k.storeKey) @@ -79,16 +86,14 @@ func (k Keeper) SetAuction(ctx sdk.Context, auction types.Auction) { // remove the auction from the byTime index if it is already in there existingAuction, found := k.GetAuction(ctx, auction.GetID()) if found { - k.removeFromIndex(ctx, existingAuction.GetEndTime(), existingAuction.GetID()) + k.removeFromByTimeIndex(ctx, existingAuction.GetEndTime(), existingAuction.GetID()) } - // store auction store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) bz := k.cdc.MustMarshalBinaryLengthPrefixed(auction) store.Set(types.GetAuctionKey(auction.GetID()), bz) - // add to index - k.insertIntoIndex(ctx, auction.GetEndTime(), auction.GetID()) + k.InsertIntoByTimeIndex(ctx, auction.GetEndTime(), auction.GetID()) } // GetAuction gets an auction from the store. @@ -107,25 +112,23 @@ func (k Keeper) GetAuction(ctx sdk.Context, auctionID uint64) (types.Auction, bo // DeleteAuction removes an auction from the store, and any indexes. func (k Keeper) DeleteAuction(ctx sdk.Context, auctionID uint64) { - // remove from index auction, found := k.GetAuction(ctx, auctionID) if found { - k.removeFromIndex(ctx, auction.GetEndTime(), auctionID) + k.removeFromByTimeIndex(ctx, auction.GetEndTime(), auctionID) } - // delete auction store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) store.Delete(types.GetAuctionKey(auctionID)) } -// insertIntoIndex adds an auction ID and end time into the byTime index. -func (k Keeper) insertIntoIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) { +// InsertIntoByTimeIndex adds an auction ID and end time into the byTime index. +func (k Keeper) InsertIntoByTimeIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) { // TODO make private, and find way to make tests work store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) store.Set(types.GetAuctionByTimeKey(endTime, auctionID), types.Uint64ToBytes(auctionID)) } -// removeFromIndex removes an auction ID and end time from the byTime index. -func (k Keeper) removeFromIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) { +// removeFromByTimeIndex removes an auction ID and end time from the byTime index. +func (k Keeper) removeFromByTimeIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) store.Delete(types.GetAuctionByTimeKey(endTime, auctionID)) } diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go index 3c59d9ae..03adaeeb 100644 --- a/x/auction/keeper/keeper_test.go +++ b/x/auction/keeper/keeper_test.go @@ -113,7 +113,7 @@ func TestIterateAuctionsByTime(t *testing.T) { {time.Date(9999, time.January, 1, 0, 0, 0, 0, time.UTC), 0}, // distant future } for _, v := range byTimeIndex { - keeper.InsertIntoIndex(ctx, v.endTime, v.auctionID) + keeper.InsertIntoByTimeIndex(ctx, v.endTime, v.auctionID) } // read out values from index up to a cutoff time and check they are as expected diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 9fbd01cd..1db5036e 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -46,7 +46,8 @@ func (a BaseAuction) String() string { ) } -// SurplusAuction is a forward auction that burns what it receives as bids. +// SurplusAuction is a forward auction that burns what it receives from bids. +// It is normally used to sell off excess pegged asset acquired by the CDP system. type SurplusAuction struct { BaseAuction } @@ -69,6 +70,7 @@ func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime tim } // DebtAuction is a reverse auction that mints what it pays out. +// It is normally used to acquire pegged asset to cover the CDP system's debts that were not covered by selling collateral. type DebtAuction struct { BaseAuction } @@ -86,7 +88,7 @@ func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, E Initiator: buyerModAccName, Lot: initialLot, Bidder: supply.NewModuleAddress(buyerModAccName), // send proceeds from the first bid to the buyer. - Bid: bid, // amount that the buyer it buying - doesn't change over course of auction + Bid: bid, // amount that the buyer is buying - doesn't change over course of auction EndTime: EndTime, MaxEndTime: EndTime, }} @@ -95,8 +97,9 @@ func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, E // CollateralAuction is a two phase auction. // Initially, in forward auction phase, bids can be placed up to a max bid. -// Then it switches to a reverse auction phase, where the initial amount up for auction is bidded down. +// Then it switches to a reverse auction phase, where the initial amount up for auction is bid down. // Unsold Lot is sent to LotReturns, being divided among the addresses by weight. +// Collateral auctions are normally used to sell off collateral seized from CDPs. type CollateralAuction struct { BaseAuction MaxBid sdk.Coin diff --git a/x/auction/types/auctions_test.go b/x/auction/types/auctions_test.go deleted file mode 100644 index a4d059d6..00000000 --- a/x/auction/types/auctions_test.go +++ /dev/null @@ -1,396 +0,0 @@ -package types - -// // 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 TesSurplusAuction_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 SurplusAuction -// args args -// expectedOutputs []BankOutput -// expectedInputs []BankInput -// expectedEndTime EndTime -// expectedBidder sdk.AccAddress -// expectedBid sdk.Coin -// expectpass bool -// }{ -// { -// "normal", -// SurplusAuction{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", -// SurplusAuction{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", -// SurplusAuction{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", -// SurplusAuction{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", -// SurplusAuction{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 TestDebtAuction_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 DebtAuction -// args args -// expectedOutputs []BankOutput -// expectedInputs []BankInput -// expectedEndTime EndTime -// expectedBidder sdk.AccAddress -// expectedLot sdk.Coin -// expectpass bool -// }{ -// { -// "normal", -// DebtAuction{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", -// DebtAuction{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", -// DebtAuction{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", -// DebtAuction{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", -// DebtAuction{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 TestCollateralAuction_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 CollateralAuction -// args args -// expectedOutputs []BankOutput -// expectedInputs []BankInput -// expectedEndTime EndTime -// expectedBidder sdk.AccAddress -// expectedLot sdk.Coin -// expectedBid sdk.Coin -// expectpass bool -// }{ -// { -// "normalForwardBid", -// CollateralAuction{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", -// CollateralAuction{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, -// }, -// { -// "normalDebtBid", -// CollateralAuction{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) -// } diff --git a/x/auction/types/keys.go b/x/auction/types/keys.go index 78ce6f34..aca635a9 100644 --- a/x/auction/types/keys.go +++ b/x/auction/types/keys.go @@ -21,7 +21,6 @@ const ( DefaultParamspace = ModuleName ) -// TODO use cont to keep immutability? var ( AuctionKeyPrefix = []byte{0x00} // prefix for keys that store auctions AuctionByTimeKeyPrefix = []byte{0x01} // prefix for keys that are part of the auctionsByTime index @@ -37,14 +36,14 @@ func GetAuctionByTimeKey(endTime time.Time, auctionID uint64) []byte { return append(sdk.FormatTimeBytes(endTime), Uint64ToBytes(auctionID)...) } -// Uint64FromBytes converts some fixed length bytes back into a uint64. -func Uint64FromBytes(bz []byte) uint64 { - return binary.BigEndian.Uint64(bz) -} - // Uint64ToBytes converts a uint64 into fixed length bytes for use in store keys. func Uint64ToBytes(id uint64) []byte { bz := make([]byte, 8) binary.BigEndian.PutUint64(bz, uint64(id)) return bz } + +// Uint64FromBytes converts some fixed length bytes back into a uint64. +func Uint64FromBytes(bz []byte) uint64 { + return binary.BigEndian.Uint64(bz) +} From 65ef8a9ba38ce4ba121be4709780949d907ab451 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 10 Jan 2020 18:55:48 +0100 Subject: [PATCH 47/99] resolve minor TODOs --- x/auction/keeper/auctions.go | 4 ++-- x/auction/keeper/auctions_test.go | 2 +- x/auction/keeper/keeper.go | 4 ---- x/auction/keeper/math_test.go | 1 - 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index 755027fe..cecbadd5 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -121,7 +121,7 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder if bid.Denom != a.Bid.Denom { return a, sdk.ErrInternal("bid denom doesn't match auction") } - if !a.Bid.IsLT(bid) { // TODO add minimum bid size + if !a.Bid.IsLT(bid) { return a, sdk.ErrInternal("bid not greater than last bid") } @@ -255,7 +255,7 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac if lot.IsNegative() { return a, sdk.ErrInternal("lot less than 0") } - if !lot.IsLT(a.Lot) { // TODO add min bid decrements + if !lot.IsLT(a.Lot) { return a, sdk.ErrInternal("lot not smaller than last lot") } diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index 6e4ffeb4..197e1c66 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -202,7 +202,7 @@ func TestStartSurplusAuction(t *testing.T) { initialLiquidatorCoins := cs(c("stable", 100)) tApp := app.NewTestApp() - liqAcc := supply.NewEmptyModuleAccount(liquidator.ModuleName, supply.Burner) // TODO could add test to check for burner permissions + liqAcc := supply.NewEmptyModuleAccount(liquidator.ModuleName, supply.Burner) require.NoError(t, liqAcc.SetCoins(initialLiquidatorCoins)) tApp.InitializeFromGenesisStates( NewAuthGenStateFromAccs(authexported.GenesisAccounts{liqAcc}), diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index c9ee241b..cd0b01c2 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -18,7 +18,6 @@ type Keeper struct { storeKey sdk.StoreKey cdc *codec.Codec paramSubspace subspace.Subspace - // TODO codespace } // NewKeeper returns a new auction keeper. @@ -43,12 +42,10 @@ func (k Keeper) SetNextAuctionID(ctx sdk.Context, id uint64) { } // GetNextAuctionID reads the next available global ID from store -// TODO might be nicer to convert not found error to a panic, it's not an error that can be recovered from func (k Keeper) GetNextAuctionID(ctx sdk.Context) (uint64, sdk.Error) { store := ctx.KVStore(k.storeKey) bz := store.Get(types.NextAuctionIDKey) if bz == nil { - //return 0, types.ErrInvalidGenesis(k.codespace, "initial auction ID hasn't been set") // TODO create error return 0, sdk.ErrInternal("initial auction ID hasn't been set") } return types.Uint64FromBytes(bz), nil @@ -135,7 +132,6 @@ func (k Keeper) removeFromByTimeIndex(ctx sdk.Context, endTime time.Time, auctio // IterateAuctionByTime provides an iterator over auctions ordered by auction.EndTime. // For each auction cb will be callled. If cb returns true the iterator will close and stop. -// TODO can the cutoff time be removed in favour of caller specifying cutoffs in the callback? func (k Keeper) IterateAuctionsByTime(ctx sdk.Context, inclusiveCutoffTime time.Time, cb func(auctionID uint64) (stop bool)) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) iterator := store.Iterator( diff --git a/x/auction/keeper/math_test.go b/x/auction/keeper/math_test.go index 58e33e7e..ee42c594 100644 --- a/x/auction/keeper/math_test.go +++ b/x/auction/keeper/math_test.go @@ -18,7 +18,6 @@ func TestSplitIntIntoWeightedBuckets(t *testing.T) { {"100split1,9", i(100), is(1, 9), is(10, 90)}, {"7split1,2", i(7), is(1, 2), is(2, 5)}, {"17split1,1,1", i(17), is(1, 1, 1), is(6, 6, 5)}, - // TODO more tests } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { From 27f3e76da3cb90b2f7e2024e9a61151435ec882a Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 10 Jan 2020 18:57:38 +0100 Subject: [PATCH 48/99] sync spec with code --- x/auction/spec/02_state.md | 28 +++++++++++++++++----------- x/auction/spec/05_params.md | 2 +- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/x/auction/spec/02_state.md b/x/auction/spec/02_state.md index 075cf08c..306665f9 100644 --- a/x/auction/spec/02_state.md +++ b/x/auction/spec/02_state.md @@ -33,37 +33,43 @@ type Auction interface { GetEndTime() time.Time } -// BaseAuction type shared by all Auctions +// BaseAuction is a common type shared by all Auctions. type BaseAuction struct { ID uint64 - Initiator string // Module that starts the auction. Giving away Lot (aka seller in a forward auction). Restricted to being a module account name rather than any account. - 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 time.Time // Auction closing time. Triggers at the end of the block with time ≥ endTime (bids placed in that block are valid) // TODO ensure everything is consistent with this + Initiator string // Module name that starts the auction. Pays out Lot. + Lot sdk.Coin // Coins that will paid out by Initiator to the winning bidder. + Bidder sdk.AccAddress // Latest bidder. Receiver of Lot. + Bid sdk.Coin // Coins paid into the auction the bidder. + EndTime time.Time // Current auction closing time. Triggers at the end of the block with time ≥ EndTime. MaxEndTime time.Time // Maximum closing time. Auctions can close before this but never after. } -// SurplusAuction type for forward auctions +// SurplusAuction is a forward auction that burns what it receives from bids. +// It is normally used to sell off excess pegged asset acquired by the CDP system. type SurplusAuction struct { BaseAuction } -// DebtAuction type for reverse auctions +// DebtAuction is a reverse auction that mints what it pays out. +// It is normally used to acquire pegged asset to cover the CDP system's debts that were not covered by selling collateral. type DebtAuction struct { BaseAuction } -// WeightedAddresses type for storing an address and its associated weight +// WeightedAddresses is a type for storing some addresses and associated weights. type WeightedAddresses struct { Addresses []sdk.AccAddress Weights []sdk.Int } -// CollateralAuction type for forward reverse auction +// CollateralAuction is a two phase auction. +// Initially, in forward auction phase, bids can be placed up to a max bid. +// Then it switches to a reverse auction phase, where the initial amount up for auction is bid down. +// Unsold Lot is sent to LotReturns, being divided among the addresses by weight. +// Collateral auctions are normally used to sell off collateral seized from CDPs. type CollateralAuction struct { BaseAuction MaxBid sdk.Coin - LotReturns WeightedAddresses // return addresses to pay out reductions in the lot amount to. Lot is bid down during reverse phase. + LotReturns WeightedAddresses } ``` diff --git a/x/auction/spec/05_params.md b/x/auction/spec/05_params.md index f6751bc1..9a108303 100644 --- a/x/auction/spec/05_params.md +++ b/x/auction/spec/05_params.md @@ -5,4 +5,4 @@ The auction module contains the following parameters: | Key | Type | Example | | ------------------ | ---------------------- | -----------| | MaxAuctionDuration | string (time.Duration) | "48h0m0s" | -| MaxBidDuration | string (time.Duration) | "3h0m0s" | +| BidDuration | string (time.Duration) | "3h0m0s" | From 61e5de556cfdf6fdf842110c6bf20541d04ef437 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Sun, 12 Jan 2020 15:17:47 +0100 Subject: [PATCH 49/99] add debt tracking to auctions --- x/auction/abci_test.go | 4 +- x/auction/keeper/auctions.go | 65 ++++++++++++++++++++++++++++--- x/auction/keeper/auctions_test.go | 22 ++++++----- x/auction/keeper/keeper_test.go | 4 +- x/auction/types/auctions.go | 35 ++++++++++------- 5 files changed, 96 insertions(+), 34 deletions(-) diff --git a/x/auction/abci_test.go b/x/auction/abci_test.go index 4ee5cd13..d147c768 100644 --- a/x/auction/abci_test.go +++ b/x/auction/abci_test.go @@ -25,7 +25,7 @@ func TestKeeper_EndBlocker(t *testing.T) { tApp := app.NewTestApp() sellerAcc := supply.NewEmptyModuleAccount(sellerModName) - require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100)))) + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100), c("debt", 100)))) tApp.InitializeFromGenesisStates( NewAuthGenStateFromAccs(authexported.GenesisAccounts{ auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), @@ -36,7 +36,7 @@ func TestKeeper_EndBlocker(t *testing.T) { ctx := tApp.NewContext(true, abci.Header{}) keeper := tApp.GetAuctionKeeper() - auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights) + auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights, c("debt", 40)) require.NoError(t, err) require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 30))) diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index cecbadd5..50979e26 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -31,13 +31,14 @@ func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin } // StartDebtAuction starts a new debt (reverse) auction. -func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin) (uint64, sdk.Error) { +func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin, debt sdk.Coin) (uint64, sdk.Error) { auction := types.NewDebtAuction( buyer, bid, initialLot, - ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration)) + ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration), + debt) // This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker. macc := k.supplyKeeper.GetModuleAccount(ctx, buyer) @@ -45,6 +46,11 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in return 0, sdk.ErrInternal("module does not have minting permissions") } + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, buyer, types.ModuleName, sdk.NewCoins(debt)) + if err != nil { + return 0, err + } + auctionID, err := k.StoreNewAuction(ctx, auction) if err != nil { return 0, err @@ -53,18 +59,28 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in } // StartCollateralAuction starts a new collateral (2-phase) auction. -func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int) (uint64, sdk.Error) { +func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int, debt sdk.Coin) (uint64, sdk.Error) { weightedAddresses, err := types.NewWeightedAddresses(lotReturnAddrs, lotReturnWeights) if err != nil { return 0, err } - auction := types.NewCollateralAuction(seller, lot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), maxBid, weightedAddresses) + auction := types.NewCollateralAuction( + seller, + lot, + ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), + maxBid, + weightedAddresses, + debt) err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) if err != nil { return 0, err } + err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(debt)) + if err != nil { + return 0, err + } auctionID, err := k.StoreNewAuction(ctx, auction) if err != nil { @@ -184,10 +200,24 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc } } // Increase in bid sent to auction initiator - err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, a.Initiator, sdk.NewCoins(bid.Sub(a.Bid))) + bidIncrement := bid.Sub(a.Bid) + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, a.Initiator, sdk.NewCoins(bidIncrement)) if err != nil { return a, err } + // Debt coins are sent to liquidator (until there is no CorrespondingDebt left). Amount sent is equal to bidIncrement. + if a.CorrespondingDebt.IsPositive() { + + debtAmountToReturn := sdk.MinInt(bidIncrement.Amount, a.CorrespondingDebt.Amount) + debtToReturn := sdk.NewCoin(a.CorrespondingDebt.Denom, debtAmountToReturn) + + err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(debtToReturn)) + if err != nil { + return a, err + } + a.CorrespondingDebt = a.CorrespondingDebt.Sub(debtToReturn) // debtToReturn will always be ≤ a.CorrespondingDebt from the MinInt above + // TODO optionally burn out debt and stable just returned to liquidator + } // Update Auction a.Bidder = bidder @@ -271,6 +301,19 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac return a, err } } + // Debt coins are sent to liquidator the first time a bid is placed. Amount sent is equal to Bid. + if a.Bidder.Equals(supply.NewModuleAddress(a.Initiator)) { + + debtAmountToReturn := sdk.MinInt(a.Bid.Amount, a.CorrespondingDebt.Amount) + debtToReturn := sdk.NewCoin(a.CorrespondingDebt.Denom, debtAmountToReturn) + + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(debtToReturn)) + if err != nil { + return a, err + } + a.CorrespondingDebt = a.CorrespondingDebt.Sub(debtToReturn) // debtToReturn will always be ≤ a.CorrespondingDebt from the MinInt above + // TODO optionally burn out debt and stable just returned to liquidator + } // Update Auction a.Bidder = bidder @@ -324,6 +367,12 @@ func (k Keeper) PayoutDebtAuction(ctx sdk.Context, a types.DebtAuction) sdk.Erro if err != nil { return err } + if a.CorrespondingDebt.IsPositive() { + err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(a.CorrespondingDebt)) + if err != nil { + return err + } + } return nil } @@ -342,6 +391,12 @@ func (k Keeper) PayoutCollateralAuction(ctx sdk.Context, a types.CollateralAucti if err != nil { return err } + if a.CorrespondingDebt.IsPositive() { + err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(a.CorrespondingDebt)) + if err != nil { + return err + } + } return nil } diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index 197e1c66..9a45b036 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -69,27 +69,29 @@ func TestDebtAuctionBasic(t *testing.T) { tApp := app.NewTestApp() + buyerAcc := supply.NewEmptyModuleAccount(buyerModName, supply.Minter) // reverse auctions mint payout + require.NoError(t, buyerAcc.SetCoins(cs(c("debt", 100)))) tApp.InitializeFromGenesisStates( NewAuthGenStateFromAccs(authexported.GenesisAccounts{ auth.NewBaseAccount(seller, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), - supply.NewEmptyModuleAccount(buyerModName, supply.Minter), // reverse auctions mint payout + buyerAcc, }), ) ctx := tApp.NewContext(false, abci.Header{}) keeper := tApp.GetAuctionKeeper() // Start auction - auctionID, err := keeper.StartDebtAuction(ctx, buyerModName, c("token1", 20), c("token2", 99999)) // buyer, bid, initialLot + auctionID, err := keeper.StartDebtAuction(ctx, buyerModName, c("token1", 20), c("token2", 99999), c("debt", 20)) require.NoError(t, err) - // Check buyer's coins have not decreased, as lot is minted at the end - tApp.CheckBalance(t, ctx, buyerAddr, nil) // zero coins + // Check buyer's coins have not decreased (except for debt), as lot is minted at the end + tApp.CheckBalance(t, ctx, buyerAddr, cs(c("debt", 80))) // Place a bid require.NoError(t, keeper.PlaceBid(ctx, 0, seller, c("token2", 10))) // Check seller's coins have decreased tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) // Check buyer's coins have increased - tApp.CheckBalance(t, ctx, buyerAddr, cs(c("token1", 20))) + tApp.CheckBalance(t, ctx, buyerAddr, cs(c("token1", 20), c("debt", 100))) // Close auction at just after auction expiry ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration)) @@ -109,7 +111,7 @@ func TestCollateralAuctionBasic(t *testing.T) { tApp := app.NewTestApp() sellerAcc := supply.NewEmptyModuleAccount(sellerModName) - require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100)))) + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100), c("debt", 100)))) tApp.InitializeFromGenesisStates( NewAuthGenStateFromAccs(authexported.GenesisAccounts{ auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), @@ -123,17 +125,17 @@ func TestCollateralAuctionBasic(t *testing.T) { keeper := tApp.GetAuctionKeeper() // Start auction - auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights) // seller, lot, maxBid, otherPerson + auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights, c("debt", 40)) require.NoError(t, err) // Check seller's coins have decreased - tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100), c("debt", 60))) // Place a forward bid require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 10))) // Check bidder's coins have decreased tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) // Check seller's coins have increased - tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 110))) + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 110), c("debt", 70))) // Check return addresses have not received coins for _, ra := range returnAddrs { tApp.CheckBalance(t, ctx, ra, cs(c("token1", 100), c("token2", 100))) @@ -145,7 +147,7 @@ func TestCollateralAuctionBasic(t *testing.T) { // Check bidder's coins have decreased tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50))) // Check seller's coins have increased - tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 150))) + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 150), c("debt", 100))) // Check return addresses have received coins tApp.CheckBalance(t, ctx, returnAddrs[0], cs(c("token1", 102), c("token2", 100))) tApp.CheckBalance(t, ctx, returnAddrs[1], cs(c("token1", 102), c("token2", 100))) diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go index 03adaeeb..4b811bb9 100644 --- a/x/auction/keeper/keeper_test.go +++ b/x/auction/keeper/keeper_test.go @@ -74,8 +74,8 @@ func TestIterateAuctions(t *testing.T) { auctions := []types.Auction{ types.NewSurplusAuction("sellerMod", c("denom", 12345678), "anotherdenom", time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)).WithID(0), - types.NewDebtAuction("buyerMod", c("denom", 12345678), c("anotherdenom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)).WithID(1), - types.NewCollateralAuction("sellerMod", c("denom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC), c("anotherdenom", 12345678), types.WeightedAddresses{}).WithID(2), + types.NewDebtAuction("buyerMod", c("denom", 12345678), c("anotherdenom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC), c("debt", 12345678)).WithID(1), + types.NewCollateralAuction("sellerMod", c("denom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC), c("anotherdenom", 12345678), types.WeightedAddresses{}, c("debt", 12345678)).WithID(2), } for _, a := range auctions { keeper.SetAuction(ctx, a) diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 1db5036e..beec6d24 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -73,25 +73,28 @@ func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime tim // It is normally used to acquire pegged asset to cover the CDP system's debts that were not covered by selling collateral. type DebtAuction struct { BaseAuction + CorrespondingDebt sdk.Coin } // WithID returns an auction with the ID set. func (a DebtAuction) WithID(id uint64) Auction { a.ID = id; return a } // NewDebtAuction returns a new debt auction. -func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime time.Time) DebtAuction { +func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime time.Time, debt sdk.Coin) DebtAuction { // Note: Bidder is set to the initiator's module account address instead of module name. (when the first bid is placed, it is paid out to the initiator) // Setting to the module account address bypasses calling supply.SendCoinsFromModuleToModule, instead calls SendCoinsFromModuleToAccount. // This isn't a problem currently, but if additional logic/validation was added for sending to coins to Module Accounts, it would be bypassed. - auction := DebtAuction{BaseAuction{ - // no ID - Initiator: buyerModAccName, - Lot: initialLot, - Bidder: supply.NewModuleAddress(buyerModAccName), // send proceeds from the first bid to the buyer. - Bid: bid, // amount that the buyer is buying - doesn't change over course of auction - EndTime: EndTime, - MaxEndTime: EndTime, - }} + auction := DebtAuction{ + BaseAuction: BaseAuction{ + // no ID + Initiator: buyerModAccName, + Lot: initialLot, + Bidder: supply.NewModuleAddress(buyerModAccName), // send proceeds from the first bid to the buyer. + Bid: bid, // amount that the buyer is buying - doesn't change over course of auction + EndTime: EndTime, + MaxEndTime: EndTime}, + CorrespondingDebt: debt, + } return auction } @@ -102,8 +105,9 @@ func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, E // Collateral auctions are normally used to sell off collateral seized from CDPs. type CollateralAuction struct { BaseAuction - MaxBid sdk.Coin - LotReturns WeightedAddresses + CorrespondingDebt sdk.Coin + MaxBid sdk.Coin + LotReturns WeightedAddresses } // WithID returns an auction with the ID set. @@ -132,7 +136,7 @@ func (a CollateralAuction) String() string { } // NewCollateralAuction returns a new collateral auction. -func NewCollateralAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses) CollateralAuction { +func NewCollateralAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses, debt sdk.Coin) CollateralAuction { auction := CollateralAuction{ BaseAuction: BaseAuction{ // no ID @@ -142,8 +146,9 @@ func NewCollateralAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid Bid: sdk.NewInt64Coin(maxBid.Denom, 0), EndTime: EndTime, MaxEndTime: EndTime}, - MaxBid: maxBid, - LotReturns: lotReturns, + CorrespondingDebt: debt, + MaxBid: maxBid, + LotReturns: lotReturns, } return auction } From e1c11d411a86f8ecb4a65d6e90e17eccb75a4240 Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Sun, 12 Jan 2020 16:12:22 +0100 Subject: [PATCH 50/99] Update Auction Module (#276) * rough auction type refactor * replace endTime type * split keeper file up * update store methods * move store methods to keeper.go * move nextAuctionID from params to genState * simplify auction type to not use pointers * add basic auction tests * update endblocker test * add payout to depositors feature * add more tests * move index updates to Get/Set for more safety * remove slightly unecessary ID type * remove unused message types * feat: add spec, update redundant type names * stop sending zero coins * use only one coins field in MsgPlaceBid * remove uncessary Auction interface methods * give auction types more accurate names * remove vuepress comments from spec * minor spec updates * update doc comments * add params validation * code cleanup, address review comments * resolve minor TODOs * sync spec with code Co-authored-by: Kevin Davis --- app/app.go | 26 +- app/test_common.go | 5 +- x/auction/abci.go | 16 +- x/auction/abci_test.go | 38 +- x/auction/alias.go | 75 ++-- x/auction/client/cli/tx.go | 18 +- x/auction/client/rest/tx.go | 14 +- x/auction/doc.go | 14 - x/auction/genesis.go | 26 +- x/auction/handler.go | 2 +- x/auction/keeper/auctions.go | 386 +++++++++++++++++++ x/auction/keeper/auctions_test.go | 246 ++++++++++++ x/auction/keeper/integration_test.go | 24 ++ x/auction/keeper/keeper.go | 337 +++++----------- x/auction/keeper/keeper_test.go | 244 +++++------- x/auction/keeper/math.go | 69 ++++ x/auction/keeper/math_test.go | 36 ++ x/auction/keeper/params.go | 8 +- x/auction/keeper/querier.go | 17 +- x/auction/module.go | 15 +- x/auction/spec/01_concepts.md | 9 + x/auction/spec/02_state.md | 75 ++++ x/auction/spec/03_messages.md | 32 ++ x/auction/spec/04_events.md | 5 + x/auction/spec/05_params.md | 8 + x/auction/spec/06_end_block.md | 18 + x/auction/spec/README.md | 13 + x/auction/types/auctions.go | 334 +++++----------- x/auction/types/auctions_test.go | 403 -------------------- x/auction/types/codec.go | 7 +- x/auction/types/expected_keepers.go | 14 +- x/auction/types/genesis.go | 30 +- x/auction/types/keys.go | 34 ++ x/auction/types/msg.go | 67 +--- x/auction/types/msg_test.go | 24 +- x/auction/types/params.go | 87 ++--- x/auction/types/params_test.go | 38 ++ x/auction/types/{quierier.go => querier.go} | 0 x/auction/types/utils.go | 9 - x/liquidator/keeper/keeper.go | 7 +- x/liquidator/types/expected_keepers.go | 7 +- 41 files changed, 1511 insertions(+), 1326 deletions(-) delete mode 100644 x/auction/doc.go create mode 100644 x/auction/keeper/auctions.go create mode 100644 x/auction/keeper/auctions_test.go create mode 100644 x/auction/keeper/integration_test.go create mode 100644 x/auction/keeper/math.go create mode 100644 x/auction/keeper/math_test.go create mode 100644 x/auction/spec/01_concepts.md create mode 100644 x/auction/spec/02_state.md create mode 100644 x/auction/spec/03_messages.md create mode 100644 x/auction/spec/04_events.md create mode 100644 x/auction/spec/05_params.md create mode 100644 x/auction/spec/06_end_block.md create mode 100644 x/auction/spec/README.md delete mode 100644 x/auction/types/auctions_test.go create mode 100644 x/auction/types/params_test.go rename x/auction/types/{quierier.go => querier.go} (100%) delete mode 100644 x/auction/types/utils.go diff --git a/app/app.go b/app/app.go index 088e26a5..d8eeec15 100644 --- a/app/app.go +++ b/app/app.go @@ -61,7 +61,7 @@ var ( supply.AppModuleBasic{}, auction.AppModuleBasic{}, cdp.AppModuleBasic{}, - liquidator.AppModuleBasic{}, + //liquidator.AppModuleBasic{}, pricefeed.AppModuleBasic{}, ) @@ -74,6 +74,8 @@ var ( staking.NotBondedPoolName: {supply.Burner, supply.Staking}, gov.ModuleName: {supply.Burner}, validatorvesting.ModuleName: {supply.Burner}, + auction.ModuleName: nil, + liquidator.ModuleName: {supply.Minter, supply.Burner}, } ) @@ -151,7 +153,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, crisisSubspace := app.paramsKeeper.Subspace(crisis.DefaultParamspace) auctionSubspace := app.paramsKeeper.Subspace(auction.DefaultParamspace) cdpSubspace := app.paramsKeeper.Subspace(cdp.DefaultParamspace) - liquidatorSubspace := app.paramsKeeper.Subspace(liquidator.DefaultParamspace) + //liquidatorSubspace := app.paramsKeeper.Subspace(liquidator.DefaultParamspace) pricefeedSubspace := app.paramsKeeper.Subspace(pricefeed.DefaultParamspace) // add keepers @@ -237,16 +239,16 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, app.bankKeeper) app.auctionKeeper = auction.NewKeeper( app.cdc, - app.cdpKeeper, // CDP keeper standing in for bank keys[auction.StoreKey], + app.supplyKeeper, auctionSubspace) - app.liquidatorKeeper = liquidator.NewKeeper( - app.cdc, - keys[liquidator.StoreKey], - liquidatorSubspace, - app.cdpKeeper, - app.auctionKeeper, - app.cdpKeeper) // CDP keeper standing in for bank + // app.liquidatorKeeper = liquidator.NewKeeper( + // app.cdc, + // keys[liquidator.StoreKey], + // liquidatorSubspace, + // app.cdpKeeper, + // app.auctionKeeper, + // app.cdpKeeper) // CDP keeper standing in for bank // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks @@ -269,7 +271,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper), auction.NewAppModule(app.auctionKeeper), cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper), - liquidator.NewAppModule(app.liquidatorKeeper), + //liquidator.NewAppModule(app.liquidatorKeeper), pricefeed.NewAppModule(app.pricefeedKeeper), ) @@ -289,7 +291,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, auth.ModuleName, validatorvesting.ModuleName, distr.ModuleName, staking.ModuleName, bank.ModuleName, slashing.ModuleName, gov.ModuleName, mint.ModuleName, supply.ModuleName, crisis.ModuleName, genutil.ModuleName, - pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, liquidator.ModuleName, // TODO is this order ok? + pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, //liquidator.ModuleName, // TODO is this order ok? ) app.mm.RegisterInvariants(&app.crisisKeeper) diff --git a/app/test_common.go b/app/test_common.go index f6a99036..f25c1c23 100644 --- a/app/test_common.go +++ b/app/test_common.go @@ -97,8 +97,9 @@ func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) T } func (tApp TestApp) CheckBalance(t *testing.T, ctx sdk.Context, owner sdk.AccAddress, expectedCoins sdk.Coins) { - actualCoins := tApp.GetAccountKeeper().GetAccount(ctx, owner).GetCoins() - require.Equal(t, expectedCoins, actualCoins) + acc := tApp.GetAccountKeeper().GetAccount(ctx, owner) + require.NotNilf(t, acc, "account with address '%s' doesn't exist", owner) + require.Equal(t, expectedCoins, acc.GetCoins()) } // Create a new auth genesis state from some addresses and coins. The state is returned marshalled into a map. diff --git a/x/auction/abci.go b/x/auction/abci.go index 164b35b7..402c5c12 100644 --- a/x/auction/abci.go +++ b/x/auction/abci.go @@ -6,18 +6,8 @@ import ( // 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? - } + err := k.CloseExpiredAuctions(ctx) + if err != nil { + panic(err) } } diff --git a/x/auction/abci_test.go b/x/auction/abci_test.go index 38941aeb..4ee5cd13 100644 --- a/x/auction/abci_test.go +++ b/x/auction/abci_test.go @@ -4,40 +4,53 @@ 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/supply" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/auction" + "github.com/kava-labs/kava/x/liquidator" ) func TestKeeper_EndBlocker(t *testing.T) { // Setup - _, addrs := app.GeneratePrivKeyAddressPairs(1) - seller := addrs[0] + _, addrs := app.GeneratePrivKeyAddressPairs(2) + buyer := addrs[0] + returnAddrs := addrs[1:] + returnWeights := []sdk.Int{sdk.NewInt(1)} + sellerModName := liquidator.ModuleName tApp := app.NewTestApp() + sellerAcc := supply.NewEmptyModuleAccount(sellerModName) + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100)))) tApp.InitializeFromGenesisStates( - app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100))}), + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + sellerAcc, + }), ) ctx := tApp.NewContext(true, abci.Header{}) keeper := tApp.GetAuctionKeeper() - auctionID, err := keeper.StartForwardAuction(ctx, seller, c("token1", 20), c("token2", 0)) + auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights) require.NoError(t, err) + require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 30))) - // Run the endblocker, simulating a block height just before auction expiry - preExpiryHeight := ctx.BlockHeight() + int64(auction.DefaultMaxAuctionDuration) - 1 - auction.EndBlocker(ctx.WithBlockHeight(preExpiryHeight), keeper) + // Run the endblocker, simulating a block time 1ns before auction expiry + preExpiryTime := ctx.BlockTime().Add(auction.DefaultBidDuration - 1) + auction.EndBlocker(ctx.WithBlockTime(preExpiryTime), keeper) // Check auction has not been closed yet _, found := keeper.GetAuction(ctx, auctionID) require.True(t, found) - // Run the endblocker, simulating a block height just after auction expiry - expiryHeight := preExpiryHeight + 1 - auction.EndBlocker(ctx.WithBlockHeight(expiryHeight), keeper) + // Run the endblocker, simulating a block time equal to auction expiry + expiryTime := ctx.BlockTime().Add(auction.DefaultBidDuration) + auction.EndBlocker(ctx.WithBlockTime(expiryTime), keeper) // Check auction has been closed _, found = keeper.GetAuction(ctx, auctionID) @@ -46,3 +59,8 @@ func TestKeeper_EndBlocker(t *testing.T) { 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 NewAuthGenStateFromAccs(accounts authexported.GenesisAccounts) app.GenesisState { + authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts) + return app.GenesisState{auth.ModuleName: auth.ModuleCdc.MustMarshalJSON(authGenesis)} +} diff --git a/x/auction/alias.go b/x/auction/alias.go index 58040464..99b63c4f 100644 --- a/x/auction/alias.go +++ b/x/auction/alias.go @@ -16,51 +16,52 @@ const ( RouterKey = types.RouterKey DefaultParamspace = types.DefaultParamspace DefaultMaxAuctionDuration = types.DefaultMaxAuctionDuration - DefaultMaxBidDuration = types.DefaultMaxBidDuration - DefaultStartingAuctionID = types.DefaultStartingAuctionID + DefaultBidDuration = types.DefaultBidDuration 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 + NewSurplusAuction = types.NewSurplusAuction + NewDebtAuction = types.NewDebtAuction + NewCollateralAuction = types.NewCollateralAuction + NewWeightedAddresses = types.NewWeightedAddresses + RegisterCodec = types.RegisterCodec + NewGenesisState = types.NewGenesisState + DefaultGenesisState = types.DefaultGenesisState + ValidateGenesis = types.ValidateGenesis + GetAuctionKey = types.GetAuctionKey + GetAuctionByTimeKey = types.GetAuctionByTimeKey + Uint64FromBytes = types.Uint64FromBytes + Uint64ToBytes = types.Uint64ToBytes + NewMsgPlaceBid = types.NewMsgPlaceBid + NewParams = types.NewParams + DefaultParams = types.DefaultParams + ParamKeyTable = types.ParamKeyTable + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier // variable aliases - ModuleCdc = types.ModuleCdc - KeyAuctionBidDuration = types.KeyAuctionBidDuration - KeyAuctionDuration = types.KeyAuctionDuration - KeyAuctionStartingID = types.KeyAuctionStartingID + ModuleCdc = types.ModuleCdc + AuctionKeyPrefix = types.AuctionKeyPrefix + AuctionByTimeKeyPrefix = types.AuctionByTimeKeyPrefix + NextAuctionIDKey = types.NextAuctionIDKey + KeyAuctionBidDuration = types.KeyAuctionBidDuration + KeyAuctionDuration = types.KeyAuctionDuration ) 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 + Auction = types.Auction + BaseAuction = types.BaseAuction + SurplusAuction = types.SurplusAuction + DebtAuction = types.DebtAuction + CollateralAuction = types.CollateralAuction + WeightedAddresses = types.WeightedAddresses + SupplyKeeper = types.SupplyKeeper + Auctions = types.Auctions + GenesisState = types.GenesisState + MsgPlaceBid = types.MsgPlaceBid + Params = types.Params + QueryResAuctions = types.QueryResAuctions + Keeper = keeper.Keeper ) diff --git a/x/auction/client/cli/tx.go b/x/auction/client/cli/tx.go index c663658b..e705eb74 100644 --- a/x/auction/client/cli/tx.go +++ b/x/auction/client/cli/tx.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "strconv" "github.com/kava-labs/kava/x/auction/types" "github.com/spf13/cobra" @@ -32,31 +33,26 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { // GetCmdPlaceBid cli command for creating and modifying cdps. func GetCmdPlaceBid(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "placebid [AuctionID] [Bidder] [Bid] [Lot]", + Use: "placebid [auctionID] [amount]", Short: "place a bid on an auction", - Args: cobra.ExactArgs(4), + Args: cobra.MinimumNArgs(2), 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]) + id, err := strconv.ParseUint(args[0], 10, 64) if err != nil { fmt.Printf("invalid auction id - %s \n", string(args[0])) return err } - bid, err := sdk.ParseCoin(args[2]) + amt, err := sdk.ParseCoin(args[2]) if err != nil { - fmt.Printf("invalid bid amount - %s \n", string(args[2])) + fmt.Printf("invalid 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) + msg := types.NewMsgPlaceBid(id, cliCtx.GetFromAddress(), amt) err = msg.ValidateBasic() if err != nil { return err diff --git a/x/auction/client/rest/tx.go b/x/auction/client/rest/tx.go index 61c9e18f..910714ca 100644 --- a/x/auction/client/rest/tx.go +++ b/x/auction/client/rest/tx.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "net/http" + "strconv" "github.com/gorilla/mux" @@ -32,7 +33,7 @@ const ( 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") + fmt.Sprintf("/auction/bid/{%s}/{%s}/{%s}", restAuctionID, restBidder, restBid), bidHandlerFn(cliCtx)).Methods("PUT") } func bidHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { @@ -43,9 +44,8 @@ func bidHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { strAuctionID := vars[restAuctionID] bechBidder := vars[restBidder] strBid := vars[restBid] - strLot := vars[restLot] - auctionID, err := types.NewIDFromString(strAuctionID) + auctionID, err := strconv.ParseUint(strAuctionID, 10, 64) if err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return @@ -63,13 +63,7 @@ func bidHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return } - lot, err := sdk.ParseCoin(strLot) - if err != nil { - rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } - - msg := types.NewMsgPlaceBid(auctionID, bidder, bid, lot) + msg := types.NewMsgPlaceBid(auctionID, bidder, bid) if err := msg.ValidateBasic(); err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return diff --git a/x/auction/doc.go b/x/auction/doc.go deleted file mode 100644 index 79e8def7..00000000 --- a/x/auction/doc.go +++ /dev/null @@ -1,14 +0,0 @@ -/* -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 diff --git a/x/auction/genesis.go b/x/auction/genesis.go index 1b7a327c..b5fad1ab 100644 --- a/x/auction/genesis.go +++ b/x/auction/genesis.go @@ -4,9 +4,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// InitGenesis - initializes the store state from genesis data +// InitGenesis initializes the store state from genesis data. func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { - keeper.SetParams(ctx, data.AuctionParams) + keeper.SetNextAuctionID(ctx, data.NextAuctionID) + + keeper.SetParams(ctx, data.Params) for _, a := range data.Auctions { keeper.SetAuction(ctx, a) @@ -15,16 +17,18 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { // ExportGenesis returns a GenesisState for a given context and keeper. func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { + nextAuctionID, err := keeper.GetNextAuctionID(ctx) + if err != nil { + panic(err) + } + params := keeper.GetParams(ctx) - var genAuctions GenesisAuctions - iterator := keeper.GetAuctionIterator(ctx) + var genAuctions Auctions + keeper.IterateAuctions(ctx, func(a Auction) bool { + genAuctions = append(genAuctions, a) + return false + }) - for ; iterator.Valid(); iterator.Next() { - - auction := keeper.DecodeAuction(ctx, iterator.Value()) - genAuctions = append(genAuctions, auction) - - } - return NewGenesisState(params, genAuctions) + return NewGenesisState(nextAuctionID, params, genAuctions) } diff --git a/x/auction/handler.go b/x/auction/handler.go index ec238957..f9b7c137 100644 --- a/x/auction/handler.go +++ b/x/auction/handler.go @@ -21,7 +21,7 @@ func NewHandler(keeper Keeper) sdk.Handler { func handleMsgPlaceBid(ctx sdk.Context, keeper Keeper, msg MsgPlaceBid) sdk.Result { - err := keeper.PlaceBid(ctx, msg.AuctionID, msg.Bidder, msg.Bid, msg.Lot) + err := keeper.PlaceBid(ctx, msg.AuctionID, msg.Bidder, msg.Amount) if err != nil { return err.Result() } diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go new file mode 100644 index 00000000..cecbadd5 --- /dev/null +++ b/x/auction/keeper/auctions.go @@ -0,0 +1,386 @@ +package keeper + +import ( + "fmt" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/supply" + "github.com/kava-labs/kava/x/auction/types" +) + +// StartSurplusAuction starts a new surplus (forward) auction. +func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (uint64, sdk.Error) { + + auction := types.NewSurplusAuction( + seller, + lot, + bidDenom, + ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration)) + + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) + if err != nil { + return 0, err + } + + auctionID, err := k.StoreNewAuction(ctx, auction) + if err != nil { + return 0, err + } + return auctionID, nil +} + +// StartDebtAuction starts a new debt (reverse) auction. +func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin) (uint64, sdk.Error) { + + auction := types.NewDebtAuction( + buyer, + bid, + initialLot, + ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration)) + + // This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker. + macc := k.supplyKeeper.GetModuleAccount(ctx, buyer) + if !macc.HasPermission(supply.Minter) { + return 0, sdk.ErrInternal("module does not have minting permissions") + } + + auctionID, err := k.StoreNewAuction(ctx, auction) + if err != nil { + return 0, err + } + return auctionID, nil +} + +// StartCollateralAuction starts a new collateral (2-phase) auction. +func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int) (uint64, sdk.Error) { + + weightedAddresses, err := types.NewWeightedAddresses(lotReturnAddrs, lotReturnWeights) + if err != nil { + return 0, err + } + auction := types.NewCollateralAuction(seller, lot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), maxBid, weightedAddresses) + + err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) + if err != nil { + return 0, err + } + + auctionID, err := k.StoreNewAuction(ctx, auction) + if err != nil { + return 0, err + } + return auctionID, nil +} + +// PlaceBid places a bid on any auction. +func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddress, newAmount sdk.Coin) sdk.Error { + + auction, found := k.GetAuction(ctx, auctionID) + if !found { + return sdk.ErrInternal("auction doesn't exist") + } + + // validation common to all auctions + if ctx.BlockTime().After(auction.GetEndTime()) { + return sdk.ErrInternal("auction has closed") + } + + // move coins and return updated auction + var err sdk.Error + var updatedAuction types.Auction + switch a := auction.(type) { + case types.SurplusAuction: + if updatedAuction, err = k.PlaceBidSurplus(ctx, a, bidder, newAmount); err != nil { + return err + } + case types.DebtAuction: + if updatedAuction, err = k.PlaceBidDebt(ctx, a, bidder, newAmount); err != nil { + return err + } + case types.CollateralAuction: + if !a.IsReversePhase() { + updatedAuction, err = k.PlaceForwardBidCollateral(ctx, a, bidder, newAmount) + } else { + updatedAuction, err = k.PlaceReverseBidCollateral(ctx, a, bidder, newAmount) + } + if err != nil { + return err + } + default: + panic(fmt.Sprintf("unrecognized auction type: %T", auction)) + } + + k.SetAuction(ctx, updatedAuction) + return nil +} + +// PlaceBidSurplus places a forward bid on a surplus auction, moving coins and returning the updated auction. +func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.SurplusAuction, sdk.Error) { + // Validate new bid + if bid.Denom != a.Bid.Denom { + return a, sdk.ErrInternal("bid denom doesn't match auction") + } + if !a.Bid.IsLT(bid) { + return a, sdk.ErrInternal("bid not greater than last bid") + } + + // New bidder pays back old bidder + // Catch edge cases of a bidder replacing their own bid, and the amount being zero (sending zero coins produces meaningless send events). + if !bidder.Equals(a.Bidder) && !a.Bid.IsZero() { + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err + } + } + // Increase in bid is burned + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, a.Initiator, sdk.NewCoins(bid.Sub(a.Bid))) + if err != nil { + return a, err + } + err = k.supplyKeeper.BurnCoins(ctx, a.Initiator, sdk.NewCoins(bid.Sub(a.Bid))) + if err != nil { + return a, err + } + + // Update Auction + a.Bidder = bidder + a.Bid = bid + a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout + + return a, nil +} + +// PlaceForwardBidCollateral places a forward bid on a collateral auction, moving coins and returning the updated auction. +func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.CollateralAuction, sdk.Error) { + // Validate new bid + if bid.Denom != a.Bid.Denom { + return a, sdk.ErrInternal("bid denom doesn't match auction") + } + if a.IsReversePhase() { + return a, sdk.ErrInternal("auction is not in forward phase") + } + if !a.Bid.IsLT(bid) { + return a, sdk.ErrInternal("auction in forward phase, new bid not higher than last bid") + } + if a.MaxBid.IsLT(bid) { + return a, sdk.ErrInternal("bid higher than max bid") + } + + // New bidder pays back old bidder + // Catch edge cases of a bidder replacing their own bid, and the amount being zero (sending zero coins produces meaningless send events). + if !bidder.Equals(a.Bidder) && !a.Bid.IsZero() { + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err + } + } + // Increase in bid sent to auction initiator + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, a.Initiator, sdk.NewCoins(bid.Sub(a.Bid))) + if err != nil { + return a, err + } + + // Update Auction + a.Bidder = bidder + a.Bid = bid + a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout + + return a, nil +} + +// PlaceReverseBidCollateral places a reverse bid on a collateral auction, moving coins and returning the updated auction. +func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.CollateralAuction, sdk.Error) { + // Validate new bid + if lot.Denom != a.Lot.Denom { + return a, sdk.ErrInternal("lot denom doesn't match auction") + } + if !a.IsReversePhase() { + return a, sdk.ErrInternal("auction not in reverse phase") + } + if lot.IsNegative() { + return a, sdk.ErrInternal("can't bid negative amount") + } + if !lot.IsLT(a.Lot) { + return a, sdk.ErrInternal("auction in reverse phase, new bid not less than previous amount") + } + + // New bidder pays back old bidder + // Catch edge cases of a bidder replacing their own bid + if !bidder.Equals(a.Bidder) { + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err + } + } + // Decrease in lot is sent to weighted addresses (normally the CDP depositors) + // TODO paying out rateably to cdp depositors is vulnerable to errors compounding over multiple bids - check this can't be gamed. + lotPayouts, err := splitCoinIntoWeightedBuckets(a.Lot.Sub(lot), a.LotReturns.Weights) + if err != nil { + return a, err + } + for i, payout := range lotPayouts { + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.LotReturns.Addresses[i], sdk.NewCoins(payout)) + if err != nil { + return a, err + } + } + + // Update Auction + a.Bidder = bidder + a.Lot = lot + a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout + + return a, nil +} + +// PlaceBidDebt places a reverse bid on a debt auction, moving coins and returning the updated auction. +func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.DebtAuction, sdk.Error) { + // Validate new bid + if lot.Denom != a.Lot.Denom { + return a, sdk.ErrInternal("lot denom doesn't match auction") + } + if lot.IsNegative() { + return a, sdk.ErrInternal("lot less than 0") + } + if !lot.IsLT(a.Lot) { + return a, sdk.ErrInternal("lot not smaller than last lot") + } + + // New bidder pays back old bidder + // Catch edge cases of a bidder replacing their own bid + if !bidder.Equals(a.Bidder) { + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid)) + if err != nil { + return a, err + } + } + + // Update Auction + a.Bidder = bidder + a.Lot = lot + a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout + + return a, nil +} + +// CloseAuction closes an auction and distributes funds to the highest bidder. +func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error { + + auction, found := k.GetAuction(ctx, auctionID) + if !found { + return sdk.ErrInternal("auction doesn't exist") + } + + if ctx.BlockTime().Before(auction.GetEndTime()) { + return sdk.ErrInternal(fmt.Sprintf("auction can't be closed as curent block time (%v) is under auction end time (%v)", ctx.BlockTime(), auction.GetEndTime())) + } + + // payout to the last bidder + switch auc := auction.(type) { + case types.SurplusAuction: + if err := k.PayoutSurplusAuction(ctx, auc); err != nil { + return err + } + case types.DebtAuction: + if err := k.PayoutDebtAuction(ctx, auc); err != nil { + return err + } + case types.CollateralAuction: + if err := k.PayoutCollateralAuction(ctx, auc); err != nil { + return err + } + default: + panic("unrecognized auction type") + } + + k.DeleteAuction(ctx, auctionID) + return nil +} + +// PayoutDebtAuction pays out the proceeds for a debt auction, first minting the coins. +func (k Keeper) PayoutDebtAuction(ctx sdk.Context, a types.DebtAuction) sdk.Error { + err := k.supplyKeeper.MintCoins(ctx, a.Initiator, sdk.NewCoins(a.Lot)) + if err != nil { + return err + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, a.Initiator, a.Bidder, sdk.NewCoins(a.Lot)) + if err != nil { + return err + } + return nil +} + +// PayoutSurplusAuction pays out the proceeds for a surplus auction. +func (k Keeper) PayoutSurplusAuction(ctx sdk.Context, a types.SurplusAuction) sdk.Error { + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot)) + if err != nil { + return err + } + return nil +} + +// PayoutCollateralAuction pays out the proceeds for a collateral auction. +func (k Keeper) PayoutCollateralAuction(ctx sdk.Context, a types.CollateralAuction) sdk.Error { + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot)) + if err != nil { + return err + } + return nil +} + +// CloseExpiredAuctions finds all auctions that are past (or at) their ending times and closes them, paying out to the highest bidder. +func (k Keeper) CloseExpiredAuctions(ctx sdk.Context) sdk.Error { + var expiredAuctions []uint64 + k.IterateAuctionsByTime(ctx, ctx.BlockTime(), func(id uint64) bool { + expiredAuctions = append(expiredAuctions, id) + return false + }) + // Note: iteration and auction closing are in separate loops as db should not be modified during iteration // TODO is this correct? gov modifies during iteration + for _, id := range expiredAuctions { + if err := k.CloseAuction(ctx, id); err != nil { + return err + } + } + return nil +} + +// earliestTime returns the earliest of two times. +func earliestTime(t1, t2 time.Time) time.Time { + if t1.Before(t2) { + return t1 + } else { + return t2 // also returned if times are equal + } +} + +// splitCoinIntoWeightedBuckets divides up some amount of coins according to some weights. +func splitCoinIntoWeightedBuckets(coin sdk.Coin, buckets []sdk.Int) ([]sdk.Coin, sdk.Error) { + for _, bucket := range buckets { + if bucket.IsNegative() { + return nil, sdk.ErrInternal("cannot split coin into bucket with negative weight") + } + } + amounts := splitIntIntoWeightedBuckets(coin.Amount, buckets) + result := make([]sdk.Coin, len(amounts)) + for i, a := range amounts { + result[i] = sdk.NewCoin(coin.Denom, a) + } + return result, nil +} diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go new file mode 100644 index 00000000..197e1c66 --- /dev/null +++ b/x/auction/keeper/auctions_test.go @@ -0,0 +1,246 @@ +package keeper_test + +import ( + "testing" + "time" + + 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/supply" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/auction/types" + "github.com/kava-labs/kava/x/liquidator" +) + +func TestSurplusAuctionBasic(t *testing.T) { + // Setup + _, addrs := app.GeneratePrivKeyAddressPairs(1) + buyer := addrs[0] + sellerModName := liquidator.ModuleName + sellerAddr := supply.NewModuleAddress(sellerModName) + + tApp := app.NewTestApp() + + sellerAcc := supply.NewEmptyModuleAccount(sellerModName, supply.Burner) // forward auctions burn proceeds + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100)))) + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + sellerAcc, + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Create an auction (lot: 20 token1, initialBid: 0 token2) + auctionID, err := keeper.StartSurplusAuction(ctx, sellerModName, c("token1", 20), "token2") // lot, bid denom + require.NoError(t, err) + // Check seller's coins have decreased + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) + + // PlaceBid (bid: 10 token, lot: same as starting) + require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 10))) + // Check buyer's coins have decreased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) + // Check seller's coins have not increased (because proceeds are burned) + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) + + // increment bid same bidder + err = keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 20)) + require.NoError(t, err) + + // Close auction at just at auction expiry time + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration)) + require.NoError(t, keeper.CloseAuction(ctx, auctionID)) + // Check buyer's coins increased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 80))) +} + +func TestDebtAuctionBasic(t *testing.T) { + // Setup + _, addrs := app.GeneratePrivKeyAddressPairs(1) + seller := addrs[0] + buyerModName := liquidator.ModuleName + buyerAddr := supply.NewModuleAddress(buyerModName) + + tApp := app.NewTestApp() + + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(seller, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + supply.NewEmptyModuleAccount(buyerModName, supply.Minter), // reverse auctions mint payout + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Start auction + auctionID, err := keeper.StartDebtAuction(ctx, buyerModName, c("token1", 20), c("token2", 99999)) // buyer, bid, initialLot + require.NoError(t, err) + // Check buyer's coins have not decreased, as lot is minted at the end + tApp.CheckBalance(t, ctx, buyerAddr, nil) // zero coins + + // Place a bid + require.NoError(t, keeper.PlaceBid(ctx, 0, seller, c("token2", 10))) + // Check seller's coins have decreased + tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) + // Check buyer's coins have increased + tApp.CheckBalance(t, ctx, buyerAddr, cs(c("token1", 20))) + + // Close auction at just after auction expiry + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration)) + require.NoError(t, keeper.CloseAuction(ctx, auctionID)) + // Check seller's coins increased + tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) +} + +func TestCollateralAuctionBasic(t *testing.T) { + // Setup + _, addrs := app.GeneratePrivKeyAddressPairs(4) + buyer := addrs[0] + returnAddrs := addrs[1:] + returnWeights := is(30, 20, 10) + sellerModName := liquidator.ModuleName + sellerAddr := supply.NewModuleAddress(sellerModName) + + tApp := app.NewTestApp() + sellerAcc := supply.NewEmptyModuleAccount(sellerModName) + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100)))) + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + auth.NewBaseAccount(returnAddrs[0], cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + auth.NewBaseAccount(returnAddrs[1], cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + auth.NewBaseAccount(returnAddrs[2], cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + sellerAcc, + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Start auction + auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights) // seller, lot, maxBid, otherPerson + require.NoError(t, err) + // Check seller's coins have decreased + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) + + // Place a forward bid + require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 10))) + // Check bidder's coins have decreased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) + // Check seller's coins have increased + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 110))) + // Check return addresses have not received coins + for _, ra := range returnAddrs { + tApp.CheckBalance(t, ctx, ra, cs(c("token1", 100), c("token2", 100))) + } + + // Place a reverse bid + require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50))) // first bid up to max bid to switch phases + require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token1", 15))) + // Check bidder's coins have decreased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50))) + // Check seller's coins have increased + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 150))) + // Check return addresses have received coins + tApp.CheckBalance(t, ctx, returnAddrs[0], cs(c("token1", 102), c("token2", 100))) + tApp.CheckBalance(t, ctx, returnAddrs[1], cs(c("token1", 102), c("token2", 100))) + tApp.CheckBalance(t, ctx, returnAddrs[2], cs(c("token1", 101), c("token2", 100))) + + // Close auction at just after auction expiry + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration)) + require.NoError(t, keeper.CloseAuction(ctx, auctionID)) + // Check buyer's coins increased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 115), c("token2", 50))) +} + +func TestStartSurplusAuction(t *testing.T) { + someTime := time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC) + type args struct { + seller string + lot sdk.Coin + bidDenom string + } + testCases := []struct { + name string + blockTime time.Time + args args + expectPass bool + }{ + { + "normal", + someTime, + args{liquidator.ModuleName, c("stable", 10), "gov"}, + true, + }, + { + "no module account", + someTime, + args{"nonExistentModule", c("stable", 10), "gov"}, + false, + }, + { + "not enough coins", + someTime, + args{liquidator.ModuleName, c("stable", 101), "gov"}, + false, + }, + { + "incorrect denom", + someTime, + args{liquidator.ModuleName, c("notacoin", 10), "gov"}, + false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // setup + initialLiquidatorCoins := cs(c("stable", 100)) + tApp := app.NewTestApp() + + liqAcc := supply.NewEmptyModuleAccount(liquidator.ModuleName, supply.Burner) + require.NoError(t, liqAcc.SetCoins(initialLiquidatorCoins)) + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{liqAcc}), + ) + ctx := tApp.NewContext(false, abci.Header{}).WithBlockTime(tc.blockTime) + keeper := tApp.GetAuctionKeeper() + + // run function under test + id, err := keeper.StartSurplusAuction(ctx, tc.args.seller, tc.args.lot, tc.args.bidDenom) + + // check + sk := tApp.GetSupplyKeeper() + liquidatorCoins := sk.GetModuleAccount(ctx, liquidator.ModuleName).GetCoins() + actualAuc, found := keeper.GetAuction(ctx, id) + + if tc.expectPass { + require.NoError(t, err) + // check coins moved + require.Equal(t, initialLiquidatorCoins.Sub(cs(tc.args.lot)), liquidatorCoins) + // check auction in store and is correct + require.True(t, found) + expectedAuction := types.Auction(types.SurplusAuction{BaseAuction: types.BaseAuction{ + ID: 0, + Initiator: tc.args.seller, + Lot: tc.args.lot, + Bidder: nil, + Bid: c(tc.args.bidDenom, 0), + EndTime: tc.blockTime.Add(types.DefaultMaxAuctionDuration), + MaxEndTime: tc.blockTime.Add(types.DefaultMaxAuctionDuration), + }}) + require.Equal(t, expectedAuction, actualAuc) + } else { + require.Error(t, err) + // check coins not moved + require.Equal(t, initialLiquidatorCoins, liquidatorCoins) + // check auction not in store + require.False(t, found) + } + }) + } +} diff --git a/x/auction/keeper/integration_test.go b/x/auction/keeper/integration_test.go new file mode 100644 index 00000000..38d9e4ce --- /dev/null +++ b/x/auction/keeper/integration_test.go @@ -0,0 +1,24 @@ +package keeper_test + +import ( + 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/kava-labs/kava/app" +) + +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 i(n int64) sdk.Int { return sdk.NewInt(n) } +func is(ns ...int64) (is []sdk.Int) { + for _, n := range ns { + is = append(is, sdk.NewInt(n)) + } + return +} + +func NewAuthGenStateFromAccs(accounts authexported.GenesisAccounts) app.GenesisState { + authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts) + return app.GenesisState{auth.ModuleName: auth.ModuleCdc.MustMarshalJSON(authGenesis)} +} diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index f43f81cc..cd0b01c2 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -1,314 +1,167 @@ package keeper import ( - "bytes" "fmt" + "time" "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/params/subspace" + "github.com/tendermint/tendermint/libs/log" + "github.com/kava-labs/kava/x/auction/types" ) type Keeper struct { - bankKeeper types.BankKeeper + supplyKeeper types.SupplyKeeper 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 { +func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, supplyKeeper types.SupplyKeeper, paramstore subspace.Subspace) Keeper { return Keeper{ - bankKeeper: bankKeeper, + supplyKeeper: supplyKeeper, 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 +// Logger returns a module-specific logger. +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) } -// 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 +// SetNextAuctionID stores an ID to be used for the next created auction +func (k Keeper) SetNextAuctionID(ctx sdk.Context, id uint64) { + store := ctx.KVStore(k.storeKey) + store.Set(types.NextAuctionIDKey, types.Uint64ToBytes(id)) } -// 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 +// GetNextAuctionID reads the next available global ID from store +func (k Keeper) GetNextAuctionID(ctx sdk.Context) (uint64, sdk.Error) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.NextAuctionIDKey) + if bz == nil { + return 0, sdk.ErrInternal("initial auction ID hasn't been set") } - return auctionID, nil + return types.Uint64FromBytes(bz), 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) +// IncrementNextAuctionID increments the next auction ID in the store by 1. +func (k Keeper) IncrementNextAuctionID(ctx sdk.Context) sdk.Error { + id, err := k.GetNextAuctionID(ctx) + if err != nil { + return err + } + k.SetNextAuctionID(ctx, id+1) + return nil +} + +// StoreNewAuction stores an auction, adding a new ID +func (k Keeper) StoreNewAuction(ctx sdk.Context, auction types.Auction) (uint64, sdk.Error) { + newAuctionID, err := k.GetNextAuctionID(ctx) if err != nil { return 0, err } - // set ID - auction.SetID(newAuctionID) + auction = auction.WithID(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) + + err = k.IncrementNextAuctionID(ctx) + if err != nil { + return 0, err + } 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 +// SetAuction puts the auction into the store, and updates any indexes. func (k Keeper) SetAuction(ctx sdk.Context, auction types.Auction) { - // remove the auction from the queue if it is already in there + // remove the auction from the byTime index if it is already in there existingAuction, found := k.GetAuction(ctx, auction.GetID()) if found { - k.removeFromQueue(ctx, existingAuction.GetEndTime(), existingAuction.GetID()) + k.removeFromByTimeIndex(ctx, existingAuction.GetEndTime(), existingAuction.GetID()) } - // store auction - store := ctx.KVStore(k.storeKey) + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) bz := k.cdc.MustMarshalBinaryLengthPrefixed(auction) - store.Set(k.getAuctionKey(auction.GetID()), bz) + store.Set(types.GetAuctionKey(auction.GetID()), bz) - // add to the queue - k.InsertIntoQueue(ctx, auction.GetEndTime(), auction.GetID()) + k.InsertIntoByTimeIndex(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) { +// GetAuction gets an auction from the store. +func (k Keeper) GetAuction(ctx sdk.Context, auctionID uint64) (types.Auction, bool) { var auction types.Auction - store := ctx.KVStore(k.storeKey) - bz := store.Get(k.getAuctionKey(auctionID)) + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) + bz := store.Get(types.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 + return auction, false } 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 +// DeleteAuction removes an auction from the store, and any indexes. +func (k Keeper) DeleteAuction(ctx sdk.Context, auctionID uint64) { auction, found := k.GetAuction(ctx, auctionID) if found { - k.removeFromQueue(ctx, auction.GetEndTime(), auctionID) + k.removeFromByTimeIndex(ctx, auction.GetEndTime(), auctionID) } - // delete auction - store := ctx.KVStore(k.storeKey) - store.Delete(k.getAuctionKey(auctionID)) + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) + store.Delete(types.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)) +// InsertIntoByTimeIndex adds an auction ID and end time into the byTime index. +func (k Keeper) InsertIntoByTimeIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) { // TODO make private, and find way to make tests work + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) + store.Set(types.GetAuctionByTimeKey(endTime, auctionID), types.Uint64ToBytes(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, +// removeFromByTimeIndex removes an auction ID and end time from the byTime index. +func (k Keeper) removeFromByTimeIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) + store.Delete(types.GetAuctionByTimeKey(endTime, auctionID)) +} + +// IterateAuctionByTime provides an iterator over auctions ordered by auction.EndTime. +// For each auction cb will be callled. If cb returns true the iterator will close and stop. +func (k Keeper) IterateAuctionsByTime(ctx sdk.Context, inclusiveCutoffTime time.Time, cb func(auctionID uint64) (stop bool)) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) + iterator := store.Iterator( + nil, // start at the very start of the prefix store + sdk.PrefixEndBytes(sdk.FormatTimeBytes(inclusiveCutoffTime)), // include any keys with times equal to inclusiveCutoffTime ) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + + auctionID := types.Uint64FromBytes(iterator.Value()) + + if cb(auctionID) { + break + } + } } -// 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)) -} +// IterateAuctions provides an iterator over all stored auctions. +// For each auction, cb will be called. If cb returns true, the iterator will close and stop. +func (k Keeper) IterateAuctions(ctx sdk.Context, cb func(auction types.Auction) (stop bool)) { + iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix) -// 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) - ) -} + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var auction types.Auction + k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &auction) -// 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 + if cb(auction) { + break + } + } } diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go index 873dcb81..03adaeeb 100644 --- a/x/auction/keeper/keeper_test.go +++ b/x/auction/keeper/keeper_test.go @@ -2,142 +2,36 @@ package keeper_test import ( "testing" + "time" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" "github.com/kava-labs/kava/app" - "github.com/kava-labs/kava/x/auction/keeper" "github.com/kava-labs/kava/x/auction/types" ) -func TestKeeper_ForwardAuction(t *testing.T) { - // Setup - _, addrs := app.GeneratePrivKeyAddressPairs(2) - seller := addrs[0] - buyer := addrs[1] - - tApp := app.NewTestApp() - tApp.InitializeFromGenesisStates( - app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}), - ) - - ctx := tApp.NewContext(false, abci.Header{}) - keeper := tApp.GetAuctionKeeper() - - // Create an auction (lot: 20 t1, initialBid: 0 t2) - auctionID, err := keeper.StartForwardAuction(ctx, seller, c("token1", 20), c("token2", 0)) // lot, initialBid - require.NoError(t, err) - // Check seller's coins have decreased - tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) - - // PlaceBid (bid: 10 t2, lot: same as starting) - require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 10), c("token1", 20))) // bid, lot - // Check buyer's coins have decreased - tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) - // Check seller's coins have increased - tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) - - // Close auction at just after auction expiry - ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration)) - require.NoError(t, keeper.CloseAuction(ctx, auctionID)) - // Check buyer's coins increased - tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90))) -} - -func TestKeeper_ReverseAuction(t *testing.T) { - // Setup - _, addrs := app.GeneratePrivKeyAddressPairs(2) - seller := addrs[0] - buyer := addrs[1] - - tApp := app.NewTestApp() - tApp.InitializeFromGenesisStates( - app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}), - ) - - ctx := tApp.NewContext(false, abci.Header{}) - keeper := tApp.GetAuctionKeeper() - - // Start auction - auctionID, err := keeper.StartReverseAuction(ctx, buyer, c("token1", 20), c("token2", 99)) // buyer, bid, initialLot - require.NoError(t, err) - // Check buyer's coins have decreased - tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 1))) - - // Place a bid - require.NoError(t, keeper.PlaceBid(ctx, 0, seller, c("token1", 20), c("token2", 10))) // bid, lot - // Check seller's coins have decreased - tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) - // Check buyer's coins have increased - tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90))) - - // Close auction at just after auction expiry - ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration)) - require.NoError(t, keeper.CloseAuction(ctx, auctionID)) - // Check seller's coins increased - tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) -} - -func TestKeeper_ForwardReverseAuction(t *testing.T) { - // Setup - _, addrs := app.GeneratePrivKeyAddressPairs(3) - seller := addrs[0] - buyer := addrs[1] - recipient := addrs[2] - - tApp := app.NewTestApp() - tApp.InitializeFromGenesisStates( - app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}), - ) - - ctx := tApp.NewContext(false, abci.Header{}) - keeper := tApp.GetAuctionKeeper() - - // Start auction - auctionID, err := keeper.StartForwardReverseAuction(ctx, seller, c("token1", 20), c("token2", 50), recipient) // seller, lot, maxBid, otherPerson - require.NoError(t, err) - // Check seller's coins have decreased - tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) - - // Place a bid - require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50), c("token1", 15))) // bid, lot - // Check bidder's coins have decreased - tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50))) - // Check seller's coins have increased - tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 150))) - // Check "recipient" has received coins - tApp.CheckBalance(t, ctx, recipient, cs(c("token1", 105), c("token2", 100))) - - // Close auction at just after auction expiry - ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration)) - require.NoError(t, keeper.CloseAuction(ctx, auctionID)) - // Check buyer's coins increased - tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 115), c("token2", 50))) -} - -func TestKeeper_SetGetDeleteAuction(t *testing.T) { +func SetGetDeleteAuction(t *testing.T) { // setup keeper, create auction - _, addrs := app.GeneratePrivKeyAddressPairs(1) tApp := app.NewTestApp() keeper := tApp.GetAuctionKeeper() ctx := tApp.NewContext(true, abci.Header{}) - auction, _ := types.NewForwardAuction(addrs[0], c("usdx", 100), c("kava", 0), types.EndTime(1000)) - id := types.ID(5) - auction.SetID(id) + someTime := time.Date(43, time.January, 1, 0, 0, 0, 0, time.UTC) // need to specify UTC as tz info is lost on unmarshal + var id uint64 = 5 + auction := types.NewSurplusAuction("some_module", c("usdx", 100), "kava", someTime).WithID(id) // write and read from store - keeper.SetAuction(ctx, &auction) + keeper.SetAuction(ctx, auction) readAuction, found := keeper.GetAuction(ctx, id) // check before and after match require.True(t, found) - require.Equal(t, &auction, readAuction) - // check auction is in queue - iter := keeper.GetQueueIterator(ctx, 100000) - require.Equal(t, 1, len(convertIteratorToSlice(keeper, iter))) - iter.Close() + require.Equal(t, auction, readAuction) + // check auction is in the index + keeper.IterateAuctionsByTime(ctx, auction.GetEndTime(), func(readID uint64) bool { + require.Equal(t, auction.GetID(), readID) + return false + }) // delete auction keeper.DeleteAuction(ctx, id) @@ -145,53 +39,97 @@ func TestKeeper_SetGetDeleteAuction(t *testing.T) { // 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() - + // check auction not in index + keeper.IterateAuctionsByTime(ctx, time.Unix(999999999, 0), func(readID uint64) bool { + require.Fail(t, "index should be empty", " found auction ID '%s", readID) + return false + }) } -// TODO convert to table driven test with more test cases -func TestKeeper_ExpiredAuctionQueue(t *testing.T) { +func TestIncrementNextAuctionID(t *testing.T) { // setup keeper tApp := app.NewTestApp() keeper := tApp.GetAuctionKeeper() ctx := tApp.NewContext(true, abci.Header{}) - // create an example queue - type queue []struct { - endTime types.EndTime - auctionID types.ID - } - q := queue{{1000, 0}, {1300, 2}, {5200, 1}} + // store id + var id uint64 = 123456 + keeper.SetNextAuctionID(ctx, id) - // write and read queue - for _, v := range q { - keeper.InsertIntoQueue(ctx, v.endTime, v.auctionID) - } - iter := keeper.GetQueueIterator(ctx, 1000) + require.NoError(t, keeper.IncrementNextAuctionID(ctx)) - // check before and after match - i := 0 - for ; iter.Valid(); iter.Next() { - var auctionID types.ID - tApp.Codec().MustUnmarshalBinaryLengthPrefixed(iter.Value(), &auctionID) - require.Equal(t, q[i].auctionID, auctionID) - i++ - } + // check id was incremented + readID, err := keeper.GetNextAuctionID(ctx) + require.NoError(t, err) + require.Equal(t, id+1, readID) } -func convertIteratorToSlice(keeper keeper.Keeper, iterator sdk.Iterator) []types.ID { - var queue []types.ID - for ; iterator.Valid(); iterator.Next() { - var auctionID types.ID - types.ModuleCdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &auctionID) - queue = append(queue, auctionID) +func TestIterateAuctions(t *testing.T) { + // setup + tApp := app.NewTestApp() + tApp.InitializeFromGenesisStates() + keeper := tApp.GetAuctionKeeper() + ctx := tApp.NewContext(true, abci.Header{}) + + auctions := []types.Auction{ + types.NewSurplusAuction("sellerMod", c("denom", 12345678), "anotherdenom", time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)).WithID(0), + types.NewDebtAuction("buyerMod", c("denom", 12345678), c("anotherdenom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)).WithID(1), + types.NewCollateralAuction("sellerMod", c("denom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC), c("anotherdenom", 12345678), types.WeightedAddresses{}).WithID(2), } - return queue + for _, a := range auctions { + keeper.SetAuction(ctx, a) + } + + // run + var readAuctions []types.Auction + keeper.IterateAuctions(ctx, func(a types.Auction) bool { + readAuctions = append(readAuctions, a) + return false + }) + + // check + require.Equal(t, auctions, readAuctions) } -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 TestIterateAuctionsByTime(t *testing.T) { + // setup keeper + tApp := app.NewTestApp() + keeper := tApp.GetAuctionKeeper() + ctx := tApp.NewContext(true, abci.Header{}) + + // setup byTime index + byTimeIndex := []struct { + endTime time.Time + auctionID uint64 + }{ + {time.Date(0, time.January, 1, 0, 0, 0, 0, time.UTC), 9999}, // distant past + {time.Date(1998, time.January, 1, 11, 59, 59, 999999999, time.UTC), 1}, // just before cutoff + {time.Date(1998, time.January, 1, 11, 59, 59, 999999999, time.UTC), 2}, // + {time.Date(1998, time.January, 1, 12, 0, 0, 0, time.UTC), 3}, // equal to cutoff + {time.Date(1998, time.January, 1, 12, 0, 0, 0, time.UTC), 4}, // + {time.Date(1998, time.January, 1, 12, 0, 0, 1, time.UTC), 5}, // just after cutoff + {time.Date(1998, time.January, 1, 12, 0, 0, 1, time.UTC), 6}, // + {time.Date(9999, time.January, 1, 0, 0, 0, 0, time.UTC), 0}, // distant future + } + for _, v := range byTimeIndex { + keeper.InsertIntoByTimeIndex(ctx, v.endTime, v.auctionID) + } + + // read out values from index up to a cutoff time and check they are as expected + cutoffTime := time.Date(1998, time.January, 1, 12, 0, 0, 0, time.UTC) + var expectedIndex []uint64 + for _, v := range byTimeIndex { + if v.endTime.Before(cutoffTime) || v.endTime.Equal(cutoffTime) { // endTime ≤ cutoffTime + expectedIndex = append(expectedIndex, v.auctionID) + } + + } + var readIndex []uint64 + keeper.IterateAuctionsByTime(ctx, cutoffTime, func(id uint64) bool { + readIndex = append(readIndex, id) + return false + }) + + require.Equal(t, expectedIndex, readIndex) +} diff --git a/x/auction/keeper/math.go b/x/auction/keeper/math.go new file mode 100644 index 00000000..437b4f22 --- /dev/null +++ b/x/auction/keeper/math.go @@ -0,0 +1,69 @@ +package keeper + +import ( + "sort" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// splitIntIntoWeightedBuckets divides an initial +ve integer among several buckets in proportion to the buckets' weights +// It uses the largest remainder method: +// https://en.wikipedia.org/wiki/Largest_remainder_method +// see also: https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100 +func splitIntIntoWeightedBuckets(amount sdk.Int, buckets []sdk.Int) []sdk.Int { + // TODO ideally change algorithm to work with -ve numbers. Limiting to +ve numbers until them + if amount.IsNegative() { + panic("negative amount") + } + for _, bucket := range buckets { + if bucket.IsNegative() { + panic("negative bucket") + } + } + + totalWeights := totalInts(buckets...) + + // split amount by weights, recording whole number part and remainder + quotients := make([]quoRem, len(buckets)) + for i := range buckets { + q := amount.Mul(buckets[i]).Quo(totalWeights) + r := amount.Mul(buckets[i]).Mod(totalWeights) + quotients[i] = quoRem{index: i, quo: q, rem: r} + } + + // apportion left over to buckets with the highest remainder (to minimize error) + sort.Slice(quotients, func(i, j int) bool { + return quotients[i].rem.GT(quotients[j].rem) // decreasing remainder order + }) + + allocated := sdk.ZeroInt() + for _, qr := range quotients { + allocated = allocated.Add(qr.quo) + } + leftToAllocate := amount.Sub(allocated) + + results := make([]sdk.Int, len(quotients)) + for _, qr := range quotients { + results[qr.index] = qr.quo + if !leftToAllocate.IsZero() { + results[qr.index] = results[qr.index].Add(sdk.OneInt()) + leftToAllocate = leftToAllocate.Sub(sdk.OneInt()) + } + } + return results +} + +type quoRem struct { + index int + quo sdk.Int + rem sdk.Int +} + +// totalInts adds together sdk.Ints +func totalInts(is ...sdk.Int) sdk.Int { + total := sdk.ZeroInt() + for _, i := range is { + total = total.Add(i) + } + return total +} diff --git a/x/auction/keeper/math_test.go b/x/auction/keeper/math_test.go new file mode 100644 index 00000000..ee42c594 --- /dev/null +++ b/x/auction/keeper/math_test.go @@ -0,0 +1,36 @@ +package keeper + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestSplitIntIntoWeightedBuckets(t *testing.T) { + testCases := []struct { + name string + amount sdk.Int + buckets []sdk.Int + want []sdk.Int + }{ + {"2split1,1", i(2), is(1, 1), is(1, 1)}, + {"100split1,9", i(100), is(1, 9), is(10, 90)}, + {"7split1,2", i(7), is(1, 2), is(2, 5)}, + {"17split1,1,1", i(17), is(1, 1, 1), is(6, 6, 5)}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := splitIntIntoWeightedBuckets(tc.amount, tc.buckets) + require.Equal(t, tc.want, got) + }) + } +} + +func i(n int64) sdk.Int { return sdk.NewInt(n) } +func is(ns ...int64) (is []sdk.Int) { + for _, n := range ns { + is = append(is, sdk.NewInt(n)) + } + return +} diff --git a/x/auction/keeper/params.go b/x/auction/keeper/params.go index c832caf5..6d7ddfd5 100644 --- a/x/auction/keeper/params.go +++ b/x/auction/keeper/params.go @@ -5,13 +5,11 @@ import ( "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) { +func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { k.paramSubspace.SetParamSet(ctx, ¶ms) } -// GetParams gets the auth module's parameters. -func (k Keeper) GetParams(ctx sdk.Context) (params types.AuctionParams) { +func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) { k.paramSubspace.GetParamSet(ctx, ¶ms) return -} \ No newline at end of file +} diff --git a/x/auction/keeper/querier.go b/x/auction/keeper/querier.go index 15748733..3b92476e 100644 --- a/x/auction/keeper/querier.go +++ b/x/auction/keeper/querier.go @@ -1,6 +1,7 @@ package keeper import ( + "fmt" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/x/auction/types" @@ -20,18 +21,14 @@ func NewQuerier(keeper Keeper) sdk.Querier { } func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { - var AuctionsList types.QueryResAuctions + var auctionsList types.QueryResAuctions - iterator := keeper.GetAuctionIterator(ctx) + keeper.IterateAuctions(ctx, func(a types.Auction) bool { + auctionsList = append(auctionsList, fmt.Sprintf("%+v", a)) // TODO formatting + return false + }) - 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) + bz, err2 := codec.MarshalJSONIndent(keeper.cdc, auctionsList) if err2 != nil { panic("could not marshal result to JSON") } diff --git a/x/auction/module.go b/x/auction/module.go index 7e0f1bd7..14c2d023 100644 --- a/x/auction/module.go +++ b/x/auction/module.go @@ -21,20 +21,20 @@ var ( _ module.AppModuleBasic = AppModuleBasic{} ) -// AppModuleBasic app module basics object +// AppModuleBasic implements the sdk.AppModuleBasic interface. type AppModuleBasic struct{} -// Name get module name +// Name returns the module name. func (AppModuleBasic) Name() string { return ModuleName } -// RegisterCodec register module codec +// RegisterCodec registers the module codec. func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { RegisterCodec(cdc) } -// DefaultGenesis default genesis state +// DefaultGenesis returns the default genesis state. func (AppModuleBasic) DefaultGenesis() json.RawMessage { return ModuleCdc.MustMarshalJSON(DefaultGenesisState()) } @@ -64,7 +64,7 @@ func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { return cli.GetQueryCmd(StoreKey, cdc) } -// AppModule app module type +// AppModule implements the sdk.AppModule interface. type AppModule struct { AppModuleBasic keeper Keeper @@ -78,11 +78,6 @@ func NewAppModule(keeper Keeper) AppModule { } } -// Name module name -func (AppModule) Name() string { - return ModuleName -} - // RegisterInvariants performs a no-op. func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} diff --git a/x/auction/spec/01_concepts.md b/x/auction/spec/01_concepts.md new file mode 100644 index 00000000..a7365d0c --- /dev/null +++ b/x/auction/spec/01_concepts.md @@ -0,0 +1,9 @@ +# Concepts + +Auctions are broken down into three distinct types, which correspond to three specific functionalities within the CDP system. + +* **Surplus Auction:** An auction in which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 they are willing to pay for the lot of c1. After the completion of a surplus auction, the winning bid of c2 is burned, and the bidder receives the lot of c1. As a concrete example, surplus auction are used to sell a fixed amount of USDX stable coins in exchange for increasing bids of KAVA governance tokens. The governance tokens are then burned and the winner receives USDX. +* **Debt Auction:** An auction in which a fixed amount of coins (c1) is bid for a decreasing lot of other coins (c2). Bidders decrement the lot of c2 they are willing to receive for the fixed amount of c1. As a concrete example, debt auctions are used to raise a certain amount of USDX stable coins in exchange for decreasing lots of KAVA governance tokens. The USDX tokens are used to recapitalize the cdp system and the winner receives KAVA. +* **Surplus Reverse Auction:** Are two phase auction is which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 until a specific `maxBid` is reached. Once `maxBid` is reached, a fixed amount of c2 is bid for a decreasing lot of c1. In the second phase, bidders decrement the lot of c1 they are willing to receive for a fixed amount of c2. As a concrete example, collateral auctions are used to sell collateral (ATOM, for example) for up to a `maxBid` amount of USDX. The USDX tokens are used to recapitalize the cdp system and the winner receives the specified lot of ATOM. In the event that the winning lot is smaller than the total lot, the excess ATOM is ratably returned to the original owners of the liquidated CDPs that were collateralized with that ATOM. + +Auctions are always initiated by another module, and not directly by users. Auctions start with an expiry, the time at which the auction is guaranteed to end, even if there have been no bidders. After each bid, the auction is extended by a specific amount of time, `BidDuration`. In the case that increasing the auction time by `BidDuration` would cause the auction to go past its expiry, the expiry is chosen as the ending time. diff --git a/x/auction/spec/02_state.md b/x/auction/spec/02_state.md new file mode 100644 index 00000000..306665f9 --- /dev/null +++ b/x/auction/spec/02_state.md @@ -0,0 +1,75 @@ +# State + +## Parameters and genesis state + +`Paramaters` define the rules according to which auctions are run. There is only one active parameter set at any given time. Updates to the parameter set can be made via on-chain parameter update proposals. + +```go +// Params governance parameters for auction module +type Params struct { + MaxAuctionDuration time.Duration `json:"max_auction_duration" yaml:"max_auction_duration"` // max length of auction + MaxBidDuration time.Duration `json:"max_bid_duration" yaml:"max_bid_duration"` // additional time added to the auction end time after each bid, capped by the expiry. +} +``` + +`GenesisState` defines the state that must be persisted when the blockchain stops/restarts in order for normal function of the auction module to resume. + +```go +// GenesisState - auction state that must be provided at genesis +type GenesisState struct { + NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` // auctionID that will be used for the next created auction + Params Params `json:"auction_params" yaml:"auction_params"` // auction params + Auctions Auctions `json:"genesis_auctions" yaml:"genesis_auctions"` // auctions currently in the store +} +``` + +## Base types + +```go +// Auction is an interface to several types of auction. +type Auction interface { + GetID() uint64 + WithID(uint64) Auction + GetEndTime() time.Time +} + +// BaseAuction is a common type shared by all Auctions. +type BaseAuction struct { + ID uint64 + Initiator string // Module name that starts the auction. Pays out Lot. + Lot sdk.Coin // Coins that will paid out by Initiator to the winning bidder. + Bidder sdk.AccAddress // Latest bidder. Receiver of Lot. + Bid sdk.Coin // Coins paid into the auction the bidder. + EndTime time.Time // Current auction closing time. Triggers at the end of the block with time ≥ EndTime. + MaxEndTime time.Time // Maximum closing time. Auctions can close before this but never after. +} + +// SurplusAuction is a forward auction that burns what it receives from bids. +// It is normally used to sell off excess pegged asset acquired by the CDP system. +type SurplusAuction struct { + BaseAuction +} + +// DebtAuction is a reverse auction that mints what it pays out. +// It is normally used to acquire pegged asset to cover the CDP system's debts that were not covered by selling collateral. +type DebtAuction struct { + BaseAuction +} + +// WeightedAddresses is a type for storing some addresses and associated weights. +type WeightedAddresses struct { + Addresses []sdk.AccAddress + Weights []sdk.Int +} + +// CollateralAuction is a two phase auction. +// Initially, in forward auction phase, bids can be placed up to a max bid. +// Then it switches to a reverse auction phase, where the initial amount up for auction is bid down. +// Unsold Lot is sent to LotReturns, being divided among the addresses by weight. +// Collateral auctions are normally used to sell off collateral seized from CDPs. +type CollateralAuction struct { + BaseAuction + MaxBid sdk.Coin + LotReturns WeightedAddresses +} +``` diff --git a/x/auction/spec/03_messages.md b/x/auction/spec/03_messages.md new file mode 100644 index 00000000..3a26a722 --- /dev/null +++ b/x/auction/spec/03_messages.md @@ -0,0 +1,32 @@ +# Messages + +## Bidding + +Users can bid on auctions using the `MsgPlaceBid` message type. All auction types can be bid on using the same message type. + +```go +// MsgPlaceBid is the message type used to place a bid on any type of auction. +type MsgPlaceBid struct { + AuctionID uint64 + Bidder sdk.AccAddress + Amount sdk.Coin +} +``` + +**State Modifications:** + +* Update bidder if different than previous bidder +* For Surplus auctions: + * Update Bid to msg.Amount + * Return bid coins to previous bidder + * Burn coins equal to the increment in the bid (CurrentBid - PreviousBid) +* For Debt auctions: + * Update Lot amount to msg.Amount + * Return bid coins to previous bidder +* For Collateral auctions: + * Return bid coins to previous bidder + * If in forward phase: + * Update Bid amount to msg.Amount + * If in reverse phase: + * Update Lot amount to msg.Amount +* Extend auction by `BidDuration`, up to `MaxEndTime` diff --git a/x/auction/spec/04_events.md b/x/auction/spec/04_events.md new file mode 100644 index 00000000..0c6b3238 --- /dev/null +++ b/x/auction/spec/04_events.md @@ -0,0 +1,5 @@ +# Events + + diff --git a/x/auction/spec/05_params.md b/x/auction/spec/05_params.md new file mode 100644 index 00000000..9a108303 --- /dev/null +++ b/x/auction/spec/05_params.md @@ -0,0 +1,8 @@ +# Parameters + +The auction module contains the following parameters: + +| Key | Type | Example | +| ------------------ | ---------------------- | -----------| +| MaxAuctionDuration | string (time.Duration) | "48h0m0s" | +| BidDuration | string (time.Duration) | "3h0m0s" | diff --git a/x/auction/spec/06_end_block.md b/x/auction/spec/06_end_block.md new file mode 100644 index 00000000..43694df9 --- /dev/null +++ b/x/auction/spec/06_end_block.md @@ -0,0 +1,18 @@ +# End Block + +At the end of each block, auctions that have reached `EndTime` are closed. The logic to close auctions is as follows: + +```go +var expiredAuctions []uint64 + k.IterateAuctionsByTime(ctx, ctx.BlockTime(), func(id uint64) bool { + expiredAuctions = append(expiredAuctions, id) + return false + }) + + for _, id := range expiredAuctions { + err := k.CloseAuction(ctx, id) + if err != nil { + panic(err) + } + } +``` diff --git a/x/auction/spec/README.md b/x/auction/spec/README.md new file mode 100644 index 00000000..32ae71d1 --- /dev/null +++ b/x/auction/spec/README.md @@ -0,0 +1,13 @@ +# `auction` + + +1. **[Concepts](01_concepts.md)** +2. **[State](02_state.md)** +3. **[Messages](03_messages.md)** +4. **[Events](04_events.md)** +5. **[Params](05_params.md)** +6. **[BeginBlock](06_begin_block.md)** + +## Abstract + +`x/auction` is an implementation of a Cosmos SDK Module that handles the creation, bidding, and payout of 3 distinct auction types. All auction types implement the `Auction` interface. Each auction type is used at different points during the normal functioning of the CDP system. diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 386fb232..1db5036e 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -2,100 +2,35 @@ package types import ( "fmt" - "strconv" + "time" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/supply" ) -// Auction is an interface to several types of auction. +// Auction is an interface for handling common actions on auctions. 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 + GetID() uint64 + WithID(uint64) Auction + GetEndTime() time.Time } -// BaseAuction type shared by all Auctions +// BaseAuction is a common 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 uint64 + Initiator string // Module name that starts the auction. Pays out Lot. + Lot sdk.Coin // Coins that will paid out by Initiator to the winning bidder. + Bidder sdk.AccAddress // Latest bidder. Receiver of Lot. + Bid sdk.Coin // Coins paid into the auction the bidder. + EndTime time.Time // Current auction closing time. Triggers at the end of the block with time ≥ EndTime. + MaxEndTime time.Time // Maximum closing time. Auctions can close before this but never after. } -// ID type for auction IDs -type ID uint64 +// GetID is a getter for auction ID. +func (a BaseAuction) GetID() uint64 { return a.ID } -// 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) -} +// GetEndTime is a getter for auction end time. +func (a BaseAuction) GetEndTime() time.Time { return a.EndTime } func (a BaseAuction) String() string { return fmt.Sprintf(`Auction %d: @@ -111,118 +46,76 @@ func (a BaseAuction) String() string { ) } -// NewBaseAuction creates a new base auction -func NewBaseAuction(seller sdk.AccAddress, lot sdk.Coin, initialBid sdk.Coin, EndTime EndTime) BaseAuction { - auction := BaseAuction{ +// SurplusAuction is a forward auction that burns what it receives from bids. +// It is normally used to sell off excess pegged asset acquired by the CDP system. +type SurplusAuction struct { + BaseAuction +} + +// WithID returns an auction with the ID set. +func (a SurplusAuction) WithID(id uint64) Auction { a.ID = id; return a } + +// NewSurplusAuction returns a new surplus auction. +func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime time.Time) SurplusAuction { + auction := SurplusAuction{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, - } + Bidder: nil, + Bid: sdk.NewInt64Coin(bidDenom, 0), + EndTime: endTime, + MaxEndTime: endTime, + }} return auction } -// ForwardAuction type for forward auctions -type ForwardAuction struct { +// DebtAuction is a reverse auction that mints what it pays out. +// It is normally used to acquire pegged asset to cover the CDP system's debts that were not covered by selling collateral. +type DebtAuction 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{ +// WithID returns an auction with the ID set. +func (a DebtAuction) WithID(id uint64) Auction { a.ID = id; return a } + +// NewDebtAuction returns a new debt auction. +func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime time.Time) DebtAuction { + // Note: Bidder is set to the initiator's module account address instead of module name. (when the first bid is placed, it is paid out to the initiator) + // Setting to the module account address bypasses calling supply.SendCoinsFromModuleToModule, instead calls SendCoinsFromModuleToAccount. + // This isn't a problem currently, but if additional logic/validation was added for sending to coins to Module Accounts, it would be bypassed. + auction := DebtAuction{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, + Initiator: buyerModAccName, 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 + Bidder: supply.NewModuleAddress(buyerModAccName), // send proceeds from the first bid to the buyer. + Bid: bid, // amount that the buyer is buying - doesn't change over course of auction EndTime: EndTime, MaxEndTime: EndTime, }} - output := BankOutput{buyer, initialLot} - return auction, output + return auction } -// 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 { +// CollateralAuction is a two phase auction. +// Initially, in forward auction phase, bids can be placed up to a max bid. +// Then it switches to a reverse auction phase, where the initial amount up for auction is bid down. +// Unsold Lot is sent to LotReturns, being divided among the addresses by weight. +// Collateral auctions are normally used to sell off collateral seized from CDPs. +type CollateralAuction struct { BaseAuction - MaxBid sdk.Coin - OtherPerson sdk.AccAddress // TODO rename, this is normally the original CDP owner + MaxBid sdk.Coin + LotReturns WeightedAddresses } -func (a ForwardReverseAuction) String() string { +// WithID returns an auction with the ID set. +func (a CollateralAuction) WithID(id uint64) Auction { a.ID = id; return a } + +// IsReversePhase returns whether the auction has switched over to reverse phase or not. +// Auction initially start in forward phase. +func (a CollateralAuction) IsReversePhase() bool { + return a.Bid.IsEqual(a.MaxBid) +} + +func (a CollateralAuction) String() string { return fmt.Sprintf(`Auction %d: Initiator: %s Lot: %s @@ -231,77 +124,48 @@ func (a ForwardReverseAuction) String() string { End Time: %s Max End Time: %s Max Bid %s - Other Person %s`, + LotReturns %s`, a.GetID(), a.Initiator, a.Lot, a.Bidder, a.Bid, a.GetEndTime().String(), - a.MaxEndTime.String(), a.MaxBid, a.OtherPerson, + a.MaxEndTime.String(), a.MaxBid, a.LotReturns, ) } -// 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{ +// NewCollateralAuction returns a new collateral auction. +func NewCollateralAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses) CollateralAuction { + auction := CollateralAuction{ 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 + Bidder: nil, + Bid: sdk.NewInt64Coin(maxBid.Denom, 0), EndTime: EndTime, MaxEndTime: EndTime}, - MaxBid: maxBid, - OtherPerson: otherPerson, + MaxBid: maxBid, + LotReturns: lotReturns, } - output := BankOutput{seller, lot} - return auction, output + return auction } -// 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 +// WeightedAddresses is a type for storing some addresses and associated weights. +type WeightedAddresses struct { + Addresses []sdk.AccAddress + Weights []sdk.Int +} + +// NewWeightedAddresses returns a new list addresses with weights. +func NewWeightedAddresses(addrs []sdk.AccAddress, weights []sdk.Int) (WeightedAddresses, sdk.Error) { + if len(addrs) != len(weights) { + return WeightedAddresses{}, sdk.ErrInternal("number of addresses doesn't match number of weights") + } + for _, w := range weights { + if w.IsNegative() { + return WeightedAddresses{}, sdk.ErrInternal("weights contain a negative amount") + } + } + return WeightedAddresses{ + Addresses: addrs, + Weights: weights, + }, nil } diff --git a/x/auction/types/auctions_test.go b/x/auction/types/auctions_test.go deleted file mode 100644 index b2252f55..00000000 --- a/x/auction/types/auctions_test.go +++ /dev/null @@ -1,403 +0,0 @@ -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) -} diff --git a/x/auction/types/codec.go b/x/auction/types/codec.go index 00e97c8b..1904f432 100644 --- a/x/auction/types/codec.go +++ b/x/auction/types/codec.go @@ -15,9 +15,8 @@ func init() { 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) + cdc.RegisterConcrete(SurplusAuction{}, "auction/SurplusAuction", nil) + cdc.RegisterConcrete(DebtAuction{}, "auction/DebtAuction", nil) + cdc.RegisterConcrete(CollateralAuction{}, "auction/CollateralAuction", nil) } diff --git a/x/auction/types/expected_keepers.go b/x/auction/types/expected_keepers.go index 5a99550d..021b8918 100644 --- a/x/auction/types/expected_keepers.go +++ b/x/auction/types/expected_keepers.go @@ -2,9 +2,17 @@ package types import ( sdk "github.com/cosmos/cosmos-sdk/types" + supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported" ) -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) +// SupplyKeeper defines the expected supply Keeper +type SupplyKeeper interface { + GetModuleAccount(ctx sdk.Context, moduleName string) supplyexported.ModuleAccountI + + SendCoinsFromModuleToModule(ctx sdk.Context, sender, recipient string, amt sdk.Coins) sdk.Error + SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) sdk.Error + SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) sdk.Error + + BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error + MintCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error } diff --git a/x/auction/types/genesis.go b/x/auction/types/genesis.go index 31c1efb5..808ac050 100644 --- a/x/auction/types/genesis.go +++ b/x/auction/types/genesis.go @@ -4,43 +4,45 @@ import ( "bytes" ) -// GenesisAuctions type for an array of auctions -type GenesisAuctions []Auction +// Auctions is a slice of auctions. +type Auctions []Auction -// GenesisState - auction state that must be provided at genesis +// GenesisState is auction state that must be provided at chain genesis. type GenesisState struct { - AuctionParams AuctionParams `json:"auction_params" yaml:"auction_params"` - Auctions GenesisAuctions `json:"genesis_auctions" yaml:"genesis_auctions"` + NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` + Params Params `json:"auction_params" yaml:"auction_params"` + Auctions Auctions `json:"genesis_auctions" yaml:"genesis_auctions"` } -// NewGenesisState returns a new genesis state object for auctions module -func NewGenesisState(ap AuctionParams, ga GenesisAuctions) GenesisState { +// NewGenesisState returns a new genesis state object for auctions module. +func NewGenesisState(nextID uint64, ap Params, ga Auctions) GenesisState { return GenesisState{ - AuctionParams: ap, + NextAuctionID: nextID, + Params: ap, Auctions: ga, } } -// DefaultGenesisState defines default genesis state for auction module +// DefaultGenesisState returns the default genesis state for auction module. func DefaultGenesisState() GenesisState { - return NewGenesisState(DefaultAuctionParams(), GenesisAuctions{}) + return NewGenesisState(0, DefaultParams(), Auctions{}) } -// Equal checks whether two GenesisState structs are equivalent +// 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 +// 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. +// ValidateGenesis validates genesis inputs. It returns error if validation of any input fails. func ValidateGenesis(data GenesisState) error { - if err := data.AuctionParams.Validate(); err != nil { + if err := data.Params.Validate(); err != nil { return err } return nil diff --git a/x/auction/types/keys.go b/x/auction/types/keys.go index cf2153b3..aca635a9 100644 --- a/x/auction/types/keys.go +++ b/x/auction/types/keys.go @@ -1,5 +1,12 @@ package types +import ( + "encoding/binary" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + const ( // ModuleName The name that will be used throughout the module ModuleName = "auction" @@ -13,3 +20,30 @@ const ( // DefaultParamspace default name for parameter store DefaultParamspace = ModuleName ) + +var ( + AuctionKeyPrefix = []byte{0x00} // prefix for keys that store auctions + AuctionByTimeKeyPrefix = []byte{0x01} // prefix for keys that are part of the auctionsByTime index + + NextAuctionIDKey = []byte{0x02} // key for the next auction id +) + +func GetAuctionKey(auctionID uint64) []byte { + return Uint64ToBytes(auctionID) +} + +func GetAuctionByTimeKey(endTime time.Time, auctionID uint64) []byte { + return append(sdk.FormatTimeBytes(endTime), Uint64ToBytes(auctionID)...) +} + +// Uint64ToBytes converts a uint64 into fixed length bytes for use in store keys. +func Uint64ToBytes(id uint64) []byte { + bz := make([]byte, 8) + binary.BigEndian.PutUint64(bz, uint64(id)) + return bz +} + +// Uint64FromBytes converts some fixed length bytes back into a uint64. +func Uint64FromBytes(bz []byte) uint64 { + return binary.BigEndian.Uint64(bz) +} diff --git a/x/auction/types/msg.go b/x/auction/types/msg.go index 5cdc60e3..976aedb1 100644 --- a/x/auction/types/msg.go +++ b/x/auction/types/msg.go @@ -2,42 +2,39 @@ package types import sdk "github.com/cosmos/cosmos-sdk/types" +// ensure Msg interface compliance at compile time +var _ sdk.Msg = &MsgPlaceBid{} + // 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 + AuctionID uint64 + Bidder sdk.AccAddress + Amount sdk.Coin // The new bid or lot to be set on the auction. } // NewMsgPlaceBid returns a new MsgPlaceBid. -func NewMsgPlaceBid(auctionID ID, bidder sdk.AccAddress, bid sdk.Coin, lot sdk.Coin) MsgPlaceBid { +func NewMsgPlaceBid(auctionID uint64, bidder sdk.AccAddress, amt sdk.Coin) MsgPlaceBid { return MsgPlaceBid{ AuctionID: auctionID, Bidder: bidder, - Bid: bid, - Lot: lot, + Amount: amt, } } // Route return the message type used for routing the message. -func (msg MsgPlaceBid) Route() string { return "auction" } +func (msg MsgPlaceBid) Route() string { return RouterKey } // 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. +// ValidateBasic does a simple validation check that doesn't require access to state. 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.Amount.IsValid() { + return sdk.ErrInternal("invalid bid amount") } - if msg.Lot.Amount.LT(sdk.ZeroInt()) { - return sdk.ErrInternal("invalid (negative) lot amount") - } - // TODO check coin denoms return nil } @@ -51,43 +48,3 @@ func (msg MsgPlaceBid) GetSignBytes() []byte { 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} -// } diff --git a/x/auction/types/msg_test.go b/x/auction/types/msg_test.go index cfb51b5c..f78e7102 100644 --- a/x/auction/types/msg_test.go +++ b/x/auction/types/msg_test.go @@ -14,19 +14,29 @@ func TestMsgPlaceBid_ValidateBasic(t *testing.T) { 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}, + {"normal", + NewMsgPlaceBid(0, addr, c("token", 10)), + true}, + {"emptyAddr", + NewMsgPlaceBid(0, sdk.AccAddress{}, c("token", 10)), + false}, + {"negativeAmount", + NewMsgPlaceBid(0, addr, sdk.Coin{Denom: "token", Amount: sdk.NewInt(-10)}), + false}, + {"zeroAmount", + NewMsgPlaceBid(0, addr, c("token", 0)), + true}, } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { if tc.expectPass { - require.Nil(t, tc.msg.ValidateBasic()) + require.NoError(t, tc.msg.ValidateBasic()) } else { - require.NotNil(t, tc.msg.ValidateBasic()) + require.Error(t, tc.msg.ValidateBasic()) } }) } } + +func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } diff --git a/x/auction/types/params.go b/x/auction/types/params.go index 527cd844..5f52f258 100644 --- a/x/auction/types/params.go +++ b/x/auction/types/params.go @@ -3,96 +3,89 @@ package types import ( "bytes" "fmt" + "time" + sdk "github.com/cosmos/cosmos-sdk/types" "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) + // DefaultMaxAuctionDuration max length of auction + DefaultMaxAuctionDuration time.Duration = 2 * 24 * time.Hour + // DefaultBidDuration how long an auction gets extended when someone bids + DefaultBidDuration time.Duration = 1 * time.Hour ) // Parameter keys var ( - // ParamStoreKeyAuctionParams Param store key for auction params - KeyAuctionBidDuration = []byte("MaxBidDuration") - KeyAuctionDuration = []byte("MaxAuctionDuration") - KeyAuctionStartingID = []byte("StartingAuctionID") + // ParamStoreKeyParams Param store key for auction params + KeyAuctionBidDuration = []byte("BidDuration") + KeyAuctionDuration = []byte("MaxAuctionDuration") ) -var _ subspace.ParamSet = &AuctionParams{} +var _ subspace.ParamSet = &Params{} -// 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"` +// Params is the governance parameters for the auction module. +type Params struct { + MaxAuctionDuration time.Duration `json:"max_auction_duration" yaml:"max_auction_duration"` // max length of auction + BidDuration time.Duration `json:"bid_duration" yaml:"bid_duration"` // additional time added to the auction end time after each bid, capped by the expiry. } -// NewAuctionParams creates a new AuctionParams object -func NewAuctionParams(maxAuctionDuration EndTime, bidDuration EndTime, startingID ID) AuctionParams { - return AuctionParams{ +// NewParams returns a new Params object. +func NewParams(maxAuctionDuration time.Duration, bidDuration time.Duration) Params { + return Params{ MaxAuctionDuration: maxAuctionDuration, - MaxBidDuration: bidDuration, - StartingAuctionID: startingID, + BidDuration: bidDuration, } } -// DefaultAuctionParams default parameters for auctions -func DefaultAuctionParams() AuctionParams { - return NewAuctionParams( +// DefaultParams returns the default parameters for auctions. +func DefaultParams() Params { + return NewParams( DefaultMaxAuctionDuration, - DefaultMaxBidDuration, - DefaultStartingAuctionID, + DefaultBidDuration, ) } // ParamKeyTable Key declaration for parameters func ParamKeyTable() subspace.KeyTable { - return subspace.NewKeyTable().RegisterParamSet(&AuctionParams{}) + return subspace.NewKeyTable().RegisterParamSet(&Params{}) } -// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs -// pairs of auth module's parameters. +// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs. // nolint -func (ap *AuctionParams) ParamSetPairs() subspace.ParamSetPairs { +func (p *Params) ParamSetPairs() subspace.ParamSetPairs { return subspace.ParamSetPairs{ - {KeyAuctionBidDuration, &ap.MaxBidDuration}, - {KeyAuctionDuration, &ap.MaxAuctionDuration}, - {KeyAuctionStartingID, &ap.StartingAuctionID}, + {KeyAuctionBidDuration, &p.BidDuration}, + {KeyAuctionDuration, &p.MaxAuctionDuration}, } } -// 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) +// Equal returns a boolean determining if two Params types are identical. +func (p Params) Equal(p2 Params) bool { + bz1 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&p) + bz2 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&p2) return bytes.Equal(bz1, bz2) } // String implements stringer interface -func (ap AuctionParams) String() string { +func (p Params) 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) + Bid Duration: %s`, p.MaxAuctionDuration, p.BidDuration) } // 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) +func (p Params) Validate() error { + if p.BidDuration < 0 { + return sdk.ErrInternal("bid duration cannot be negative") } - if ap.MaxBidDuration <= EndTime(0) { - return fmt.Errorf("bid duration should be positive, is %s", ap.MaxBidDuration) + if p.MaxAuctionDuration < 0 { + return sdk.ErrInternal("max auction duration cannot be negative") } - if ap.StartingAuctionID <= ID(0) { - return fmt.Errorf("starting auction ID should be positive, is %v", ap.StartingAuctionID) + if p.BidDuration > p.MaxAuctionDuration { + return sdk.ErrInternal("bid duration param cannot be larger than max auction duration") } return nil } diff --git a/x/auction/types/params_test.go b/x/auction/types/params_test.go new file mode 100644 index 00000000..0f642875 --- /dev/null +++ b/x/auction/types/params_test.go @@ -0,0 +1,38 @@ +package types + +import ( + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestParams_Validate(t *testing.T) { + type fields struct { + } + testCases := []struct { + name string + MaxAuctionDuration time.Duration + BidDuration time.Duration + expectErr bool + }{ + {"normal", 24 * time.Hour, 1 * time.Hour, false}, + {"negativeBid", 24 * time.Hour, -1 * time.Hour, true}, + {"negativeAuction", -24 * time.Hour, 1 * time.Hour, true}, + {"bid>auction", 1 * time.Hour, 24 * time.Hour, true}, + {"zeros", 0, 0, false}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + p := Params{ + MaxAuctionDuration: tc.MaxAuctionDuration, + BidDuration: tc.BidDuration, + } + err := p.Validate() + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/x/auction/types/quierier.go b/x/auction/types/querier.go similarity index 100% rename from x/auction/types/quierier.go rename to x/auction/types/querier.go diff --git a/x/auction/types/utils.go b/x/auction/types/utils.go deleted file mode 100644 index b1aad11e..00000000 --- a/x/auction/types/utils.go +++ /dev/null @@ -1,9 +0,0 @@ -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 -} diff --git a/x/liquidator/keeper/keeper.go b/x/liquidator/keeper/keeper.go index 59354270..5134d47e 100644 --- a/x/liquidator/keeper/keeper.go +++ b/x/liquidator/keeper/keeper.go @@ -5,7 +5,6 @@ import ( 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" ) @@ -33,7 +32,7 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, paramstore subspace.Subs // 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) { +func (k Keeper) SeizeAndStartCollateralAuction(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string) (uint64, sdk.Error) { // Get CDP cdp, found := k.cdpKeeper.GetCDP(ctx, owner, collateralDenom) if !found { @@ -73,7 +72,7 @@ func (k Keeper) SeizeAndStartCollateralAuction(ctx sdk.Context, owner sdk.AccAdd // 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) { +func (k Keeper) StartDebtAuction(ctx sdk.Context) (uint64, 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()) @@ -107,7 +106,7 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context) (auction.ID, sdk.Error) { // 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) { +// func (k Keeper) StartSurplusAuction(ctx sdk.Context) (uint64, sdk.Error) { // // TODO ensure seized debt is 0 diff --git a/x/liquidator/types/expected_keepers.go b/x/liquidator/types/expected_keepers.go index deea2bd4..39bede95 100644 --- a/x/liquidator/types/expected_keepers.go +++ b/x/liquidator/types/expected_keepers.go @@ -3,7 +3,6 @@ package types import ( sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/cdp" ) @@ -26,7 +25,7 @@ type BankKeeper interface { // 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) + StartForwardAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin) (uint64, sdk.Error) + StartReverseAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin) (uint64, sdk.Error) + StartForwardReverseAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin, sdk.AccAddress) (uint64, sdk.Error) } From d849d690e5bd23b4984ea6fb4583b095dd971892 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Sun, 12 Jan 2020 16:35:34 +0100 Subject: [PATCH 51/99] R4R: CDP types and methods (#275) * wip: tpyes and keeper methods * wip: iterators * wip: types and keeper methods * wip: add msgs * wip: client methods * wip: rebase develop * wip: types tests * wip: keeper tests, small fixes * wip: add cdp tests * wip: deposit tests * wip: keeper tests * wip: tests and module methods * feat: error when fetching expired price * feat: conversion factor for external assets * feat: debt floor for new cdps * feat: save deposits on export genesis * feat: ensure messages implement msg * feat: index deposits by status * fix: stray comment * wip: address review comments * address review comments --- .gitignore | 2 +- app/app.go | 51 +-- app/test_common.go | 2 - x/auction/abci_test.go | 4 +- x/auction/keeper/auctions_test.go | 18 +- x/cdp/abci.go | 25 ++ x/cdp/abci_test.go | 126 ++++++ x/cdp/alias.go | 176 ++++++-- x/cdp/app_test.go | 55 --- x/cdp/client/cli/query.go | 68 +-- x/cdp/client/cli/tx.go | 154 ++++++- x/cdp/client/module_client.go | 51 --- x/cdp/client/rest/query.go | 123 +++--- x/cdp/client/rest/rest.go | 42 ++ x/cdp/client/rest/tx.go | 136 ++++-- x/cdp/genesis.go | 73 +++- x/cdp/genesis_test.go | 61 +++ x/cdp/handler.go | 101 ++++- x/cdp/handler_test.go | 64 +++ x/cdp/integration_test.go | 232 ++++++++++- x/cdp/keeper/bank_test.go | 60 --- x/cdp/keeper/cdp.go | 421 +++++++++++++++++++ x/cdp/keeper/cdp_test.go | 299 +++++++++++++ x/cdp/keeper/deposit.go | 223 ++++++++++ x/cdp/keeper/deposit_test.go | 154 +++++++ x/cdp/keeper/draw.go | 216 ++++++++++ x/cdp/keeper/draw_test.go | 198 +++++++++ x/cdp/keeper/fees.go | 102 +++++ x/cdp/keeper/fees_test.go | 95 +++++ x/cdp/keeper/integration_test.go | 101 +++-- x/cdp/keeper/keeper.go | 530 +++--------------------- x/cdp/keeper/keeper_test.go | 316 -------------- x/cdp/keeper/params.go | 74 +++- x/cdp/keeper/querier.go | 93 +++-- x/cdp/keeper/querier_test.go | 226 ++++++++++ x/cdp/keeper/seize.go | 102 +++++ x/cdp/keeper/seize_test.go | 141 +++++++ x/cdp/module.go | 12 +- x/cdp/types/cdp.go | 64 +++ x/cdp/types/codec.go | 8 +- x/cdp/types/deposit.go | 83 ++++ x/cdp/types/errors.go | 103 +++++ x/cdp/types/events.go | 16 + x/cdp/types/expected_keepers.go | 22 +- x/cdp/types/genesis.go | 55 ++- x/cdp/types/genesis_test.go | 22 + x/cdp/types/keys.go | 164 +++++++- x/cdp/types/keys_test.go | 65 +++ x/cdp/types/msg.go | 313 +++++++++++++- x/cdp/types/msg_test.go | 166 ++++++++ x/cdp/types/params.go | 290 ++++++++----- x/cdp/types/querier.go | 55 ++- x/cdp/types/types.go | 77 ---- x/cdp/types/utils.go | 99 +++++ x/cdp/types/utils_test.go | 83 ++++ x/liquidator/alias.go | 52 --- x/liquidator/client/cli/query.go | 49 --- x/liquidator/client/cli/tx.go | 98 ----- x/liquidator/client/rest/query.go | 30 -- x/liquidator/client/rest/rest.go | 13 - x/liquidator/client/rest/tx.go | 73 ---- x/liquidator/doc.go | 21 - x/liquidator/genesis.go | 19 - x/liquidator/handler.go | 54 --- x/liquidator/keeper/integration_test.go | 72 ---- x/liquidator/keeper/keeper.go | 203 --------- x/liquidator/keeper/keeper_test.go | 118 ------ x/liquidator/keeper/params.go | 18 - x/liquidator/keeper/querier.go | 45 -- x/liquidator/module.go | 130 ------ x/liquidator/types/codec.go | 20 - x/liquidator/types/expected_keepers.go | 31 -- x/liquidator/types/genesis.go | 22 - x/liquidator/types/keys.go | 18 - x/liquidator/types/msg.go | 97 ----- x/liquidator/types/params.go | 100 ----- x/liquidator/types/querier.go | 22 - x/liquidator/types/types.go | 30 -- x/pricefeed/keeper/keeper.go | 55 +-- x/pricefeed/keeper/keeper_test.go | 6 +- x/pricefeed/keeper/querier.go | 6 +- 81 files changed, 5135 insertions(+), 2879 deletions(-) create mode 100644 x/cdp/abci.go create mode 100644 x/cdp/abci_test.go delete mode 100644 x/cdp/app_test.go delete mode 100644 x/cdp/client/module_client.go create mode 100644 x/cdp/genesis_test.go create mode 100644 x/cdp/handler_test.go delete mode 100644 x/cdp/keeper/bank_test.go create mode 100644 x/cdp/keeper/cdp.go create mode 100644 x/cdp/keeper/cdp_test.go create mode 100644 x/cdp/keeper/deposit.go create mode 100644 x/cdp/keeper/deposit_test.go create mode 100644 x/cdp/keeper/draw.go create mode 100644 x/cdp/keeper/draw_test.go create mode 100644 x/cdp/keeper/fees.go create mode 100644 x/cdp/keeper/fees_test.go delete mode 100644 x/cdp/keeper/keeper_test.go create mode 100644 x/cdp/keeper/querier_test.go create mode 100644 x/cdp/keeper/seize.go create mode 100644 x/cdp/keeper/seize_test.go create mode 100644 x/cdp/types/cdp.go create mode 100644 x/cdp/types/deposit.go create mode 100644 x/cdp/types/errors.go create mode 100644 x/cdp/types/events.go create mode 100644 x/cdp/types/genesis_test.go create mode 100644 x/cdp/types/keys_test.go create mode 100644 x/cdp/types/msg_test.go delete mode 100644 x/cdp/types/types.go create mode 100644 x/cdp/types/utils.go create mode 100644 x/cdp/types/utils_test.go delete mode 100644 x/liquidator/alias.go delete mode 100644 x/liquidator/client/cli/query.go delete mode 100644 x/liquidator/client/cli/tx.go delete mode 100644 x/liquidator/client/rest/query.go delete mode 100644 x/liquidator/client/rest/rest.go delete mode 100644 x/liquidator/client/rest/tx.go delete mode 100644 x/liquidator/doc.go delete mode 100644 x/liquidator/genesis.go delete mode 100644 x/liquidator/handler.go delete mode 100644 x/liquidator/keeper/integration_test.go delete mode 100644 x/liquidator/keeper/keeper.go delete mode 100644 x/liquidator/keeper/keeper_test.go delete mode 100644 x/liquidator/keeper/params.go delete mode 100644 x/liquidator/keeper/querier.go delete mode 100644 x/liquidator/module.go delete mode 100644 x/liquidator/types/codec.go delete mode 100644 x/liquidator/types/expected_keepers.go delete mode 100644 x/liquidator/types/genesis.go delete mode 100644 x/liquidator/types/keys.go delete mode 100644 x/liquidator/types/msg.go delete mode 100644 x/liquidator/types/params.go delete mode 100644 x/liquidator/types/querier.go delete mode 100644 x/liquidator/types/types.go diff --git a/.gitignore b/.gitignore index 71fbf7e6..ae38c8a5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ # Test binary, build with `go test -c` *.test -# Output of the go coverage tool, specifically when used with LiteIDE +# Output of the go coverage tool *.out # Exclude build files diff --git a/app/app.go b/app/app.go index d8eeec15..9e9a1ea0 100644 --- a/app/app.go +++ b/app/app.go @@ -6,7 +6,6 @@ import ( "github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/cdp" - "github.com/kava-labs/kava/x/liquidator" "github.com/kava-labs/kava/x/pricefeed" validatorvesting "github.com/kava-labs/kava/x/validator-vesting" @@ -61,7 +60,6 @@ var ( supply.AppModuleBasic{}, auction.AppModuleBasic{}, cdp.AppModuleBasic{}, - //liquidator.AppModuleBasic{}, pricefeed.AppModuleBasic{}, ) @@ -75,7 +73,8 @@ var ( gov.ModuleName: {supply.Burner}, validatorvesting.ModuleName: {supply.Burner}, auction.ModuleName: nil, - liquidator.ModuleName: {supply.Minter, supply.Burner}, + cdp.ModuleName: {supply.Minter, supply.Burner}, + cdp.LiquidatorMacc: {supply.Minter, supply.Burner}, } ) @@ -91,21 +90,20 @@ type App struct { tkeys map[string]*sdk.TransientStoreKey // keepers from all the modules - accountKeeper auth.AccountKeeper - bankKeeper bank.Keeper - supplyKeeper supply.Keeper - stakingKeeper staking.Keeper - slashingKeeper slashing.Keeper - mintKeeper mint.Keeper - distrKeeper distr.Keeper - govKeeper gov.Keeper - crisisKeeper crisis.Keeper - paramsKeeper params.Keeper - vvKeeper validatorvesting.Keeper - auctionKeeper auction.Keeper - cdpKeeper cdp.Keeper - liquidatorKeeper liquidator.Keeper - pricefeedKeeper pricefeed.Keeper + accountKeeper auth.AccountKeeper + bankKeeper bank.Keeper + supplyKeeper supply.Keeper + stakingKeeper staking.Keeper + slashingKeeper slashing.Keeper + mintKeeper mint.Keeper + distrKeeper distr.Keeper + govKeeper gov.Keeper + crisisKeeper crisis.Keeper + paramsKeeper params.Keeper + vvKeeper validatorvesting.Keeper + auctionKeeper auction.Keeper + cdpKeeper cdp.Keeper + pricefeedKeeper pricefeed.Keeper // the module manager mm *module.Manager @@ -129,7 +127,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, bam.MainStoreKey, auth.StoreKey, staking.StoreKey, supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey, gov.StoreKey, params.StoreKey, validatorvesting.StoreKey, - auction.StoreKey, cdp.StoreKey, liquidator.StoreKey, pricefeed.StoreKey, + auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey, ) tkeys := sdk.NewTransientStoreKeys(params.TStoreKey) @@ -153,7 +151,6 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, crisisSubspace := app.paramsKeeper.Subspace(crisis.DefaultParamspace) auctionSubspace := app.paramsKeeper.Subspace(auction.DefaultParamspace) cdpSubspace := app.paramsKeeper.Subspace(cdp.DefaultParamspace) - //liquidatorSubspace := app.paramsKeeper.Subspace(liquidator.DefaultParamspace) pricefeedSubspace := app.paramsKeeper.Subspace(pricefeed.DefaultParamspace) // add keepers @@ -231,24 +228,19 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, keys[pricefeed.StoreKey], pricefeedSubspace, pricefeed.DefaultCodespace) + // NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, sk types.SupplyKeeper, codespace sdk.CodespaceType) app.cdpKeeper = cdp.NewKeeper( app.cdc, keys[cdp.StoreKey], cdpSubspace, app.pricefeedKeeper, - app.bankKeeper) + app.supplyKeeper, + cdp.DefaultCodespace) app.auctionKeeper = auction.NewKeeper( app.cdc, keys[auction.StoreKey], app.supplyKeeper, auctionSubspace) - // app.liquidatorKeeper = liquidator.NewKeeper( - // app.cdc, - // keys[liquidator.StoreKey], - // liquidatorSubspace, - // app.cdpKeeper, - // app.auctionKeeper, - // app.cdpKeeper) // CDP keeper standing in for bank // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks @@ -271,7 +263,6 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper), auction.NewAppModule(app.auctionKeeper), cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper), - //liquidator.NewAppModule(app.liquidatorKeeper), pricefeed.NewAppModule(app.pricefeedKeeper), ) @@ -291,7 +282,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, auth.ModuleName, validatorvesting.ModuleName, distr.ModuleName, staking.ModuleName, bank.ModuleName, slashing.ModuleName, gov.ModuleName, mint.ModuleName, supply.ModuleName, crisis.ModuleName, genutil.ModuleName, - pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, //liquidator.ModuleName, // TODO is this order ok? + pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, // TODO is this order ok? ) app.mm.RegisterInvariants(&app.crisisKeeper) diff --git a/app/test_common.go b/app/test_common.go index f25c1c23..2b598bb5 100644 --- a/app/test_common.go +++ b/app/test_common.go @@ -29,7 +29,6 @@ import ( "github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/cdp" - "github.com/kava-labs/kava/x/liquidator" "github.com/kava-labs/kava/x/pricefeed" validatorvesting "github.com/kava-labs/kava/x/validator-vesting" ) @@ -67,7 +66,6 @@ func (tApp TestApp) GetParamsKeeper() params.Keeper { return tApp.params func (tApp TestApp) GetVVKeeper() validatorvesting.Keeper { return tApp.vvKeeper } func (tApp TestApp) GetAuctionKeeper() auction.Keeper { return tApp.auctionKeeper } func (tApp TestApp) GetCDPKeeper() cdp.Keeper { return tApp.cdpKeeper } -func (tApp TestApp) GetLiquidatorKeeper() liquidator.Keeper { return tApp.liquidatorKeeper } func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefeedKeeper } // This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states diff --git a/x/auction/abci_test.go b/x/auction/abci_test.go index 4ee5cd13..04a7efe7 100644 --- a/x/auction/abci_test.go +++ b/x/auction/abci_test.go @@ -12,7 +12,7 @@ import ( "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/auction" - "github.com/kava-labs/kava/x/liquidator" + "github.com/kava-labs/kava/x/cdp" ) func TestKeeper_EndBlocker(t *testing.T) { @@ -21,7 +21,7 @@ func TestKeeper_EndBlocker(t *testing.T) { buyer := addrs[0] returnAddrs := addrs[1:] returnWeights := []sdk.Int{sdk.NewInt(1)} - sellerModName := liquidator.ModuleName + sellerModName := cdp.LiquidatorMacc tApp := app.NewTestApp() sellerAcc := supply.NewEmptyModuleAccount(sellerModName) diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index 197e1c66..18b1971b 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -13,14 +13,14 @@ import ( "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/auction/types" - "github.com/kava-labs/kava/x/liquidator" + "github.com/kava-labs/kava/x/cdp" ) func TestSurplusAuctionBasic(t *testing.T) { // Setup _, addrs := app.GeneratePrivKeyAddressPairs(1) buyer := addrs[0] - sellerModName := liquidator.ModuleName + sellerModName := cdp.LiquidatorMacc sellerAddr := supply.NewModuleAddress(sellerModName) tApp := app.NewTestApp() @@ -64,7 +64,7 @@ func TestDebtAuctionBasic(t *testing.T) { // Setup _, addrs := app.GeneratePrivKeyAddressPairs(1) seller := addrs[0] - buyerModName := liquidator.ModuleName + buyerModName := cdp.LiquidatorMacc buyerAddr := supply.NewModuleAddress(buyerModName) tApp := app.NewTestApp() @@ -104,7 +104,7 @@ func TestCollateralAuctionBasic(t *testing.T) { buyer := addrs[0] returnAddrs := addrs[1:] returnWeights := is(30, 20, 10) - sellerModName := liquidator.ModuleName + sellerModName := cdp.LiquidatorMacc sellerAddr := supply.NewModuleAddress(sellerModName) tApp := app.NewTestApp() @@ -174,7 +174,7 @@ func TestStartSurplusAuction(t *testing.T) { { "normal", someTime, - args{liquidator.ModuleName, c("stable", 10), "gov"}, + args{cdp.LiquidatorMacc, c("stable", 10), "gov"}, true, }, { @@ -186,13 +186,13 @@ func TestStartSurplusAuction(t *testing.T) { { "not enough coins", someTime, - args{liquidator.ModuleName, c("stable", 101), "gov"}, + args{cdp.LiquidatorMacc, c("stable", 101), "gov"}, false, }, { "incorrect denom", someTime, - args{liquidator.ModuleName, c("notacoin", 10), "gov"}, + args{cdp.LiquidatorMacc, c("notacoin", 10), "gov"}, false, }, } @@ -202,7 +202,7 @@ func TestStartSurplusAuction(t *testing.T) { initialLiquidatorCoins := cs(c("stable", 100)) tApp := app.NewTestApp() - liqAcc := supply.NewEmptyModuleAccount(liquidator.ModuleName, supply.Burner) + liqAcc := supply.NewEmptyModuleAccount(cdp.LiquidatorMacc, supply.Burner) require.NoError(t, liqAcc.SetCoins(initialLiquidatorCoins)) tApp.InitializeFromGenesisStates( NewAuthGenStateFromAccs(authexported.GenesisAccounts{liqAcc}), @@ -215,7 +215,7 @@ func TestStartSurplusAuction(t *testing.T) { // check sk := tApp.GetSupplyKeeper() - liquidatorCoins := sk.GetModuleAccount(ctx, liquidator.ModuleName).GetCoins() + liquidatorCoins := sk.GetModuleAccount(ctx, cdp.LiquidatorMacc).GetCoins() actualAuc, found := keeper.GetAuction(ctx, id) if tc.expectPass { diff --git a/x/cdp/abci.go b/x/cdp/abci.go new file mode 100644 index 00000000..5c3e1bb9 --- /dev/null +++ b/x/cdp/abci.go @@ -0,0 +1,25 @@ +package cdp + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +// BeginBlock compounds the debt in outstanding cdps and liquidates cdps that are below the required collateralization ratio +func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) { + params := k.GetParams(ctx) + previousBlockTime, found := k.GetPreviousBlockTime(ctx) + if !found { + previousBlockTime = ctx.BlockTime() + } + timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousBlockTime.Unix()) + for _, cp := range params.CollateralParams { + for _, dp := range params.DebtParams { + k.HandleNewDebt(ctx, cp.Denom, dp.Denom, timeElapsed) + } + + k.LiquidateCdps(ctx, cp.MarketID, cp.Denom, cp.LiquidationRatio) + } + k.SetPreviousBlockTime(ctx, ctx.BlockTime()) + return +} diff --git a/x/cdp/abci_test.go b/x/cdp/abci_test.go new file mode 100644 index 00000000..f9249856 --- /dev/null +++ b/x/cdp/abci_test.go @@ -0,0 +1,126 @@ +package cdp_test + +import ( + "math/rand" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/cdp" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type ModuleTestSuite struct { + suite.Suite + + keeper cdp.Keeper + addrs []sdk.AccAddress + app app.TestApp + cdps cdp.CDPs + ctx sdk.Context + liquidations liquidationTracker +} + +type liquidationTracker struct { + xrp []uint64 + btc []uint64 + debt int64 +} + +func (suite *ModuleTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + cdps := make(cdp.CDPs, 100) + _, addrs := app.GeneratePrivKeyAddressPairs(100) + coins := []sdk.Coins{} + tracker := liquidationTracker{} + + for j := 0; j < 100; j++ { + coins = append(coins, cs(c("btc", 100000000), c("xrp", 10000000000))) + } + + authGS := app.NewAuthGenState( + addrs, coins) + tApp.InitializeFromGenesisStates( + authGS, + NewPricefeedGenStateMulti(), + NewCDPGenStateMulti(), + ) + + suite.ctx = ctx + suite.app = tApp + suite.keeper = tApp.GetCDPKeeper() + + for j := 0; j < 100; j++ { + collateral := "xrp" + amount := 10000000000 + debt := simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 750000000, 1249000000) + if j%2 == 0 { + collateral = "btc" + amount = 100000000 + debt = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 2700000000, 5332000000) + if debt >= 4000000000 { + tracker.btc = append(tracker.btc, uint64(j+1)) + tracker.debt += int64(debt) + } + } else { + if debt >= 1000000000 { + tracker.xrp = append(tracker.xrp, uint64(j+1)) + tracker.debt += int64(debt) + } + } + suite.Nil(suite.keeper.AddCdp(suite.ctx, addrs[j], cs(c(collateral, int64(amount))), cs(c("usdx", int64(debt))))) + c, f := suite.keeper.GetCDP(suite.ctx, collateral, uint64(j+1)) + suite.True(f) + cdps[j] = c + } + + suite.cdps = cdps + suite.addrs = addrs + suite.liquidations = tracker +} + +func (suite *ModuleTestSuite) setPrice(price sdk.Dec, market string) { + pfKeeper := suite.app.GetPriceFeedKeeper() + + pfKeeper.SetPrice(suite.ctx, sdk.AccAddress{}, market, price, suite.ctx.BlockTime().Add(time.Hour*3)) + err := pfKeeper.SetCurrentPrices(suite.ctx, market) + suite.NoError(err) + pp, err := pfKeeper.GetCurrentPrice(suite.ctx, market) + suite.NoError(err) + suite.Equal(price, pp.Price) +} +func (suite *ModuleTestSuite) TestBeginBlock() { + sk := suite.app.GetSupplyKeeper() + acc := sk.GetModuleAccount(suite.ctx, cdp.ModuleName) + originalXrpCollateral := acc.GetCoins().AmountOf("xrp") + suite.setPrice(d("0.2"), "xrp:usd") + cdp.BeginBlocker(suite.ctx, abci.RequestBeginBlock{Header: suite.ctx.BlockHeader()}, suite.keeper) + acc = sk.GetModuleAccount(suite.ctx, cdp.ModuleName) + finalXrpCollateral := acc.GetCoins().AmountOf("xrp") + seizedXrpCollateral := originalXrpCollateral.Sub(finalXrpCollateral) + xrpLiquidations := int(seizedXrpCollateral.Quo(i(10000000000)).Int64()) + suite.Equal(len(suite.liquidations.xrp), xrpLiquidations) + + acc = sk.GetModuleAccount(suite.ctx, cdp.ModuleName) + originalBtcCollateral := acc.GetCoins().AmountOf("btc") + suite.setPrice(d("6000"), "btc:usd") + cdp.BeginBlocker(suite.ctx, abci.RequestBeginBlock{Header: suite.ctx.BlockHeader()}, suite.keeper) + acc = sk.GetModuleAccount(suite.ctx, cdp.ModuleName) + finalBtcCollateral := acc.GetCoins().AmountOf("btc") + seizedBtcCollateral := originalBtcCollateral.Sub(finalBtcCollateral) + btcLiquidations := int(seizedBtcCollateral.Quo(i(100000000)).Int64()) + suite.Equal(len(suite.liquidations.btc), btcLiquidations) + + acc = sk.GetModuleAccount(suite.ctx, cdp.LiquidatorMacc) + suite.Equal(suite.liquidations.debt, acc.GetCoins().AmountOf("debt").Int64()) + +} + +func TestModuleTestSuite(t *testing.T) { + suite.Run(t, new(ModuleTestSuite)) +} diff --git a/x/cdp/alias.go b/x/cdp/alias.go index f22e0366..19618d6a 100644 --- a/x/cdp/alias.go +++ b/x/cdp/alias.go @@ -11,51 +11,151 @@ import ( ) 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 + StatusNil = types.StatusNil + StatusLiquidated = types.StatusLiquidated + DefaultCodespace = types.DefaultCodespace + CodeCdpAlreadyExists = types.CodeCdpAlreadyExists + CodeCollateralLengthInvalid = types.CodeCollateralLengthInvalid + CodeCollateralNotSupported = types.CodeCollateralNotSupported + CodeDebtNotSupported = types.CodeDebtNotSupported + CodeExceedsDebtLimit = types.CodeExceedsDebtLimit + CodeInvalidCollateralRatio = types.CodeInvalidCollateralRatio + CodeCdpNotFound = types.CodeCdpNotFound + CodeDepositNotFound = types.CodeDepositNotFound + CodeInvalidDepositDenom = types.CodeInvalidDepositDenom + CodeInvalidPaymentDenom = types.CodeInvalidPaymentDenom + CodeDepositNotAvailable = types.CodeDepositNotAvailable + CodeInvalidCollateralDenom = types.CodeInvalidCollateralDenom + CodeInvalidWithdrawAmount = types.CodeInvalidWithdrawAmount + CodeCdpNotAvailable = types.CodeCdpNotAvailable + CodeBelowDebtFloor = types.CodeBelowDebtFloor + EventTypeCreateCdp = types.EventTypeCreateCdp + EventTypeCdpDeposit = types.EventTypeCdpDeposit + EventTypeCdpDraw = types.EventTypeCdpDraw + EventTypeCdpRepay = types.EventTypeCdpRepay + EventTypeCdpClose = types.EventTypeCdpClose + EventTypeCdpWithdrawal = types.EventTypeCdpWithdrawal + EventTypeCdpLiquidation = types.EventTypeCdpLiquidation + AttributeKeyCdpID = types.AttributeKeyCdpID + AttributeKeyDepositor = types.AttributeKeyDepositor + AttributeValueCategory = types.AttributeValueCategory + LiquidatorMacc = types.LiquidatorMacc + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey + QuerierRoute = types.QuerierRoute + DefaultParamspace = types.DefaultParamspace + QueryGetCdp = types.QueryGetCdp + QueryGetCdps = types.QueryGetCdps + QueryGetCdpsByCollateralization = types.QueryGetCdpsByCollateralization + QueryGetParams = types.QueryGetParams + RestOwner = types.RestOwner + RestCollateralDenom = types.RestCollateralDenom + RestRatio = types.RestRatio ) 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 + NewCDP = types.NewCDP + RegisterCodec = types.RegisterCodec + StatusFromByte = types.StatusFromByte + NewDeposit = types.NewDeposit + ErrCdpAlreadyExists = types.ErrCdpAlreadyExists + ErrInvalidCollateralLength = types.ErrInvalidCollateralLength + ErrCollateralNotSupported = types.ErrCollateralNotSupported + ErrDebtNotSupported = types.ErrDebtNotSupported + ErrExceedsDebtLimit = types.ErrExceedsDebtLimit + ErrInvalidCollateralRatio = types.ErrInvalidCollateralRatio + ErrCdpNotFound = types.ErrCdpNotFound + ErrDepositNotFound = types.ErrDepositNotFound + ErrInvalidDepositDenom = types.ErrInvalidDepositDenom + ErrInvalidPaymentDenom = types.ErrInvalidPaymentDenom + ErrDepositNotAvailable = types.ErrDepositNotAvailable + ErrInvalidCollateralDenom = types.ErrInvalidCollateralDenom + ErrInvalidWithdrawAmount = types.ErrInvalidWithdrawAmount + ErrCdpNotAvailable = types.ErrCdpNotAvailable + ErrBelowDebtFloor = types.ErrBelowDebtFloor + DefaultGenesisState = types.DefaultGenesisState + GetCdpIDBytes = types.GetCdpIDBytes + GetCdpIDFromBytes = types.GetCdpIDFromBytes + CdpKey = types.CdpKey + SplitCdpKey = types.SplitCdpKey + DenomIterKey = types.DenomIterKey + SplitDenomIterKey = types.SplitDenomIterKey + DepositKey = types.DepositKey + SplitDepositKey = types.SplitDepositKey + DepositIterKey = types.DepositIterKey + SplitDepositIterKey = types.SplitDepositIterKey + CollateralRatioBytes = types.CollateralRatioBytes + CollateralRatioKey = types.CollateralRatioKey + SplitCollateralRatioKey = types.SplitCollateralRatioKey + CollateralRatioIterKey = types.CollateralRatioIterKey + SplitCollateralRatioIterKey = types.SplitCollateralRatioIterKey + NewMsgCreateCDP = types.NewMsgCreateCDP + NewMsgDeposit = types.NewMsgDeposit + NewMsgWithdraw = types.NewMsgWithdraw + NewMsgDrawDebt = types.NewMsgDrawDebt + NewMsgRepayDebt = types.NewMsgRepayDebt + NewParams = types.NewParams + DefaultParams = types.DefaultParams + ParamKeyTable = types.ParamKeyTable + NewQueryCdpsParams = types.NewQueryCdpsParams + NewQueryCdpParams = types.NewQueryCdpParams + NewQueryCdpsByRatioParams = types.NewQueryCdpsByRatioParams + ValidSortableDec = types.ValidSortableDec + SortableDecBytes = types.SortableDecBytes + ParseDecBytes = types.ParseDecBytes + RelativePow = types.RelativePow + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier // variable aliases - ModuleCdc = types.ModuleCdc - KeyGlobalDebtLimit = types.KeyGlobalDebtLimit - KeyCollateralParams = types.KeyCollateralParams - KeyStableDenoms = types.KeyStableDenoms - LiquidatorAccountAddress = keeper.LiquidatorAccountAddress + ModuleCdc = types.ModuleCdc + CdpIDKeyPrefix = types.CdpIDKeyPrefix + CdpKeyPrefix = types.CdpKeyPrefix + CollateralRatioIndexPrefix = types.CollateralRatioIndexPrefix + CdpIDKey = types.CdpIDKey + DebtDenomKey = types.DebtDenomKey + DepositKeyPrefix = types.DepositKeyPrefix + PrincipalKeyPrefix = types.PrincipalKeyPrefix + AccumulatorKeyPrefix = types.AccumulatorKeyPrefix + PreviousBlockTimeKey = types.PreviousBlockTimeKey + KeyGlobalDebtLimit = types.KeyGlobalDebtLimit + KeyCollateralParams = types.KeyCollateralParams + KeyDebtParams = types.KeyDebtParams + KeyCircuitBreaker = types.KeyCircuitBreaker + DefaultGlobalDebt = types.DefaultGlobalDebt + DefaultCircuitBreaker = types.DefaultCircuitBreaker + DefaultCollateralParams = types.DefaultCollateralParams + DefaultDebtParams = types.DefaultDebtParams + DefaultCdpStartingID = types.DefaultCdpStartingID + DefaultDebtDenom = types.DefaultDebtDenom + DefaultPreviousBlockTime = types.DefaultPreviousBlockTime + MaxSortableDec = types.MaxSortableDec ) 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 + CDP = types.CDP + CDPs = types.CDPs + Deposit = types.Deposit + DepositStatus = types.DepositStatus + Deposits = types.Deposits + SupplyKeeper = types.SupplyKeeper + PricefeedKeeper = types.PricefeedKeeper + GenesisState = types.GenesisState + MsgCreateCDP = types.MsgCreateCDP + MsgDeposit = types.MsgDeposit + MsgWithdraw = types.MsgWithdraw + MsgDrawDebt = types.MsgDrawDebt + MsgRepayDebt = types.MsgRepayDebt + MsgTransferCDP = types.MsgTransferCDP + Params = types.Params + CollateralParam = types.CollateralParam + CollateralParams = types.CollateralParams + DebtParam = types.DebtParam + DebtParams = types.DebtParams + QueryCdpsParams = types.QueryCdpsParams + QueryCdpParams = types.QueryCdpParams + QueryCdpsByRatioParams = types.QueryCdpsByRatioParams + Keeper = keeper.Keeper ) diff --git a/x/cdp/app_test.go b/x/cdp/app_test.go deleted file mode 100644 index a11758ee..00000000 --- a/x/cdp/app_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package cdp_test - -import ( - "testing" - - "github.com/cosmos/cosmos-sdk/simapp" - sdk "github.com/cosmos/cosmos-sdk/types" - abci "github.com/tendermint/tendermint/abci/types" - - "github.com/kava-labs/kava/app" - "github.com/kava-labs/kava/x/cdp" -) - -func TestApp_CreateModifyDeleteCDP(t *testing.T) { - // Setup - tApp := app.NewTestApp() - privKeys, addrs := app.GeneratePrivKeyAddressPairs(1) - testAddr := addrs[0] - testPrivKey := privKeys[0] - tApp.InitializeFromGenesisStates( - app.NewAuthGenState(addrs, []sdk.Coins{cs(c("xrp", 100))}), - NewPFGenState("xrp", d("1.00")), - NewCDPGenState("xrp", d("1.5")), - ) - // check balance - ctx := tApp.NewContext(false, abci.Header{}) - tApp.CheckBalance(t, ctx, testAddr, cs(c("xrp", 100))) - - tApp.EndBlock(abci.RequestEndBlock{}) - tApp.Commit() - - // Create CDP - msgs := []sdk.Msg{cdp.NewMsgCreateOrModifyCDP(testAddr, "xrp", i(10), i(5))} - simapp.SignCheckDeliver(t, tApp.Codec(), tApp.BaseApp, abci.Header{Height: tApp.LastBlockHeight() + 1}, msgs, []uint64{0}, []uint64{0}, true, true, testPrivKey) - - // check balance - ctx = tApp.NewContext(true, abci.Header{}) - tApp.CheckBalance(t, ctx, testAddr, cs(c("usdx", 5), c("xrp", 90))) - - // Modify CDP - msgs = []sdk.Msg{cdp.NewMsgCreateOrModifyCDP(testAddr, "xrp", i(40), i(5))} - simapp.SignCheckDeliver(t, tApp.Codec(), tApp.BaseApp, abci.Header{Height: tApp.LastBlockHeight() + 1}, msgs, []uint64{0}, []uint64{1}, true, true, testPrivKey) - - // check balance - ctx = tApp.NewContext(true, abci.Header{}) - tApp.CheckBalance(t, ctx, testAddr, cs(c("usdx", 10), c("xrp", 50))) - - // Delete CDP - msgs = []sdk.Msg{cdp.NewMsgCreateOrModifyCDP(testAddr, "xrp", i(-50), i(-10))} - simapp.SignCheckDeliver(t, tApp.Codec(), tApp.BaseApp, abci.Header{Height: tApp.LastBlockHeight() + 1}, msgs, []uint64{0}, []uint64{2}, true, true, testPrivKey) - - // check balance - ctx = tApp.NewContext(true, abci.Header{}) - tApp.CheckBalance(t, ctx, testAddr, cs(c("xrp", 100))) -} diff --git a/x/cdp/client/cli/query.go b/x/cdp/client/cli/query.go index e1cdaefd..eedd74bc 100644 --- a/x/cdp/client/cli/query.go +++ b/x/cdp/client/cli/query.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "strings" "github.com/spf13/cobra" @@ -22,17 +23,17 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { } cdpQueryCmd.AddCommand(client.GetCommands( - GetCmdGetCdp(queryRoute, cdc), - GetCmdGetCdps(queryRoute, cdc), - GetCmdGetUnderCollateralizedCdps(queryRoute, cdc), - GetCmdGetParams(queryRoute, cdc), + QueryCdpCmd(queryRoute, cdc), + QueryCdpsByDenomCmd(queryRoute, cdc), + QueryCdpsByDenomAndRatioCmd(queryRoute, cdc), + QueryParamsCmd(queryRoute, cdc), )...) return cdpQueryCmd } -// GetCmdGetCdp queries the latest info about a particular cdp -func GetCmdGetCdp(queryRoute string, cdc *codec.Codec) *cobra.Command { +// QueryCdpCmd returns the command handler for querying a particular cdp +func QueryCdpCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ Use: "cdp [ownerAddress] [collateralType]", Short: "get info about a cdp", @@ -47,7 +48,6 @@ func GetCmdGetCdp(queryRoute string, cdc *codec.Codec) *cobra.Command { } collateralType := args[1] // TODO validation? bz, err := cdc.MarshalJSON(types.QueryCdpsParams{ - Owner: ownerAddress, CollateralDenom: collateralType, }) if err != nil { @@ -55,7 +55,7 @@ func GetCmdGetCdp(queryRoute string, cdc *codec.Codec) *cobra.Command { } // Query - route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdps) + route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdp) res, _, err := cliCtx.QueryWithData(route, bz) if err != nil { fmt.Printf("error when getting cdp info - %s", err) @@ -64,28 +64,28 @@ func GetCmdGetCdp(queryRoute string, cdc *codec.Codec) *cobra.Command { } // 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]) + var cdp types.CDP + cdc.MustUnmarshalJSON(res, &cdp) + return cliCtx.PrintOutput(cdp) }, } } -// GetCmdGetCdps queries the store for all cdps for a collateral type -func GetCmdGetCdps(queryRoute string, cdc *codec.Codec) *cobra.Command { +// QueryCdpsByDenomCmd returns the command handler for querying cdps for a collateral type +func QueryCdpsByDenomCmd(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), + Short: "Query cdps by collateral type", + Long: strings.TrimSpace(`Query cdps by a specific collateral type, or query all cdps if none is specifed: + +$ query cdp cdps atom + `), + 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? + bz, err := cdc.MarshalJSON(types.QueryCdpsParams{CollateralDenom: args[0]}) if err != nil { return err } @@ -105,23 +105,26 @@ func GetCmdGetCdps(queryRoute string, cdc *codec.Codec) *cobra.Command { } } -func GetCmdGetUnderCollateralizedCdps(queryRoute string, cdc *codec.Codec) *cobra.Command { +// QueryCdpsByDenomAndRatioCmd returns the command handler for querying cdps +// by specified collateral type and collateralization ratio +func QueryCdpsByDenomAndRatioCmd(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), + Use: "cdps [collateralType] [ratio]", + Short: "get cdps with matching collateral type and below the specified ratio", + Long: strings.TrimSpace(`Get all CDPS of a particular collateral type with collateralization + ratio below the specified input.`), + 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 + return fmt.Errorf(errSdk.Error()) } - bz, err := cdc.MarshalJSON(types.QueryCdpsParams{ - CollateralDenom: args[0], - UnderCollateralizedAt: price, + bz, err := cdc.MarshalJSON(types.QueryCdpsByRatioParams{ + CollateralDenom: args[0], + Ratio: price, }) if err != nil { return err @@ -142,7 +145,8 @@ func GetCmdGetUnderCollateralizedCdps(queryRoute string, cdc *codec.Codec) *cobr } } -func GetCmdGetParams(queryRoute string, cdc *codec.Codec) *cobra.Command { +// QueryParamsCmd returns the command handler for cdp parameter querying +func QueryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ Use: "params", Short: "get the cdp module parameters", @@ -153,13 +157,13 @@ func GetCmdGetParams(queryRoute string, cdc *codec.Codec) *cobra.Command { // Query route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetParams) - res, _, err := cliCtx.QueryWithData(route, nil) // TODO use cliCtx.QueryStore? + res, _, err := cliCtx.QueryWithData(route, nil) if err != nil { return err } // Decode and print results - var out types.CdpParams + var out types.QueryCdpParams cdc.MustUnmarshalJSON(res, &out) return cliCtx.PrintOutput(out) }, diff --git a/x/cdp/client/cli/tx.go b/x/cdp/client/cli/tx.go index 5bb0a0a7..83d9ca94 100644 --- a/x/cdp/client/cli/tx.go +++ b/x/cdp/client/cli/tx.go @@ -1,8 +1,6 @@ package cli import ( - "fmt" - "github.com/spf13/cobra" "github.com/cosmos/cosmos-sdk/client" @@ -13,11 +11,9 @@ import ( "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", @@ -25,33 +21,151 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { } cdpTxCmd.AddCommand(client.PostCommands( - GetCmdModifyCdp(cdc), + GetCmdCreateCdp(cdc), + GetCmdDeposit(cdc), + GetCmdWithdraw(cdc), + GetCmdDraw(cdc), + GetCmdRepay(cdc), )...) return cdpTxCmd } -// GetCmdModifyCdp cli command for creating and modifying cdps. -func GetCmdModifyCdp(cdc *codec.Codec) *cobra.Command { +// GetCmdCreateCdp returns the command handler for creating a cdp +func GetCmdCreateCdp(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "modifycdp [ownerAddress] [collateralType] [collateralChange] [debtChange]", - Short: "create or modify a cdp", - Args: cobra.ExactArgs(4), + Use: "create [ownerAddress] [collateralChange] [debtChange]", + Short: "create a new cdp", + Args: cobra.ExactArgs(3), 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 + collateral, err := sdk.ParseCoins(args[1]) + if err != nil { + return err } - debtChange, ok := sdk.NewIntFromString(args[3]) - if !ok { - fmt.Printf("invalid debt amount - %s \n", string(args[3])) - return nil + debt, err := sdk.ParseCoins(args[2]) + if err != nil { + return err } - msg := types.NewMsgCreateOrModifyCDP(cliCtx.GetFromAddress(), args[1], collateralChange, debtChange) - err := msg.ValidateBasic() + msg := types.NewMsgCreateCDP(cliCtx.GetFromAddress(), collateral, debt) + err = msg.ValidateBasic() + if err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } +} + +// GetCmdDeposit cli command for depositing to a cdp. +func GetCmdDeposit(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "deposit [ownerAddress] [depositorAddress] [collateralChange]", + Short: "deposit to an existing cdp", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + collateral, err := sdk.ParseCoins(args[2]) + if err != nil { + return err + } + owner, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + depositor, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return err + } + msg := types.NewMsgDeposit(owner, depositor, collateral) + err = msg.ValidateBasic() + if err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } +} + +// GetCmdWithdraw cli command for withdrawing from a cdp. +func GetCmdWithdraw(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "withdraw [ownerAddress] [depositorAddress] [collateralChange]", + Short: "withdraw from an existing cdp", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + collateral, err := sdk.ParseCoins(args[2]) + if err != nil { + return err + } + owner, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + depositor, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return err + } + msg := types.NewMsgWithdraw(owner, depositor, collateral) + err = msg.ValidateBasic() + if err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } +} + +// GetCmdDraw cli command for depositing to a cdp. +func GetCmdDraw(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "draw [ownerAddress] [collateralDenom] [debtChange]", + Short: "draw debt off an existing cdp", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + debt, err := sdk.ParseCoins(args[2]) + if err != nil { + return err + } + owner, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + msg := types.NewMsgDrawDebt(owner, args[1], debt) + err = msg.ValidateBasic() + if err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } +} + +// GetCmdRepay cli command for depositing to a cdp. +func GetCmdRepay(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "repay [ownerAddress] [collateralDenom] [payment]", + Short: "repay debt from an existing cdp", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + payment, err := sdk.ParseCoins(args[2]) + if err != nil { + return err + } + owner, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + msg := types.NewMsgRepayDebt(owner, args[1], payment) + err = msg.ValidateBasic() if err != nil { return err } diff --git a/x/cdp/client/module_client.go b/x/cdp/client/module_client.go deleted file mode 100644 index 799aaa0c..00000000 --- a/x/cdp/client/module_client.go +++ /dev/null @@ -1,51 +0,0 @@ -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 -} diff --git a/x/cdp/client/rest/query.go b/x/cdp/client/rest/query.go index 94d93d2c..c3f00067 100644 --- a/x/cdp/client/rest/query.go +++ b/x/cdp/client/rest/query.go @@ -14,66 +14,91 @@ import ( 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 +// 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(fmt.Sprintf("/cdp/{%s}/{%s}", types.RestOwner, types.RestCollateralDenom), queryCdpHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/cdp/{%s}", types.RestCollateralDenom), queryCdpsHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/cdp/{%s}/ratio/{%s}", types.RestCollateralDenom, types.RestRatio), queryCdpsByRatioHandlerFn(cliCtx)).Methods("GET") r.HandleFunc("/cdps/params", getParamsHandlerFn(cliCtx)).Methods("GET") } -func getCdpsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { +func queryCdpHandlerFn(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) + vars := mux.Vars(r) + ownerBech32 := vars[types.RestOwner] + collateralDenom := vars[types.RestCollateralDenom] - // 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) + owner, err := sdk.AccAddressFromBech32(ownerBech32) 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) + params := types.NewQueryCdpParams(owner, collateralDenom) + + bz, err := cliCtx.Codec.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s", types.QueryGetCdp), bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + + } +} + +func queryCdpsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + collateralDenom := vars[types.RestCollateralDenom] + + params := types.NewQueryCdpsParams(collateralDenom) + + bz, err := cliCtx.Codec.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s", types.QueryGetCdps), bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + + } +} + +func queryCdpsByRatioHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + collateralDenom := vars[types.RestCollateralDenom] + ratioStr := vars[types.RestRatio] + + ratioDec, sdkError := sdk.NewDecFromStr(ratioStr) + if sdkError != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, sdkError.Error()) + return + } + + params := types.NewQueryCdpsByRatioParams(collateralDenom, ratioDec) + bz, err := cliCtx.Codec.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s", types.QueryGetCdps), bz) if err != nil { rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) return diff --git a/x/cdp/client/rest/rest.go b/x/cdp/client/rest/rest.go index 84eb85fd..d626841a 100644 --- a/x/cdp/client/rest/rest.go +++ b/x/cdp/client/rest/rest.go @@ -4,6 +4,8 @@ import ( "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" ) // RegisterRoutes - Central function to define routes that get registered by the main application @@ -11,3 +13,43 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { registerQueryRoutes(cliCtx, r) registerTxRoutes(cliCtx, r) } + +// PostCdpReq defines the properties of cdp request's body. +type PostCdpReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Owner sdk.AccAddress `json:"owner" yaml:"owner"` + Collateral sdk.Coins `json:"collateral" yaml:"collateral"` + Principal sdk.Coins `json:"principal" yaml:"principal"` +} + +// PostDepositReq defines the properties of cdp request's body. +type PostDepositReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Owner sdk.AccAddress `json:"owner" yaml:"owner"` + Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"` + Collateral sdk.Coins `json:"collateral" yaml:"collateral"` +} + +// PostWithdrawalReq defines the properties of cdp request's body. +type PostWithdrawalReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Owner sdk.AccAddress `json:"owner" yaml:"owner"` + Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"` + Collateral sdk.Coins `json:"collateral" yaml:"collateral"` +} + +// PostDrawReq defines the properties of cdp request's body. +type PostDrawReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Owner sdk.AccAddress `json:"owner" yaml:"owner"` + Denom string `json:"denom" yaml:"denom"` + Principal sdk.Coins `json:"principal" yaml:"principal"` +} + +// PostRepayReq defines the properties of cdp request's body. +type PostRepayReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Owner sdk.AccAddress `json:"owner" yaml:"owner"` + Denom string `json:"denom" yaml:"denom"` + Payment sdk.Coins `json:"principal" yaml:"principal"` +} diff --git a/x/cdp/client/rest/tx.go b/x/cdp/client/rest/tx.go index e1459a46..efef85ef 100644 --- a/x/cdp/client/rest/tx.go +++ b/x/cdp/client/rest/tx.go @@ -1,7 +1,6 @@ package rest import ( - "fmt" "net/http" "github.com/gorilla/mux" @@ -15,13 +14,18 @@ import ( ) func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) { - r.HandleFunc("/cdps", modifyCdpHandlerFn(cliCtx)).Methods("PUT") + r.HandleFunc("/cdp", postCdpHandlerFn(cliCtx)).Methods("PUT") + r.HandleFunc("/cdp/{owner}/{denom}/deposits", postDepositHandlerFn(cliCtx)).Methods("PUT") + r.HandleFunc("/cdp/{owner}/{denom}/withdraw", postWithdrawHandlerFn(cliCtx)).Methods("PUT") + r.HandleFunc("/cdp/{owner}/{denom}/draw", postDrawHandlerFn(cliCtx)).Methods("PUT") + r.HandleFunc("/cdp/{owner}/{denom}/wipe", postRepayHandlerFn(cliCtx)).Methods("PUT") + } -func modifyCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { +func postCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Decode PUT request body - var requestBody types.ModifyCdpRequestBody + var requestBody PostCdpReq if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { return } @@ -30,39 +34,99 @@ func modifyCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { 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, + msg := types.NewMsgCreateCDP( + requestBody.Owner, + requestBody.Collateral, + requestBody.Principal, + ) + utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg}) + } +} + +func postDepositHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Decode PUT request body + var requestBody PostDepositReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { + return + } + requestBody.BaseReq = requestBody.BaseReq.Sanitize() + if !requestBody.BaseReq.ValidateBasic(w) { + return + } + + // Create and return msg + msg := types.NewMsgDeposit( + requestBody.Owner, + requestBody.Depositor, + requestBody.Collateral, + ) + utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg}) + } +} + +func postWithdrawHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Decode PUT request body + var requestBody PostWithdrawalReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { + return + } + requestBody.BaseReq = requestBody.BaseReq.Sanitize() + if !requestBody.BaseReq.ValidateBasic(w) { + return + } + + // Create and return msg + msg := types.NewMsgWithdraw( + requestBody.Owner, + requestBody.Depositor, + requestBody.Collateral, + ) + utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg}) + } +} + +func postDrawHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Decode PUT request body + var requestBody PostDrawReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { + return + } + requestBody.BaseReq = requestBody.BaseReq.Sanitize() + if !requestBody.BaseReq.ValidateBasic(w) { + return + } + + // Create and return msg + msg := types.NewMsgDrawDebt( + requestBody.Owner, + requestBody.Denom, + requestBody.Principal, + ) + utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg}) + } +} + +func postRepayHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Decode PUT request body + var requestBody PostRepayReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) { + return + } + requestBody.BaseReq = requestBody.BaseReq.Sanitize() + if !requestBody.BaseReq.ValidateBasic(w) { + return + } + + // Create and return msg + msg := types.NewMsgRepayDebt( + requestBody.Owner, + requestBody.Denom, + requestBody.Payment, ) utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg}) } diff --git a/x/cdp/genesis.go b/x/cdp/genesis.go index afabd6b9..10de0f86 100644 --- a/x/cdp/genesis.go +++ b/x/cdp/genesis.go @@ -6,42 +6,77 @@ import ( 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.GetParams(ctx) - for _, m := range ap.Markets { - collateralMap[m.MarketID] = 1 +// InitGenesis sets initial genesis state for cdp module +func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, gs GenesisState) { + if err := gs.Validate(); err != nil { + panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err)) } - for _, col := range data.Params.CollateralParams { - _, found := collateralMap[col.Denom] + // validate denoms - check that any collaterals in the params are in the pricefeed, + // pricefeed MUST call InitGenesis before cdp + collateralMap := make(map[string]int) + ap := pk.GetParams(ctx) + for _, a := range ap.Markets { + collateralMap[a.MarketID] = 1 + } + + for _, col := range gs.Params.CollateralParams { + _, found := collateralMap[col.MarketID] if !found { panic(fmt.Sprintf("%s collateral not found in pricefeed", col.Denom)) } } - k.SetParams(ctx, data.Params) + k.SetParams(ctx, gs.Params) - for _, cdp := range data.CDPs { - k.SetCDP(ctx, cdp) + // set the per second fee rate for each collateral type + for _, cp := range gs.Params.CollateralParams { + for _, dp := range gs.Params.DebtParams { + k.SetTotalPrincipal(ctx, cp.Denom, dp.Denom, sdk.ZeroInt()) + } + k.SetFeeRate(ctx, cp.Denom, cp.StabilityFee) } - k.SetGlobalDebt(ctx, data.GlobalDebt) + // add cdps + for _, cdp := range gs.CDPs { + if cdp.ID == gs.StartingCdpID { + panic(fmt.Sprintf("starting cdp id is assigned to an existing cdp: %s", cdp)) + } + k.SetCDP(ctx, cdp) + k.IndexCdpByOwner(ctx, cdp) + ratio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees)) + k.IndexCdpByCollateralRatio(ctx, cdp.Collateral[0].Denom, cdp.ID, ratio) + k.IncrementTotalPrincipal(ctx, cdp.Collateral[0].Denom, cdp.Principal) + } + k.SetNextCdpID(ctx, gs.StartingCdpID) + k.SetDebtDenom(ctx, gs.DebtDenom) + + for _, d := range gs.Deposits { + k.SetDeposit(ctx, d) + } + // only set the previous block time if it's different than default + if !gs.PreviousBlockTime.Equal(DefaultPreviousBlockTime) { + k.SetPreviousBlockTime(ctx, gs.PreviousBlockTime) + } } +// ExportGenesis export genesis state for cdp module func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState { params := k.GetParams(ctx) - cdps, err := k.GetCDPs(ctx, "", sdk.Dec{}) - if err != nil { - panic(err) + cdps := k.GetAllCdps(ctx) + cdpID := k.GetNextCdpID(ctx) + previousBlockTime, found := k.GetPreviousBlockTime(ctx) + if !found { + previousBlockTime = DefaultPreviousBlockTime } - debt := k.GetGlobalDebt(ctx) + debtDenom := k.GetDebtDenom(ctx) return GenesisState{ - Params: params, - GlobalDebt: debt, - CDPs: cdps, + Params: params, + StartingCdpID: cdpID, + CDPs: cdps, + PreviousBlockTime: previousBlockTime, + DebtDenom: debtDenom, } } diff --git a/x/cdp/genesis_test.go b/x/cdp/genesis_test.go new file mode 100644 index 00000000..5d2d6895 --- /dev/null +++ b/x/cdp/genesis_test.go @@ -0,0 +1,61 @@ +package cdp_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/cdp" + + "github.com/stretchr/testify/suite" +) + +type GenesisTestSuite struct { + suite.Suite + + ctx sdk.Context + keeper cdp.Keeper +} + +func (suite *GenesisTestSuite) TestInvalidGenState() { + tApp := app.NewTestApp() + for _, gs := range badGenStates() { + + appGS := app.GenesisState{"cdp": cdp.ModuleCdc.MustMarshalJSON(gs.Genesis)} + suite.Panics(func() { + tApp.InitializeFromGenesisStates( + NewPricefeedGenStateMulti(), + appGS, + ) + }, gs.Reason) + } +} + +func (suite *GenesisTestSuite) TestValidGenState() { + tApp := app.NewTestApp() + + suite.NotPanics(func() { + tApp.InitializeFromGenesisStates( + NewPricefeedGenStateMulti(), + NewCDPGenStateMulti(), + ) + }) + + cdpGS := NewCDPGenStateMulti() + gs := cdp.GenesisState{} + cdp.ModuleCdc.UnmarshalJSON(cdpGS["cdp"], &gs) + gs.CDPs = cdps() + gs.StartingCdpID = uint64(5) + appGS := app.GenesisState{"cdp": cdp.ModuleCdc.MustMarshalJSON(gs)} + suite.NotPanics(func() { + tApp.InitializeFromGenesisStates( + NewPricefeedGenStateMulti(), + appGS, + ) + }) + +} + +func TestGenesisTestSuite(t *testing.T) { + suite.Run(t, new(GenesisTestSuite)) +} diff --git a/x/cdp/handler.go b/x/cdp/handler.go index c7cd84d6..13e1ef17 100644 --- a/x/cdp/handler.go +++ b/x/cdp/handler.go @@ -6,25 +6,108 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// Handle all cdp messages. -func NewHandler(keeper Keeper) sdk.Handler { +// NewHandler creates an sdk.Handler for cdp messages +func NewHandler(k Keeper) sdk.Handler { return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { switch msg := msg.(type) { - case MsgCreateOrModifyCDP: - return handleMsgCreateOrModifyCDP(ctx, keeper, msg) + case MsgCreateCDP: + return handleMsgCreateCDP(ctx, k, msg) + case MsgDeposit: + return handleMsgDeposit(ctx, k, msg) + case MsgWithdraw: + return handleMsgWithdraw(ctx, k, msg) + case MsgDrawDebt: + return handleMsgDrawDebt(ctx, k, msg) + case MsgRepayDebt: + return handleMsgRepayDebt(ctx, k, msg) default: - errMsg := fmt.Sprintf("Unrecognized cdp msg type: %T", msg) + 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) +func handleMsgCreateCDP(ctx sdk.Context, k Keeper, msg MsgCreateCDP) sdk.Result { + err := k.AddCdp(ctx, msg.Sender, msg.Collateral, msg.Principal) if err != nil { return err.Result() } - return sdk.Result{} + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()), + ), + ) + id, _ := k.GetCdpID(ctx, msg.Sender, msg.Collateral[0].Denom) + + return sdk.Result{ + Data: GetCdpIDBytes(id), + Events: ctx.EventManager().Events(), + } +} + +func handleMsgDeposit(ctx sdk.Context, k Keeper, msg MsgDeposit) sdk.Result { + err := k.DepositCollateral(ctx, msg.Owner, msg.Depositor, msg.Collateral) + if err != nil { + return err.Result() + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Depositor.String()), + ), + ) + return sdk.Result{Events: ctx.EventManager().Events()} +} + +func handleMsgWithdraw(ctx sdk.Context, k Keeper, msg MsgWithdraw) sdk.Result { + err := k.WithdrawCollateral(ctx, msg.Owner, msg.Depositor, msg.Collateral) + if err != nil { + return err.Result() + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Depositor.String()), + ), + ) + return sdk.Result{Events: ctx.EventManager().Events()} +} + +func handleMsgDrawDebt(ctx sdk.Context, k Keeper, msg MsgDrawDebt) sdk.Result { + err := k.AddPrincipal(ctx, msg.Sender, msg.CdpDenom, msg.Principal) + if err != nil { + return err.Result() + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()), + ), + ) + return sdk.Result{Events: ctx.EventManager().Events()} +} + +func handleMsgRepayDebt(ctx sdk.Context, k Keeper, msg MsgRepayDebt) sdk.Result { + err := k.RepayPrincipal(ctx, msg.Sender, msg.CdpDenom, msg.Payment) + if err != nil { + return err.Result() + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()), + ), + ) + return sdk.Result{Events: ctx.EventManager().Events()} } diff --git a/x/cdp/handler_test.go b/x/cdp/handler_test.go new file mode 100644 index 00000000..0652c040 --- /dev/null +++ b/x/cdp/handler_test.go @@ -0,0 +1,64 @@ +package cdp_test + +import ( + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/cdp" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" + + "github.com/stretchr/testify/suite" +) + +type HandlerTestSuite struct { + suite.Suite + + ctx sdk.Context + app app.TestApp + handler sdk.Handler + keeper cdp.Keeper +} + +func (suite *HandlerTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + tApp.InitializeFromGenesisStates( + NewPricefeedGenStateMulti(), + NewCDPGenStateMulti(), + ) + keeper := tApp.GetCDPKeeper() + suite.handler = cdp.NewHandler(keeper) + suite.app = tApp + suite.keeper = keeper + suite.ctx = ctx +} + +func (suite *HandlerTestSuite) TestMsgCreateCdp() { + _, addrs := app.GeneratePrivKeyAddressPairs(1) + ak := suite.app.GetAccountKeeper() + acc := ak.NewAccountWithAddress(suite.ctx, addrs[0]) + acc.SetCoins(cs(c("xrp", 200000000), c("btc", 500000000))) + ak.SetAccount(suite.ctx, acc) + msg := cdp.NewMsgCreateCDP( + addrs[0], + cs(c("xrp", 200000000)), + cs(c("usdx", 10000000)), + ) + res := suite.handler(suite.ctx, msg) + suite.True(res.IsOK()) + suite.Equal(cdp.GetCdpIDBytes(uint64(1)), res.Data) + +} + +func (suite *HandlerTestSuite) TestInvalidMsg() { + res := suite.handler(suite.ctx, sdk.NewTestMsg()) + suite.False(res.IsOK()) + suite.True(strings.Contains(res.Log, "unrecognized cdp msg type")) +} + +func TestHandlerTestSuite(t *testing.T) { + suite.Run(t, new(HandlerTestSuite)) +} diff --git a/x/cdp/integration_test.go b/x/cdp/integration_test.go index abf674fb..0a6eafcd 100644 --- a/x/cdp/integration_test.go +++ b/x/cdp/integration_test.go @@ -8,26 +8,25 @@ import ( "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/cdp" "github.com/kava-labs/kava/x/pricefeed" + tmtime "github.com/tendermint/tendermint/types/time" ) -// Avoid cluttering test cases with long function name +// Avoid cluttering test cases with long function names 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 NewPFGenState(asset string, price sdk.Dec) app.GenesisState { - quote := "usd" - ap := pricefeed.Params{ - Markets: []pricefeed.Market{ - pricefeed.Market{MarketID: asset, BaseAsset: asset, QuoteAsset: quote, Oracles: pricefeed.Oracles{}, Active: true}, - }, - } +func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { pfGenesis := pricefeed.GenesisState{ - Params: ap, + Params: pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{MarketID: asset + ":usd", BaseAsset: asset, QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, + }, + }, PostedPrices: []pricefeed.PostedPrice{ pricefeed.PostedPrice{ - MarketID: asset, + MarketID: asset + ":usd", OracleAddress: sdk.AccAddress{}, Price: price, Expiry: time.Now().Add(1 * time.Hour), @@ -39,18 +38,219 @@ func NewPFGenState(asset string, price sdk.Dec) app.GenesisState { func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { cdpGenesis := cdp.GenesisState{ - Params: cdp.CdpParams{ - GlobalDebtLimit: sdk.NewInt(1000000), - CollateralParams: []cdp.CollateralParams{ + Params: cdp.Params{ + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + CollateralParams: cdp.CollateralParams{ { Denom: asset, LiquidationRatio: liquidationRatio, - DebtLimit: sdk.NewInt(500000), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr + Prefix: 0x20, + ConversionFactor: i(6), + MarketID: asset + ":usd", + }, + }, + DebtParams: cdp.DebtParams{ + { + Denom: "usdx", + ReferenceAsset: "usd", + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + ConversionFactor: i(6), + DebtFloor: i(10000000), }, }, }, - GlobalDebt: sdk.ZeroInt(), - CDPs: cdp.CDPs{}, + StartingCdpID: cdp.DefaultCdpStartingID, + DebtDenom: cdp.DefaultDebtDenom, + CDPs: cdp.CDPs{}, + PreviousBlockTime: cdp.DefaultPreviousBlockTime, } return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} } + +func NewPricefeedGenStateMulti() app.GenesisState { + pfGenesis := pricefeed.GenesisState{ + Params: pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, + pricefeed.Market{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, + }, + }, + PostedPrices: []pricefeed.PostedPrice{ + pricefeed.PostedPrice{ + MarketID: "btc:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("8000.00"), + Expiry: time.Now().Add(1 * time.Hour), + }, + pricefeed.PostedPrice{ + MarketID: "xrp:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("0.25"), + Expiry: time.Now().Add(1 * time.Hour), + }, + }, + } + return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)} +} +func NewCDPGenStateMulti() app.GenesisState { + cdpGenesis := cdp.GenesisState{ + Params: cdp.Params{ + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + CollateralParams: cdp.CollateralParams{ + { + Denom: "xrp", + LiquidationRatio: sdk.MustNewDecFromStr("2.0"), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr + Prefix: 0x20, + MarketID: "xrp:usd", + ConversionFactor: i(6), + }, + { + Denom: "btc", + LiquidationRatio: sdk.MustNewDecFromStr("1.5"), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr + Prefix: 0x21, + MarketID: "btc:usd", + ConversionFactor: i(8), + }, + }, + DebtParams: cdp.DebtParams{ + { + Denom: "usdx", + ReferenceAsset: "usd", + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + ConversionFactor: i(6), + DebtFloor: i(10000000), + }, + { + Denom: "susd", + ReferenceAsset: "usd", + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("susd", 1000000000000)), + ConversionFactor: i(6), + DebtFloor: i(10000000), + }, + }, + }, + StartingCdpID: cdp.DefaultCdpStartingID, + DebtDenom: cdp.DefaultDebtDenom, + CDPs: cdp.CDPs{}, + PreviousBlockTime: cdp.DefaultPreviousBlockTime, + } + return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} +} + +func cdps() (cdps cdp.CDPs) { + _, addrs := app.GeneratePrivKeyAddressPairs(3) + c1 := cdp.NewCDP(uint64(1), addrs[0], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(10000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(8000000))), tmtime.Canonical(time.Now())) + c2 := cdp.NewCDP(uint64(2), addrs[1], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(100000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(10000000))), tmtime.Canonical(time.Now())) + c3 := cdp.NewCDP(uint64(3), addrs[1], sdk.NewCoins(sdk.NewCoin("btc", sdk.NewInt(1000000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(10000000))), tmtime.Canonical(time.Now())) + c4 := cdp.NewCDP(uint64(4), addrs[2], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(1000000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(500000000))), tmtime.Canonical(time.Now())) + cdps = append(cdps, c1, c2, c3, c4) + return +} + +type badGenState struct { + Genesis cdp.GenesisState + Reason string +} + +func badGenStates() []badGenState { + g1 := baseGenState() + g1.Params.CollateralParams[0].Denom = "btc" + + g2 := baseGenState() + g2.Params.CollateralParams[0].Prefix = 0x21 + + g3 := baseGenState() + g3.Params.CollateralParams[0].DebtLimit = sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("lol", 500000000000)) + + g4 := baseGenState() + g4.Params.CollateralParams[0].DebtLimit = sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000001)) + + g5 := baseGenState() + g5.Params.CollateralParams[0].DebtLimit = sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000001), sdk.NewInt64Coin("susd", 500000000000)) + + g6 := baseGenState() + g6.Params.DebtParams[0].Denom = "susd" + + g7 := baseGenState() + g7.Params.DebtParams[0].DebtLimit = sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000001)) + + g8 := baseGenState() + g8.Params.DebtParams = append(g8.Params.DebtParams, cdp.DebtParam{ + Denom: "lol", + ReferenceAsset: "usd", + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("lol", 1000000000000)), + }) + + g9 := baseGenState() + g9.DebtDenom = "" + + g10 := baseGenState() + g10.PreviousBlockTime = time.Time{} + + return []badGenState{ + badGenState{Genesis: g1, Reason: "duplicate collateral denom"}, + badGenState{Genesis: g2, Reason: "duplicate collateral prefix"}, + badGenState{Genesis: g3, Reason: "duplicate collateral prefix"}, + badGenState{Genesis: g4, Reason: "single collateral exceeds debt limit"}, + badGenState{Genesis: g5, Reason: "combined collateral exceeds debt limit"}, + badGenState{Genesis: g6, Reason: "duplicate debt denom"}, + badGenState{Genesis: g7, Reason: "debt limit exceeds global debt limit"}, + badGenState{Genesis: g8, Reason: "debt param not found in global debt limit"}, + badGenState{Genesis: g9, Reason: "debt denom not set"}, + badGenState{Genesis: g10, Reason: "previous block time not set"}, + } +} + +func baseGenState() cdp.GenesisState { + return cdp.GenesisState{ + Params: cdp.Params{ + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + CollateralParams: cdp.CollateralParams{ + { + Denom: "xrp", + LiquidationRatio: sdk.MustNewDecFromStr("2.0"), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr + Prefix: 0x20, + MarketID: "xrp:usd", + ConversionFactor: i(6), + }, + { + Denom: "btc", + LiquidationRatio: sdk.MustNewDecFromStr("1.5"), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr + Prefix: 0x21, + MarketID: "btc:usd", + ConversionFactor: i(8), + }, + }, + DebtParams: cdp.DebtParams{ + { + Denom: "usdx", + ReferenceAsset: "usd", + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + ConversionFactor: i(6), + DebtFloor: i(10000000), + }, + { + Denom: "susd", + ReferenceAsset: "usd", + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("susd", 1000000000000)), + ConversionFactor: i(6), + DebtFloor: i(10000000), + }, + }, + }, + StartingCdpID: cdp.DefaultCdpStartingID, + DebtDenom: cdp.DefaultDebtDenom, + CDPs: cdp.CDPs{}, + PreviousBlockTime: cdp.DefaultPreviousBlockTime, + } +} diff --git a/x/cdp/keeper/bank_test.go b/x/cdp/keeper/bank_test.go deleted file mode 100644 index 3e13b4b4..00000000 --- a/x/cdp/keeper/bank_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package keeper_test - -import ( - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" - - "github.com/kava-labs/kava/app" - keep "github.com/kava-labs/kava/x/cdp/keeper" -) - -// Test the bank functionality of the CDP keeper -func TestKeeper_AddSubtractGetCoins(t *testing.T) { - _, addrs := app.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", keep.LiquidatorAccountAddress, true, cs(c("usdx", 53)), cs(c("usdx", 153))}, - {"subLiquidatorStable", keep.LiquidatorAccountAddress, false, cs(c("usdx", 53)), cs(c("usdx", 47))}, - {"addLiquidatorGov", keep.LiquidatorAccountAddress, true, cs(c("kava", 53)), cs(c("usdx", 100))}, // no change to balance - {"subLiquidatorGov", keep.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 app with an account - tApp := app.NewTestApp() - tApp.InitializeFromGenesisStates( - app.NewAuthGenState([]sdk.AccAddress{normalAddr}, []sdk.Coins{cs(c("usdx", 100), c("kava", 100))}), - ) - - // create a new context and setup the liquidator account - ctx := tApp.NewContext(false, abci.Header{}) - keeper := tApp.GetCDPKeeper() - keeper.SetLiquidatorModuleAccount(ctx, keep.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) - } - - // check balances are as expected - require.NoError(t, err) - require.Equal(t, tc.expectedCoins, keeper.GetCoins(ctx, tc.address)) - }) - } -} diff --git a/x/cdp/keeper/cdp.go b/x/cdp/keeper/cdp.go new file mode 100644 index 00000000..af4f4808 --- /dev/null +++ b/x/cdp/keeper/cdp.go @@ -0,0 +1,421 @@ +package keeper + +import ( + "bytes" + "fmt" + + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/cdp/types" +) + +// AddCdp adds a cdp for a specific owner and collateral type +func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coins, principal sdk.Coins) sdk.Error { + // validation + err := k.ValidateCollateral(ctx, collateral) + if err != nil { + return err + } + _, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral[0].Denom) + if found { + return types.ErrCdpAlreadyExists(k.codespace, owner, collateral[0].Denom) + } + err = k.ValidatePrincipalAdd(ctx, principal) + if err != nil { + return err + } + err = k.ValidateCollateralizationRatio(ctx, collateral, principal, sdk.NewCoins()) + if err != nil { + return err + } + + // send coins from the owners account to the cdp module + id := k.GetNextCdpID(ctx) + cdp := types.NewCDP(id, owner, collateral, principal, ctx.BlockHeader().Time) + deposit := types.NewDeposit(cdp.ID, owner, collateral) + err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, collateral) + if err != nil { + return err + } + + // mint the principal and send to the owners account + err = k.supplyKeeper.MintCoins(ctx, types.ModuleName, principal) + if err != nil { + panic(err) + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, owner, principal) + if err != nil { + panic(err) + } + + // mint the corresponding amount of debt coins + err = k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), principal) + if err != nil { + panic(err) + } + + // emit events for cdp creation, deposit, and draw + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeCreateCdp, + sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)), + ), + ) + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeCdpDeposit, + sdk.NewAttribute(sdk.AttributeKeyAmount, collateral.String()), + sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)), + ), + ) + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeCdpDraw, + sdk.NewAttribute(sdk.AttributeKeyAmount, principal.String()), + sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)), + ), + ) + + // update total principal for input collateral type + k.IncrementTotalPrincipal(ctx, collateral[0].Denom, principal) + + // set the cdp, deposit, and indexes in the store + collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, collateral, principal) + k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio) + k.IndexCdpByOwner(ctx, cdp) + k.SetDeposit(ctx, deposit) + k.SetNextCdpID(ctx, id+1) + return nil +} + +// SetCdpAndCollateralRatioIndex sets the cdp and collateral ratio index in the store +func (k Keeper) SetCdpAndCollateralRatioIndex(ctx sdk.Context, cdp types.CDP, ratio sdk.Dec) { + k.SetCDP(ctx, cdp) + k.IndexCdpByCollateralRatio(ctx, cdp.Collateral[0].Denom, cdp.ID, ratio) +} + +// MintDebtCoins mints debt coins in the cdp module account +func (k Keeper) MintDebtCoins(ctx sdk.Context, moduleAccount string, denom string, principalCoins sdk.Coins) sdk.Error { + coinsToMint := sdk.NewCoins() + for _, sc := range principalCoins { + coinsToMint = coinsToMint.Add(sdk.NewCoins(sdk.NewCoin(denom, sc.Amount))) + } + err := k.supplyKeeper.MintCoins(ctx, moduleAccount, coinsToMint) + if err != nil { + return err + } + return nil +} + +// BurnDebtCoins burns debts coins from the cdp module account +func (k Keeper) BurnDebtCoins(ctx sdk.Context, moduleAccount string, denom string, paymentCoins sdk.Coins) sdk.Error { + coinsToBurn := sdk.NewCoins() + for _, pc := range paymentCoins { + coinsToBurn = coinsToBurn.Add(sdk.NewCoins(sdk.NewCoin(denom, pc.Amount))) + } + err := k.supplyKeeper.BurnCoins(ctx, moduleAccount, coinsToBurn) + if err != nil { + return err + } + return nil +} + +// GetCdpID returns the id of the cdp corresponding to a specific owner and collateral denom +func (k Keeper) GetCdpID(ctx sdk.Context, owner sdk.AccAddress, denom string) (uint64, bool) { + + cdpIDs, found := k.GetCdpIdsByOwner(ctx, owner) + if !found { + return 0, false + } + for _, id := range cdpIDs { + _, found = k.GetCDP(ctx, denom, id) + if found { + return id, true + } + } + return 0, false + +} + +// GetCdpIdsByOwner returns all the ids of cdps corresponding to a particular owner +func (k Keeper) GetCdpIdsByOwner(ctx sdk.Context, owner sdk.AccAddress) ([]uint64, bool) { + store := prefix.NewStore(ctx.KVStore(k.key), types.CdpIDKeyPrefix) + bz := store.Get(owner) + // TODO figure out why this is necessary + if bz == nil || bytes.Equal(bz, []byte{0}) { + return []uint64{}, false + } + var cdpIDs []uint64 + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &cdpIDs) + return cdpIDs, true +} + +// GetCdpByOwnerAndDenom queries cdps owned by owner and returns the cdp with matching denom +func (k Keeper) GetCdpByOwnerAndDenom(ctx sdk.Context, owner sdk.AccAddress, denom string) (types.CDP, bool) { + cdpIDs, found := k.GetCdpIdsByOwner(ctx, owner) + if !found { + return types.CDP{}, false + } + for _, id := range cdpIDs { + cdp, found := k.GetCDP(ctx, denom, id) + if found { + return cdp, true + } + } + return types.CDP{}, false +} + +// GetCDP returns the cdp associated with a particular collateral denom and id +func (k Keeper) GetCDP(ctx sdk.Context, collateralDenom string, cdpID uint64) (types.CDP, bool) { + // get store + store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix) + db, _ := k.GetDenomPrefix(ctx, collateralDenom) + // get CDP + bz := store.Get(types.CdpKey(db, cdpID)) + // unmarshal + if bz == nil { + return types.CDP{}, false + } + var cdp types.CDP + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &cdp) + return cdp, true +} + +// SetCDP sets a cdp in the store +func (k Keeper) SetCDP(ctx sdk.Context, cdp types.CDP) { + store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix) + db, _ := k.GetDenomPrefix(ctx, cdp.Collateral[0].Denom) + bz := k.cdc.MustMarshalBinaryLengthPrefixed(cdp) + store.Set(types.CdpKey(db, cdp.ID), bz) + return +} + +// DeleteCDP deletes a cdp from the store +func (k Keeper) DeleteCDP(ctx sdk.Context, cdp types.CDP) { + store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix) + db, _ := k.GetDenomPrefix(ctx, cdp.Collateral[0].Denom) + store.Delete(types.CdpKey(db, cdp.ID)) + +} + +// GetAllCdps returns all cdps from the store +func (k Keeper) GetAllCdps(ctx sdk.Context) (cdps types.CDPs) { + k.IterateAllCdps(ctx, func(cdp types.CDP) bool { + cdps = append(cdps, cdp) + return false + }) + return +} + +// GetAllCdpsByDenom returns all cdps of a particular collateral type from the store +func (k Keeper) GetAllCdpsByDenom(ctx sdk.Context, denom string) (cdps types.CDPs) { + k.IterateCdpsByDenom(ctx, denom, func(cdp types.CDP) bool { + cdps = append(cdps, cdp) + return false + }) + return +} + +// GetAllCdpsByDenomAndRatio returns all cdps of a particular collateral type and below a certain collateralization ratio +func (k Keeper) GetAllCdpsByDenomAndRatio(ctx sdk.Context, denom string, targetRatio sdk.Dec) (cdps types.CDPs) { + k.IterateCdpsByCollateralRatio(ctx, denom, targetRatio, func(cdp types.CDP) bool { + cdps = append(cdps, cdp) + return false + }) + return +} + +// SetNextCdpID sets the highest cdp id in the store +func (k Keeper) SetNextCdpID(ctx sdk.Context, id uint64) { + store := prefix.NewStore(ctx.KVStore(k.key), types.CdpIDKey) + store.Set([]byte{}, types.GetCdpIDBytes(id)) +} + +// GetNextCdpID returns the highest cdp id from the store +func (k Keeper) GetNextCdpID(ctx sdk.Context) (id uint64) { + store := prefix.NewStore(ctx.KVStore(k.key), types.CdpIDKey) + bz := store.Get([]byte{}) + if bz == nil { + panic("starting cdp id not set in genesis") + } + id = types.GetCdpIDFromBytes(bz) + return +} + +// IndexCdpByOwner sets the cdp id in the store, indexed by the owner +func (k Keeper) IndexCdpByOwner(ctx sdk.Context, cdp types.CDP) { + store := prefix.NewStore(ctx.KVStore(k.key), types.CdpIDKeyPrefix) + cdpIDs, found := k.GetCdpIdsByOwner(ctx, cdp.Owner) + + if !found { + idBytes := k.cdc.MustMarshalBinaryLengthPrefixed([]uint64{cdp.ID}) + store.Set(cdp.Owner, idBytes) + return + } + for _, id := range cdpIDs { + if id == cdp.ID { + return + } + cdpIDs = append(cdpIDs, cdp.ID) + store.Set(cdp.Owner, k.cdc.MustMarshalBinaryLengthPrefixed(cdpIDs)) + } +} + +// RemoveCdpOwnerIndex deletes the cdp id from the store's index of cdps by owner +func (k Keeper) RemoveCdpOwnerIndex(ctx sdk.Context, cdp types.CDP) { + store := prefix.NewStore(ctx.KVStore(k.key), types.CdpIDKeyPrefix) + cdpIDs, found := k.GetCdpIdsByOwner(ctx, cdp.Owner) + if !found { + return + } + updatedCdpIds := []uint64{} + for _, id := range cdpIDs { + if id != cdp.ID { + updatedCdpIds = append(updatedCdpIds, id) + } + } + if len(updatedCdpIds) == 0 { + store.Delete(cdp.Owner) + } + store.Set(cdp.Owner, k.cdc.MustMarshalBinaryLengthPrefixed(updatedCdpIds)) + +} + +// IndexCdpByCollateralRatio sets the cdp id in the store, indexed by the collateral type and collateral to debt ratio +func (k Keeper) IndexCdpByCollateralRatio(ctx sdk.Context, denom string, id uint64, collateralRatio sdk.Dec) { + store := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix) + db, _ := k.GetDenomPrefix(ctx, denom) + store.Set(types.CollateralRatioKey(db, id, collateralRatio), types.GetCdpIDBytes(id)) +} + +// RemoveCdpCollateralRatioIndex deletes the cdp id from the store's index of cdps by collateral type and collateral to debt ratio +func (k Keeper) RemoveCdpCollateralRatioIndex(ctx sdk.Context, denom string, id uint64, collateralRatio sdk.Dec) { + store := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix) + db, _ := k.GetDenomPrefix(ctx, denom) + store.Delete(types.CollateralRatioKey(db, id, collateralRatio)) +} + +// GetDebtDenom returns the denom of debt in the system +func (k Keeper) GetDebtDenom(ctx sdk.Context) (denom string) { + store := prefix.NewStore(ctx.KVStore(k.key), types.DebtDenomKey) + bz := store.Get([]byte{}) + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &denom) + return +} + +// SetDebtDenom set the denom of debt in the system +func (k Keeper) SetDebtDenom(ctx sdk.Context, denom string) { + if denom == "" { + panic("debt denom not set in genesis") + } + store := prefix.NewStore(ctx.KVStore(k.key), types.DebtDenomKey) + store.Set([]byte{}, k.cdc.MustMarshalBinaryLengthPrefixed(denom)) + return +} + +// ValidateCollateral validates that a collateral is valid for use in cdps +func (k Keeper) ValidateCollateral(ctx sdk.Context, collateral sdk.Coins) sdk.Error { + if len(collateral) != 1 { + return types.ErrInvalidCollateralLength(k.codespace, len(collateral)) + } + _, found := k.GetCollateral(ctx, collateral[0].Denom) + if !found { + return types.ErrCollateralNotSupported(k.codespace, collateral[0].Denom) + } + return nil +} + +// ValidatePrincipalAdd validates that an asset is valid for use as debt when creating a new cdp +func (k Keeper) ValidatePrincipalAdd(ctx sdk.Context, principal sdk.Coins) sdk.Error { + for _, dc := range principal { + dp, found := k.GetDebt(ctx, dc.Denom) + if !found { + return types.ErrDebtNotSupported(k.codespace, dc.Denom) + } + if sdk.NewCoins(dc).IsAnyGT(dp.DebtLimit) { + return types.ErrExceedsDebtLimit(k.codespace, sdk.NewCoins(dc), dp.DebtLimit) + } + if dc.Amount.LT(dp.DebtFloor) { + return types.ErrBelowDebtFloor(k.codespace, sdk.NewCoins(dc), dp.DebtFloor) + } + } + return nil +} + +// ValidatePrincipalDraw validates that an asset is valid for use as debt when drawing debt off an existing cdp +func (k Keeper) ValidatePrincipalDraw(ctx sdk.Context, principal sdk.Coins) sdk.Error { + for _, dc := range principal { + dp, found := k.GetDebt(ctx, dc.Denom) + if !found { + return types.ErrDebtNotSupported(k.codespace, dc.Denom) + } + if sdk.NewCoins(dc).IsAnyGT(dp.DebtLimit) { + return types.ErrExceedsDebtLimit(k.codespace, sdk.NewCoins(dc), dp.DebtLimit) + } + } + return nil +} + +// ValidateCollateralizationRatio validate that adding the input principal doesn't put the cdp below the liquidation ratio +func (k Keeper) ValidateCollateralizationRatio(ctx sdk.Context, collateral sdk.Coins, principal sdk.Coins, fees sdk.Coins) sdk.Error { + // + collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, collateral, principal, fees) + if err != nil { + return err + } + liquidationRatio := k.getLiquidationRatio(ctx, collateral[0].Denom) + if collateralizationRatio.LT(liquidationRatio) { + return types.ErrInvalidCollateralRatio(k.codespace, collateral[0].Denom, collateralizationRatio, liquidationRatio) + } + return nil +} + +// CalculateCollateralToDebtRatio returns the collateral to debt ratio of the input collateral and debt amounts +func (k Keeper) CalculateCollateralToDebtRatio(ctx sdk.Context, collateral sdk.Coins, debt sdk.Coins) sdk.Dec { + debtTotal := sdk.ZeroDec() + for _, dc := range debt { + debtBaseUnits := k.convertDebtToBaseUnits(ctx, dc) + debtTotal = debtTotal.Add(debtBaseUnits) + } + + if debtTotal.IsZero() || debtTotal.GTE(types.MaxSortableDec) { + return types.MaxSortableDec.Sub(sdk.SmallestDec()) + } + + collateralBaseUnits := k.convertCollateralToBaseUnits(ctx, collateral[0]) + return collateralBaseUnits.Quo(debtTotal) +} + +// CalculateCollateralizationRatio returns the collateralization ratio of the input collateral to the input debt plus fees +func (k Keeper) CalculateCollateralizationRatio(ctx sdk.Context, collateral sdk.Coins, principal sdk.Coins, fees sdk.Coins) (sdk.Dec, sdk.Error) { + marketID := k.getMarketID(ctx, collateral[0].Denom) + price, err := k.pricefeedKeeper.GetCurrentPrice(ctx, marketID) + if err != nil { + return sdk.Dec{}, err + } + collateralBaseUnits := k.convertCollateralToBaseUnits(ctx, collateral[0]) + collateralValue := collateralBaseUnits.Mul(price.Price) + principalTotal := sdk.ZeroDec() + for _, pc := range principal { + prinicpalBaseUnits := k.convertDebtToBaseUnits(ctx, pc) + principalTotal = principalTotal.Add(prinicpalBaseUnits) + } + for _, fc := range fees { + feeBaseUnits := k.convertDebtToBaseUnits(ctx, fc) + principalTotal = principalTotal.Add(feeBaseUnits) + } + collateralRatio := collateralValue.Quo(principalTotal) + return collateralRatio, nil +} + +// converts the input collateral to base units (ie multiplies the input by 10^(-ConversionFactor)) +func (k Keeper) convertCollateralToBaseUnits(ctx sdk.Context, collateral sdk.Coin) (baseUnits sdk.Dec) { + cp, _ := k.GetCollateral(ctx, collateral.Denom) + return sdk.NewDecFromInt(collateral.Amount).Mul(sdk.NewDecFromIntWithPrec(sdk.OneInt(), cp.ConversionFactor.Int64())) +} + +// converts the input debt to base units (ie multiplies the input by 10^(-ConversionFactor)) +func (k Keeper) convertDebtToBaseUnits(ctx sdk.Context, debt sdk.Coin) (baseUnits sdk.Dec) { + dp, _ := k.GetDebt(ctx, debt.Denom) + return sdk.NewDecFromInt(debt.Amount).Mul(sdk.NewDecFromIntWithPrec(sdk.OneInt(), dp.ConversionFactor.Int64())) +} diff --git a/x/cdp/keeper/cdp_test.go b/x/cdp/keeper/cdp_test.go new file mode 100644 index 00000000..26d93857 --- /dev/null +++ b/x/cdp/keeper/cdp_test.go @@ -0,0 +1,299 @@ +package keeper_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/cdp/keeper" + "github.com/kava-labs/kava/x/cdp/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type CdpTestSuite struct { + suite.Suite + + keeper keeper.Keeper + app app.TestApp + ctx sdk.Context +} + +func (suite *CdpTestSuite) SetupTest() { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + tApp.InitializeFromGenesisStates( + NewPricefeedGenStateMulti(), + NewCDPGenStateMulti(), + ) + keeper := tApp.GetCDPKeeper() + suite.app = tApp + suite.ctx = ctx + suite.keeper = keeper + return +} + +func (suite *CdpTestSuite) TestAddCdp() { + _, addrs := app.GeneratePrivKeyAddressPairs(1) + ak := suite.app.GetAccountKeeper() + acc := ak.NewAccountWithAddress(suite.ctx, addrs[0]) + acc.SetCoins(cs(c("xrp", 200000000), c("btc", 500000000))) + ak.SetAccount(suite.ctx, acc) + err := suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 200000000)), cs(c("usdx", 26000000))) + suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code) + err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 500000000)), cs(c("usdx", 26000000))) + suite.Error(err) + err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 200000000)), cs(c("xusd", 10000000))) + suite.Equal(types.CodeDebtNotSupported, err.Result().Code) + ctx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 2)) + pk := suite.app.GetPriceFeedKeeper() + _ = pk.SetCurrentPrices(ctx, "xrp:usd") + err = suite.keeper.AddCdp(ctx, addrs[0], cs(c("xrp", 100000000)), cs(c("usdx", 10000000))) + suite.Error(err) + + _ = pk.SetCurrentPrices(suite.ctx, "xrp:usd") + err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 100000000)), cs(c("usdx", 10000000))) + suite.NoError(err) + id := suite.keeper.GetNextCdpID(suite.ctx) + suite.Equal(uint64(2), id) + tp := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") + suite.Equal(i(10000000), tp) + sk := suite.app.GetSupplyKeeper() + macc := sk.GetModuleAccount(suite.ctx, types.ModuleName) + suite.Equal(cs(c("debt", 10000000), c("xrp", 100000000)), macc.GetCoins()) + acc = ak.GetAccount(suite.ctx, addrs[0]) + suite.Equal(cs(c("usdx", 10000000), c("xrp", 100000000), c("btc", 500000000)), acc.GetCoins()) + + err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("btc", 500000000)), cs(c("usdx", 26667000000))) + suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code) + + err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("btc", 500000000)), cs(c("usdx", 100000000))) + suite.NoError(err) + id = suite.keeper.GetNextCdpID(suite.ctx) + suite.Equal(uint64(3), id) + tp = suite.keeper.GetTotalPrincipal(suite.ctx, "btc", "usdx") + suite.Equal(i(100000000), tp) + macc = sk.GetModuleAccount(suite.ctx, types.ModuleName) + suite.Equal(cs(c("debt", 110000000), c("xrp", 100000000), c("btc", 500000000)), macc.GetCoins()) + acc = ak.GetAccount(suite.ctx, addrs[0]) + suite.Equal(cs(c("usdx", 110000000), c("xrp", 100000000)), acc.GetCoins()) + + err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("lol", 100)), cs(c("usdx", 10))) + suite.Equal(types.CodeCollateralNotSupported, err.Result().Code) + err = suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 100)), cs(c("usdx", 10))) + suite.Equal(types.CodeCdpAlreadyExists, err.Result().Code) +} + +func (suite *CdpTestSuite) TestGetSetDenomByte() { + _, found := suite.keeper.GetDenomPrefix(suite.ctx, "lol") + suite.False(found) + db, found := suite.keeper.GetDenomPrefix(suite.ctx, "xrp") + suite.True(found) + suite.Equal(byte(0x20), db) +} + +func (suite *CdpTestSuite) TestGetDebtDenom() { + suite.Panics(func() { suite.keeper.SetDebtDenom(suite.ctx, "") }) + t := suite.keeper.GetDebtDenom(suite.ctx) + suite.Equal("debt", t) + suite.keeper.SetDebtDenom(suite.ctx, "lol") + t = suite.keeper.GetDebtDenom(suite.ctx) + suite.Equal("lol", t) +} + +func (suite *CdpTestSuite) TestGetNextCdpID() { + id := suite.keeper.GetNextCdpID(suite.ctx) + suite.Equal(types.DefaultCdpStartingID, id) +} + +func (suite *CdpTestSuite) TestGetSetCdp() { + _, addrs := app.GeneratePrivKeyAddressPairs(1) + cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 1)), cs(c("usdx", 1)), tmtime.Canonical(time.Now())) + suite.keeper.SetCDP(suite.ctx, cdp) + t, found := suite.keeper.GetCDP(suite.ctx, "xrp", types.DefaultCdpStartingID) + suite.True(found) + suite.Equal(cdp, t) + _, found = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2)) + suite.False(found) + suite.keeper.DeleteCDP(suite.ctx, cdp) + _, found = suite.keeper.GetCDP(suite.ctx, "btc", types.DefaultCdpStartingID) + suite.False(found) +} + +func (suite *CdpTestSuite) TestGetSetCdpId() { + _, addrs := app.GeneratePrivKeyAddressPairs(2) + cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 1)), cs(c("usdx", 1)), tmtime.Canonical(time.Now())) + suite.keeper.SetCDP(suite.ctx, cdp) + suite.keeper.IndexCdpByOwner(suite.ctx, cdp) + id, found := suite.keeper.GetCdpID(suite.ctx, addrs[0], "xrp") + suite.True(found) + suite.Equal(types.DefaultCdpStartingID, id) + _, found = suite.keeper.GetCdpID(suite.ctx, addrs[0], "lol") + suite.False(found) + _, found = suite.keeper.GetCdpID(suite.ctx, addrs[1], "xrp") + suite.False(found) +} + +func (suite *CdpTestSuite) TestGetSetCdpByOwnerAndDenom() { + _, addrs := app.GeneratePrivKeyAddressPairs(2) + cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 1)), cs(c("usdx", 1)), tmtime.Canonical(time.Now())) + suite.keeper.SetCDP(suite.ctx, cdp) + suite.keeper.IndexCdpByOwner(suite.ctx, cdp) + t, found := suite.keeper.GetCdpByOwnerAndDenom(suite.ctx, addrs[0], "xrp") + suite.True(found) + suite.Equal(cdp, t) + _, found = suite.keeper.GetCdpByOwnerAndDenom(suite.ctx, addrs[0], "lol") + suite.False(found) + _, found = suite.keeper.GetCdpByOwnerAndDenom(suite.ctx, addrs[1], "xrp") + suite.False(found) + suite.NotPanics(func() { suite.keeper.IndexCdpByOwner(suite.ctx, cdp) }) +} + +func (suite *CdpTestSuite) TestCalculateCollateralToDebtRatio() { + _, addrs := app.GeneratePrivKeyAddressPairs(1) + cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 3)), cs(c("usdx", 1)), tmtime.Canonical(time.Now())) + cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Principal) + suite.Equal(sdk.MustNewDecFromStr("3.0"), cr) + cdp = types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 1)), cs(c("usdx", 2)), tmtime.Canonical(time.Now())) + cr = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Principal) + suite.Equal(sdk.MustNewDecFromStr("0.5"), cr) + cdp = types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 3)), cs(c("usdx", 1), c("susd", 2)), tmtime.Canonical(time.Now())) + cr = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Principal) + suite.Equal(sdk.MustNewDecFromStr("1"), cr) +} + +func (suite *CdpTestSuite) TestSetCdpByCollateralRatio() { + _, addrs := app.GeneratePrivKeyAddressPairs(1) + cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], cs(c("xrp", 3)), cs(c("usdx", 1)), tmtime.Canonical(time.Now())) + cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Principal) + suite.NotPanics(func() { suite.keeper.IndexCdpByCollateralRatio(suite.ctx, cdp.Collateral[0].Denom, cdp.ID, cr) }) +} + +func (suite *CdpTestSuite) TestIterateCdps() { + cdps := cdps() + for _, c := range cdps { + suite.keeper.SetCDP(suite.ctx, c) + suite.keeper.IndexCdpByOwner(suite.ctx, c) + cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, c.Collateral, c.Principal) + suite.keeper.IndexCdpByCollateralRatio(suite.ctx, c.Collateral[0].Denom, c.ID, cr) + } + t := suite.keeper.GetAllCdps(suite.ctx) + suite.Equal(4, len(t)) +} + +func (suite *CdpTestSuite) TestIterateCdpsByDenom() { + cdps := cdps() + for _, c := range cdps { + suite.keeper.SetCDP(suite.ctx, c) + suite.keeper.IndexCdpByOwner(suite.ctx, c) + cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, c.Collateral, c.Principal) + suite.keeper.IndexCdpByCollateralRatio(suite.ctx, c.Collateral[0].Denom, c.ID, cr) + } + xrpCdps := suite.keeper.GetAllCdpsByDenom(suite.ctx, "xrp") + suite.Equal(3, len(xrpCdps)) + btcCdps := suite.keeper.GetAllCdpsByDenom(suite.ctx, "btc") + suite.Equal(1, len(btcCdps)) + suite.keeper.DeleteCDP(suite.ctx, cdps[0]) + suite.keeper.RemoveCdpOwnerIndex(suite.ctx, cdps[0]) + xrpCdps = suite.keeper.GetAllCdpsByDenom(suite.ctx, "xrp") + suite.Equal(2, len(xrpCdps)) + suite.keeper.DeleteCDP(suite.ctx, cdps[1]) + suite.keeper.RemoveCdpOwnerIndex(suite.ctx, cdps[1]) + ids, found := suite.keeper.GetCdpIdsByOwner(suite.ctx, cdps[1].Owner) + suite.True(found) + suite.Equal(1, len(ids)) + suite.Equal(uint64(3), ids[0]) +} + +func (suite *CdpTestSuite) TestIterateCdpsByCollateralRatio() { + cdps := cdps() + for _, c := range cdps { + suite.keeper.SetCDP(suite.ctx, c) + suite.keeper.IndexCdpByOwner(suite.ctx, c) + cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, c.Collateral, c.Principal) + suite.keeper.IndexCdpByCollateralRatio(suite.ctx, c.Collateral[0].Denom, c.ID, cr) + } + xrpCdps := suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("1.25")) + suite.Equal(0, len(xrpCdps)) + xrpCdps = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("1.25").Add(sdk.SmallestDec())) + suite.Equal(1, len(xrpCdps)) + xrpCdps = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("2.0").Add(sdk.SmallestDec())) + suite.Equal(2, len(xrpCdps)) + xrpCdps = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("100.0").Add(sdk.SmallestDec())) + suite.Equal(3, len(xrpCdps)) + suite.keeper.DeleteCDP(suite.ctx, cdps[0]) + suite.keeper.RemoveCdpOwnerIndex(suite.ctx, cdps[0]) + cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdps[0].Collateral, cdps[0].Principal) + suite.keeper.RemoveCdpCollateralRatioIndex(suite.ctx, cdps[0].Collateral[0].Denom, cdps[0].ID, cr) + xrpCdps = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("2.0").Add(sdk.SmallestDec())) + suite.Equal(1, len(xrpCdps)) +} + +func (suite *CdpTestSuite) TestValidateCollateral() { + c := sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(1))) + err := suite.keeper.ValidateCollateral(suite.ctx, c) + suite.NoError(err) + c = sdk.NewCoins(sdk.NewCoin("lol", sdk.NewInt(1))) + err = suite.keeper.ValidateCollateral(suite.ctx, c) + suite.Equal(types.CodeCollateralNotSupported, err.Result().Code) + c = sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1)), sdk.NewCoin("xrp", sdk.NewInt(1))) + err = suite.keeper.ValidateCollateral(suite.ctx, c) + suite.Equal(types.CodeCollateralLengthInvalid, err.Result().Code) +} + +func (suite *CdpTestSuite) TestValidatePrincipal() { + d := sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(10000000))) + err := suite.keeper.ValidatePrincipalAdd(suite.ctx, d) + suite.NoError(err) + d = sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(10000000)), sdk.NewCoin("susd", sdk.NewInt(10000000))) + err = suite.keeper.ValidatePrincipalAdd(suite.ctx, d) + suite.NoError(err) + d = sdk.NewCoins(sdk.NewCoin("xusd", sdk.NewInt(1))) + err = suite.keeper.ValidatePrincipalAdd(suite.ctx, d) + suite.Equal(types.CodeDebtNotSupported, err.Result().Code) + d = sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(1000000000001))) + err = suite.keeper.ValidatePrincipalAdd(suite.ctx, d) + suite.Equal(types.CodeExceedsDebtLimit, err.Result().Code) +} + +func (suite *CdpTestSuite) TestCalculateCollateralizationRatio() { + c := cdps()[1] + suite.keeper.SetCDP(suite.ctx, c) + suite.keeper.IndexCdpByOwner(suite.ctx, c) + cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, c.Collateral, c.Principal) + suite.keeper.IndexCdpByCollateralRatio(suite.ctx, c.Collateral[0].Denom, c.ID, cr) + cr, err := suite.keeper.CalculateCollateralizationRatio(suite.ctx, c.Collateral, c.Principal, c.AccumulatedFees) + suite.NoError(err) + suite.Equal(d("2.5"), cr) + c.AccumulatedFees = sdk.NewCoins(sdk.NewCoin("usdx", i(10000000))) + cr, err = suite.keeper.CalculateCollateralizationRatio(suite.ctx, c.Collateral, c.Principal, c.AccumulatedFees) + suite.NoError(err) + suite.Equal(d("1.25"), cr) +} + +func (suite *CdpTestSuite) TestMintBurnDebtCoins() { + cd := cdps()[1] + err := suite.keeper.MintDebtCoins(suite.ctx, types.ModuleName, suite.keeper.GetDebtDenom(suite.ctx), cd.Principal) + suite.NoError(err) + err = suite.keeper.MintDebtCoins(suite.ctx, "notamodule", suite.keeper.GetDebtDenom(suite.ctx), cd.Principal) + suite.Error(err) + sk := suite.app.GetSupplyKeeper() + acc := sk.GetModuleAccount(suite.ctx, types.ModuleName) + suite.Equal(cs(c("debt", 10000000)), acc.GetCoins()) + + err = suite.keeper.BurnDebtCoins(suite.ctx, types.ModuleName, suite.keeper.GetDebtDenom(suite.ctx), cd.Principal) + suite.NoError(err) + err = suite.keeper.BurnDebtCoins(suite.ctx, "notamodule", suite.keeper.GetDebtDenom(suite.ctx), cd.Principal) + suite.Error(err) + sk = suite.app.GetSupplyKeeper() + acc = sk.GetModuleAccount(suite.ctx, types.ModuleName) + suite.Equal(sdk.Coins(nil), acc.GetCoins()) +} + +func TestCdpTestSuite(t *testing.T) { + suite.Run(t, new(CdpTestSuite)) +} diff --git a/x/cdp/keeper/deposit.go b/x/cdp/keeper/deposit.go new file mode 100644 index 00000000..7bfa2293 --- /dev/null +++ b/x/cdp/keeper/deposit.go @@ -0,0 +1,223 @@ +package keeper + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/cdp/types" +) + +// DepositCollateral adds collateral to a cdp +func (k Keeper) DepositCollateral(ctx sdk.Context, owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coins) sdk.Error { + err := k.ValidateCollateral(ctx, collateral) + if err != nil { + return err + } + cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral[0].Denom) + if !found { + return types.ErrCdpNotFound(k.codespace, owner, collateral[0].Denom) + } + // deposits blocked if cdp is in liquidation, have to check all deposits + err = k.ValidateAvailableCDP(ctx, cdp.ID) + if err != nil { + return err + } + + deposit, found := k.GetDeposit(ctx, types.StatusNil, cdp.ID, depositor) + if found { + deposit.Amount = deposit.Amount.Add(collateral) + } else { + deposit = types.NewDeposit(cdp.ID, depositor, collateral) + } + err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, depositor, types.ModuleName, collateral) + if err != nil { + return err + } + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeCdpDeposit, + sdk.NewAttribute(sdk.AttributeKeyAmount, collateral.String()), + sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)), + ), + ) + + k.SetDeposit(ctx, deposit) + + periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix())) + fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom) + oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees)) + k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral[0].Denom, cdp.ID, oldCollateralToDebtRatio) + + cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees) + cdp.FeesUpdated = ctx.BlockTime() + cdp.Collateral = cdp.Collateral.Add(collateral) + collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, collateral, cdp.Principal.Add(cdp.AccumulatedFees)) + k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio) + return nil +} + +// WithdrawCollateral removes collateral from a cdp if it does not put the cdp below the liquidation ratio +func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coins) sdk.Error { + err := k.ValidateCollateral(ctx, collateral) + if err != nil { + return err + } + cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, collateral[0].Denom) + if !found { + return types.ErrCdpNotFound(k.codespace, owner, collateral[0].Denom) + } + // withdrawals blocked if cdp is in liquidation + err = k.ValidateAvailableCDP(ctx, cdp.ID) + if err != nil { + return err + } + deposit, found := k.GetDeposit(ctx, types.StatusNil, cdp.ID, depositor) + if !found { + return types.ErrDepositNotFound(k.codespace, depositor, cdp.ID) + } + if collateral.IsAnyGT(deposit.Amount) { + return types.ErrInvalidWithdrawAmount(k.codespace, collateral, deposit.Amount) + } + + periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix())) + fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom) + collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, cdp.Collateral.Sub(collateral), cdp.Principal, cdp.AccumulatedFees.Add(fees)) + if err != nil { + return err + } + liquidationRatio := k.getLiquidationRatio(ctx, collateral[0].Denom) + if collateralizationRatio.LT(liquidationRatio) { + return types.ErrInvalidCollateralRatio(k.codespace, collateral[0].Denom, collateralizationRatio, liquidationRatio) + } + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeCdpWithdrawal, + sdk.NewAttribute(sdk.AttributeKeyAmount, collateral.String()), + sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)), + ), + ) + + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, depositor, collateral) + if err != nil { + panic(err) + } + oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees)) + k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral[0].Denom, cdp.ID, oldCollateralToDebtRatio) + + cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees) + cdp.FeesUpdated = ctx.BlockTime() + cdp.Collateral = cdp.Collateral.Sub(collateral) + collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, collateral, cdp.Principal.Add(cdp.AccumulatedFees)) + k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio) + + deposit.Amount = deposit.Amount.Sub(collateral) + if deposit.Amount.IsZero() { + k.DeleteDeposit(ctx, types.StatusNil, deposit.CdpID, deposit.Depositor) + } else { + k.SetDeposit(ctx, deposit) + } + return nil +} + +// ValidateAvailableCDP validates that the deposits of a cdp are not in liquidation +func (k Keeper) ValidateAvailableCDP(ctx sdk.Context, cdpID uint64) sdk.Error { + deposits := k.GetDeposits(ctx, cdpID) + for _, d := range deposits { + if d.InLiquidation { + return types.ErrCdpNotAvailable(k.codespace, cdpID) + } + } + return nil +} + +// GetDeposit returns the deposit of a depositor on a particular cdp from the store +func (k Keeper) GetDeposit(ctx sdk.Context, status types.DepositStatus, cdpID uint64, depositor sdk.AccAddress) (deposit types.Deposit, found bool) { + store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix) + bz := store.Get(types.DepositKey(status, cdpID, depositor)) + if bz == nil { + return deposit, false + } + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &deposit) + return deposit, true + +} + +// SetDeposit sets the deposit in the store +func (k Keeper) SetDeposit(ctx sdk.Context, deposit types.Deposit) { + store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix) + bz := k.cdc.MustMarshalBinaryLengthPrefixed(deposit) + if deposit.InLiquidation { + store.Set(types.DepositKey(types.StatusLiquidated, deposit.CdpID, deposit.Depositor), bz) + return + } + store.Set(types.DepositKey(types.StatusNil, deposit.CdpID, deposit.Depositor), bz) + +} + +// DeleteDeposit deletes a deposit from the store +func (k Keeper) DeleteDeposit(ctx sdk.Context, status types.DepositStatus, cdpID uint64, depositor sdk.AccAddress) { + store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix) + store.Delete(types.DepositKey(status, cdpID, depositor)) +} + +// IterateDeposits iterates over the all the deposits of a cdp and performs a callback function +func (k Keeper) IterateDeposits(ctx sdk.Context, cdpID uint64, cb func(deposit types.Deposit) (stop bool)) { + store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix) + iterator := sdk.KVStorePrefixIterator(store, types.DepositIterKey(types.StatusNil, cdpID)) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var deposit types.Deposit + k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &deposit) + + if cb(deposit) { + break + } + } + iterator = sdk.KVStorePrefixIterator(store, types.DepositIterKey(types.StatusLiquidated, cdpID)) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var deposit types.Deposit + k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &deposit) + + if cb(deposit) { + break + } + } +} + +// GetDeposits returns all the deposits to a cdp +func (k Keeper) GetDeposits(ctx sdk.Context, cdpID uint64) (deposits types.Deposits) { + k.IterateDeposits(ctx, cdpID, func(deposit types.Deposit) bool { + deposits = append(deposits, deposit) + return false + }) + return +} + +// IterateLiquidatedDeposits iterates over the all liquidated deposits performs a callback function +func (k Keeper) IterateLiquidatedDeposits(ctx sdk.Context, cb func(deposit types.Deposit) (stop bool)) { + store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix) + iterator := sdk.KVStorePrefixIterator(store, []byte{types.StatusLiquidated.AsByte()}) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var deposit types.Deposit + k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &deposit) + + if cb(deposit) { + break + } + } +} + +// GetAllLiquidatedDeposits returns all deposits with status liquidated +func (k Keeper) GetAllLiquidatedDeposits(ctx sdk.Context) (deposits types.Deposits) { + k.IterateLiquidatedDeposits(ctx, func(deposit types.Deposit) bool { + deposits = append(deposits, deposit) + return false + }) + return +} diff --git a/x/cdp/keeper/deposit_test.go b/x/cdp/keeper/deposit_test.go new file mode 100644 index 00000000..8f7cb316 --- /dev/null +++ b/x/cdp/keeper/deposit_test.go @@ -0,0 +1,154 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/cdp/keeper" + "github.com/kava-labs/kava/x/cdp/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type DepositTestSuite struct { + suite.Suite + + keeper keeper.Keeper + app app.TestApp + ctx sdk.Context + addrs []sdk.AccAddress +} + +func (suite *DepositTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + _, addrs := app.GeneratePrivKeyAddressPairs(10) + authGS := app.NewAuthGenState( + addrs[0:2], + []sdk.Coins{ + cs(c("xrp", 500000000), c("btc", 500000000)), + cs(c("xrp", 200000000))}) + tApp.InitializeFromGenesisStates( + authGS, + NewPricefeedGenStateMulti(), + NewCDPGenStateMulti(), + ) + keeper := tApp.GetCDPKeeper() + suite.app = tApp + suite.keeper = keeper + suite.ctx = ctx + suite.addrs = addrs + err := suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 400000000)), cs(c("usdx", 10000000))) + suite.NoError(err) +} + +func (suite *DepositTestSuite) TestGetSetDeposit() { + d, found := suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + suite.True(found) + td := types.NewDeposit(uint64(1), suite.addrs[0], cs(c("xrp", 400000000))) + suite.True(d.Equals(td)) + ds := suite.keeper.GetDeposits(suite.ctx, uint64(1)) + suite.Equal(1, len(ds)) + suite.True(ds[0].Equals(td)) + suite.keeper.DeleteDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + _, found = suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + suite.False(found) + ds = suite.keeper.GetDeposits(suite.ctx, uint64(1)) + suite.Equal(0, len(ds)) +} + +func (suite *DepositTestSuite) TestDepositCollateral() { + err := suite.keeper.DepositCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 10000000))) + suite.NoError(err) + d, found := suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + suite.True(found) + td := types.NewDeposit(uint64(1), suite.addrs[0], cs(c("xrp", 410000000))) + suite.True(d.Equals(td)) + ds := suite.keeper.GetDeposits(suite.ctx, uint64(1)) + suite.Equal(1, len(ds)) + suite.True(ds[0].Equals(td)) + cd, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1)) + suite.Equal(cs(c("xrp", 410000000)), cd.Collateral) + ak := suite.app.GetAccountKeeper() + acc := ak.GetAccount(suite.ctx, suite.addrs[0]) + suite.Equal(i(90000000), acc.GetCoins().AmountOf("xrp")) + + err = suite.keeper.DepositCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("btc", 1))) + suite.Equal(types.CodeCdpNotFound, err.Result().Code) + + err = suite.keeper.DepositCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], cs(c("xrp", 1))) + suite.Equal(types.CodeCdpNotFound, err.Result().Code) + + err = suite.keeper.DepositCollateral(suite.ctx, suite.addrs[0], suite.addrs[1], cs(c("xrp", 10000000))) + suite.NoError(err) + d, found = suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[1]) + suite.True(found) + td = types.NewDeposit(uint64(1), suite.addrs[1], cs(c("xrp", 10000000))) + suite.True(d.Equals(td)) + ds = suite.keeper.GetDeposits(suite.ctx, uint64(1)) + suite.Equal(2, len(ds)) + suite.True(ds[1].Equals(td)) +} + +func (suite *DepositTestSuite) TestWithdrawCollateral() { + err := suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 321000000))) + suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code) + err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], cs(c("xrp", 10000000))) + suite.Equal(types.CodeCdpNotFound, err.Result().Code) + + d, _ := suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + d.InLiquidation = true + suite.keeper.DeleteDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + suite.keeper.SetDeposit(suite.ctx, d) + _, f := suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + suite.False(f) + + err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 10000000))) + suite.Equal(types.CodeCdpNotAvailable, err.Result().Code) + + d, f = suite.keeper.GetDeposit(suite.ctx, types.StatusLiquidated, uint64(1), suite.addrs[0]) + suite.True(f) + suite.keeper.DeleteDeposit(suite.ctx, types.StatusLiquidated, uint64(1), suite.addrs[0]) + d.InLiquidation = false + suite.keeper.SetDeposit(suite.ctx, d) + _, f = suite.keeper.GetDeposit(suite.ctx, types.StatusLiquidated, uint64(1), suite.addrs[0]) + suite.False(f) + + cd, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1)) + cd.AccumulatedFees = cs(c("usdx", 1)) + suite.keeper.SetCDP(suite.ctx, cd) + err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 320000000))) + suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code) + + err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 10000000))) + suite.NoError(err) + d, _ = suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + td := types.NewDeposit(uint64(1), suite.addrs[0], cs(c("xrp", 390000000))) + suite.True(d.Equals(td)) + ak := suite.app.GetAccountKeeper() + acc := ak.GetAccount(suite.ctx, suite.addrs[0]) + suite.Equal(i(110000000), acc.GetCoins().AmountOf("xrp")) + + err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[1], cs(c("xrp", 10000000))) + suite.Equal(types.CodeDepositNotFound, err.Result().Code) +} + +func (suite *DepositTestSuite) TestIterateLiquidatedDeposits() { + for j := 0; j < 10; j++ { + d := types.NewDeposit(uint64(j+2), suite.addrs[j], cs(c("xrp", 1000000))) + if j%2 == 0 { + d.InLiquidation = true + } + suite.keeper.SetDeposit(suite.ctx, d) + } + ds := suite.keeper.GetAllLiquidatedDeposits(suite.ctx) + for _, d := range ds { + suite.True(d.InLiquidation) + } + suite.Equal(5, len(ds)) +} +func TestDepositTestSuite(t *testing.T) { + suite.Run(t, new(DepositTestSuite)) +} diff --git a/x/cdp/keeper/draw.go b/x/cdp/keeper/draw.go new file mode 100644 index 00000000..4571edf4 --- /dev/null +++ b/x/cdp/keeper/draw.go @@ -0,0 +1,216 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/cdp/types" +) + +// AddPrincipal adds debt to a cdp if the additional debt does not put the cdp below the liquidation ratio +func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string, principal sdk.Coins) sdk.Error { + // validation + cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, denom) + if !found { + return types.ErrCdpNotFound(k.codespace, owner, denom) + } + err := k.ValidateAvailableCDP(ctx, cdp.ID) + if err != nil { + return err + } + err = k.ValidatePrincipalDraw(ctx, principal) + if err != nil { + return err + } + + // fee calculation + periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix())) + fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom) + + err = k.ValidateCollateralizationRatio(ctx, cdp.Collateral, cdp.Principal.Add(principal), cdp.AccumulatedFees.Add(fees)) + if err != nil { + return err + } + + // mint the principal and send it to the cdp owner + err = k.supplyKeeper.MintCoins(ctx, types.ModuleName, principal) + if err != nil { + panic(err) + } + err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, owner, principal) + if err != nil { + panic(err) + } + + // mint the corresponding amount of debt coins in the cdp module account + err = k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), principal) + if err != nil { + panic(err) + } + + // emit cdp draw event + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeCdpDraw, + sdk.NewAttribute(sdk.AttributeKeyAmount, principal.String()), + sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)), + ), + ) + + // remove old collateral:debt index + oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees)) + k.RemoveCdpCollateralRatioIndex(ctx, denom, cdp.ID, oldCollateralToDebtRatio) + + // update cdp state + cdp.Principal = cdp.Principal.Add(principal) + cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees) + cdp.FeesUpdated = ctx.BlockTime() + + // increment total principal for the input collateral type + k.IncrementTotalPrincipal(ctx, cdp.Collateral[0].Denom, principal) + + // set cdp state and indexes in the store + collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees)) + k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio) + + return nil +} + +// RepayPrincipal removes debt from the cdp +// If all debt is repaid, the collateral is returned to depositors and the cdp is removed from the store +func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string, payment sdk.Coins) sdk.Error { + // validation + cdp, found := k.GetCdpByOwnerAndDenom(ctx, owner, denom) + if !found { + return types.ErrCdpNotFound(k.codespace, owner, denom) + } + err := k.ValidateAvailableCDP(ctx, cdp.ID) + if err != nil { + return err + } + err = k.ValidatePaymentCoins(ctx, cdp, payment) + if err != nil { + return err + } + + // calculate fees + periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix())) + fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom) + + // calculate fee and principal payment + feePayment, principalPayment := k.calculatePayment(ctx, cdp.AccumulatedFees.Add(fees), payment) + + // send the payment from the sender to the cpd module + err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, payment) + if err != nil { + return err + } + + // burn the payment coins + err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, payment) + if err != nil { + panic(err) + } + + // burn the corresponding amount of debt coins + err = k.BurnDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), payment) + if err != nil { + panic(err) + } + + // emit repayment event + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeCdpRepay, + sdk.NewAttribute(sdk.AttributeKeyAmount, payment.String()), + sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)), + ), + ) + + // remove the old collateral:debt ratio index + oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees)) + k.RemoveCdpCollateralRatioIndex(ctx, denom, cdp.ID, oldCollateralToDebtRatio) + + // update cdp state + if !principalPayment.IsZero() { + cdp.Principal = cdp.Principal.Sub(principalPayment) + } + cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees).Sub(feePayment) + cdp.FeesUpdated = ctx.BlockTime() + + // decrement the total principal for the input collateral type + k.DecrementTotalPrincipal(ctx, denom, payment) + + // if the debt is fully paid, return collateral to depositors, + // and remove the cdp and indexes from the store + if cdp.Principal.IsZero() && cdp.AccumulatedFees.IsZero() { + k.ReturnCollateral(ctx, cdp) + k.DeleteCDP(ctx, cdp) + k.RemoveCdpOwnerIndex(ctx, cdp) + + // emit cdp close event + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeCdpClose, + sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)), + ), + ) + return nil + } + + // set cdp state and update indexes + collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees)) + k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio) + return nil +} + +// ValidatePaymentCoins validates that the input coins are valid for repaying debt +func (k Keeper) ValidatePaymentCoins(ctx sdk.Context, cdp types.CDP, payment sdk.Coins) sdk.Error { + subset := payment.DenomsSubsetOf(cdp.Principal) + if !subset { + var paymentDenoms []string + var principalDenoms []string + for _, pc := range cdp.Principal { + principalDenoms = append(principalDenoms, pc.Denom) + } + for _, pc := range payment { + paymentDenoms = append(paymentDenoms, pc.Denom) + } + return types.ErrInvalidPaymentDenom(k.codespace, cdp.ID, principalDenoms, paymentDenoms) + } + return nil +} + +// ReturnCollateral returns collateral to depositors on a cdp and removes deposits from the store +func (k Keeper) ReturnCollateral(ctx sdk.Context, cdp types.CDP) { + deposits := k.GetDeposits(ctx, cdp.ID) + for _, deposit := range deposits { + err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, deposit.Depositor, deposit.Amount) + if err != nil { + panic(err) + } + k.DeleteDeposit(ctx, types.StatusNil, cdp.ID, deposit.Depositor) + } +} + +func (k Keeper) calculatePayment(ctx sdk.Context, fees sdk.Coins, payment sdk.Coins) (sdk.Coins, sdk.Coins) { + // divides repayment into principal and fee components, with fee payment applied first. + feePayment := sdk.NewCoins() + principalPayment := sdk.NewCoins() + if fees.IsZero() { + return sdk.NewCoins(), payment + } + for _, fc := range fees { + if payment.AmountOf(fc.Denom).IsPositive() { + if payment.AmountOf(fc.Denom).GT(fc.Amount) { + feePayment = feePayment.Add(sdk.NewCoins(fc)) + pc := sdk.NewCoin(fc.Denom, payment.AmountOf(fc.Denom).Sub(fc.Amount)) + principalPayment = principalPayment.Add(sdk.NewCoins(pc)) + } else { + fc := sdk.NewCoin(fc.Denom, payment.AmountOf(fc.Denom)) + feePayment = feePayment.Add(sdk.NewCoins(fc)) + } + } + } + return feePayment, principalPayment +} diff --git a/x/cdp/keeper/draw_test.go b/x/cdp/keeper/draw_test.go new file mode 100644 index 00000000..c8f97998 --- /dev/null +++ b/x/cdp/keeper/draw_test.go @@ -0,0 +1,198 @@ +package keeper_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/cdp/keeper" + "github.com/kava-labs/kava/x/cdp/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type DrawTestSuite struct { + suite.Suite + + keeper keeper.Keeper + app app.TestApp + ctx sdk.Context + addrs []sdk.AccAddress +} + +func (suite *DrawTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + _, addrs := app.GeneratePrivKeyAddressPairs(3) + authGS := app.NewAuthGenState( + addrs, + []sdk.Coins{ + cs(c("xrp", 500000000), c("btc", 500000000)), + cs(c("xrp", 200000000)), + cs(c("xrp", 10000000000000), c("usdx", 100000000000))}) + tApp.InitializeFromGenesisStates( + authGS, + NewPricefeedGenStateMulti(), + NewCDPGenStateMulti(), + ) + keeper := tApp.GetCDPKeeper() + suite.app = tApp + suite.keeper = keeper + suite.ctx = ctx + suite.addrs = addrs + err := suite.keeper.AddCdp(suite.ctx, addrs[0], cs(c("xrp", 400000000)), cs(c("usdx", 10000000))) + suite.NoError(err) +} + +func (suite *DrawTestSuite) TestAddRepayPrincipal() { + + err := suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000))) + suite.NoError(err) + + t, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1)) + suite.Equal(cs(c("usdx", 20000000)), t.Principal) + ctd := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees)) + suite.Equal(d("20.0"), ctd) + ts := suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("20.0")) + suite.Equal(0, len(ts)) + ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("20.0").Add(sdk.SmallestDec())) + suite.Equal(ts[0], t) + tp := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") + suite.Equal(i(20000000), tp) + sk := suite.app.GetSupplyKeeper() + acc := sk.GetModuleAccount(suite.ctx, types.ModuleName) + suite.Equal(cs(c("xrp", 400000000), c("debt", 20000000)), acc.GetCoins()) + + err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("susd", 10000000))) + suite.NoError(err) + t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1)) + suite.Equal(cs(c("usdx", 20000000), c("susd", 10000000)), t.Principal) + ctd = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees)) + suite.Equal(d("400000000").Quo(d("30000000")), ctd) + ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("400").Quo(d("30"))) + suite.Equal(0, len(ts)) + ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("400").Quo(d("30")).Add(sdk.SmallestDec())) + suite.Equal(ts[0], t) + tp = suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "susd") + suite.Equal(i(10000000), tp) + sk = suite.app.GetSupplyKeeper() + acc = sk.GetModuleAccount(suite.ctx, types.ModuleName) + suite.Equal(cs(c("xrp", 400000000), c("debt", 30000000)), acc.GetCoins()) + + err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[1], "xrp", cs(c("usdx", 10000000))) + suite.Equal(types.CodeCdpNotFound, err.Result().Code) + err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("xusd", 10000000))) + suite.Equal(types.CodeDebtNotSupported, err.Result().Code) + err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 311000000))) + suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code) + + err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000))) + suite.NoError(err) + + t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1)) + suite.Equal(cs(c("usdx", 10000000), c("susd", 10000000)), t.Principal) + ctd = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees)) + suite.Equal(d("20.0"), ctd) + ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("20.0")) + suite.Equal(0, len(ts)) + ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("20.0").Add(sdk.SmallestDec())) + suite.Equal(ts[0], t) + tp = suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") + suite.Equal(i(10000000), tp) + sk = suite.app.GetSupplyKeeper() + acc = sk.GetModuleAccount(suite.ctx, types.ModuleName) + suite.Equal(cs(c("xrp", 400000000), c("debt", 20000000)), acc.GetCoins()) + + err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("susd", 10000000))) + suite.NoError(err) + + t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1)) + suite.Equal(cs(c("usdx", 10000000)), t.Principal) + ctd = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, t.Collateral, t.Principal.Add(t.AccumulatedFees)) + suite.Equal(d("40.0"), ctd) + ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("40.0")) + suite.Equal(0, len(ts)) + ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", d("40.0").Add(sdk.SmallestDec())) + suite.Equal(ts[0], t) + tp = suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "susd") + suite.Equal(i(0), tp) + sk = suite.app.GetSupplyKeeper() + acc = sk.GetModuleAccount(suite.ctx, types.ModuleName) + suite.Equal(cs(c("xrp", 400000000), c("debt", 10000000)), acc.GetCoins()) + + err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("xusd", 10000000))) + suite.Equal(types.CodeInvalidPaymentDenom, err.Result().Code) + err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[1], "xrp", cs(c("xusd", 10000000))) + suite.Equal(types.CodeCdpNotFound, err.Result().Code) + err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 100000000))) + suite.Error(err) + + err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000))) + suite.NoError(err) + + _, found := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1)) + suite.False(found) + ts = suite.keeper.GetAllCdpsByDenomAndRatio(suite.ctx, "xrp", types.MaxSortableDec) + suite.Equal(0, len(ts)) + ts = suite.keeper.GetAllCdpsByDenom(suite.ctx, "xrp") + suite.Equal(0, len(ts)) + sk = suite.app.GetSupplyKeeper() + acc = sk.GetModuleAccount(suite.ctx, types.ModuleName) + suite.Equal(sdk.Coins(nil), acc.GetCoins()) + +} + +func (suite *DrawTestSuite) TestAddRepayPrincipalFees() { + err := suite.keeper.AddCdp(suite.ctx, suite.addrs[2], cs(c("xrp", 1000000000000)), cs(c("usdx", 100000000000))) + suite.NoError(err) + suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Minute * 10)) + err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[2], "xrp", cs(c("usdx", 10000000))) + suite.NoError(err) + t, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2)) + suite.Equal(cs(c("usdx", 92827)), t.AccumulatedFees) + _ = suite.keeper.MintDebtCoins(suite.ctx, types.ModuleName, "debt", cs(c("usdx", 92827))) + err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[2], "xrp", cs(c("usdx", 100))) + suite.NoError(err) + t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2)) + suite.Equal(cs(c("usdx", 92727)), t.AccumulatedFees) + err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[2], "xrp", cs(c("usdx", 100010092727))) + suite.NoError(err) + _, f := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2)) + suite.False(f) + + err = suite.keeper.AddCdp(suite.ctx, suite.addrs[2], cs(c("xrp", 1000000000000)), cs(c("usdx", 100000000))) + suite.NoError(err) + + suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 31536000)) + err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[2], "xrp", cs(c("usdx", 100000000))) + suite.NoError(err) + t, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(3)) + suite.Equal(cs(c("usdx", 5000000)), t.AccumulatedFees) +} + +func (suite *DrawTestSuite) TestPricefeedFailure() { + ctx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 2)) + pfk := suite.app.GetPriceFeedKeeper() + pfk.SetCurrentPrices(ctx, "xrp:usd") + err := suite.keeper.AddPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10))) + suite.Error(err) + err = suite.keeper.RepayPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10))) + suite.NoError(err) +} + +func (suite *DrawTestSuite) TestModuleAccountFailure() { + suite.Panics(func() { + ctx := suite.ctx.WithBlockHeader(suite.ctx.BlockHeader()) + sk := suite.app.GetSupplyKeeper() + acc := sk.GetModuleAccount(ctx, types.ModuleName) + ak := suite.app.GetAccountKeeper() + ak.RemoveAccount(ctx, acc) + _ = suite.keeper.RepayPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10))) + }) +} + +func TestDrawTestSuite(t *testing.T) { + suite.Run(t, new(DrawTestSuite)) +} diff --git a/x/cdp/keeper/fees.go b/x/cdp/keeper/fees.go new file mode 100644 index 00000000..3b5a5679 --- /dev/null +++ b/x/cdp/keeper/fees.go @@ -0,0 +1,102 @@ +package keeper + +import ( + "fmt" + "time" + + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/cdp/types" +) + +// CalculateFees returns the fees accumulated since fees were last calculated based on +// the input amount of outstanding debt (principal) and the number of periods (seconds) that have passed +func (k Keeper) CalculateFees(ctx sdk.Context, principal sdk.Coins, periods sdk.Int, denom string) sdk.Coins { + newFees := sdk.NewCoins() + for _, pc := range principal { + // how fees are calculated: + // feesAccumulated = (outstandingDebt * (feeRate^periods)) - outstandingDebt + // Note that since we can't do x^y using sdk.Decimal, we are converting to int and using RelativePow + feePerSecond := k.GetFeeRate(ctx, denom) + scalar := sdk.NewInt(1000000000000000000) + feeRateInt := feePerSecond.Mul(sdk.NewDecFromInt(scalar)).TruncateInt() + accumulator := sdk.NewDecFromInt(types.RelativePow(feeRateInt, periods, scalar)).Mul(sdk.SmallestDec()) + feesAccumulated := (sdk.NewDecFromInt(pc.Amount).Mul(accumulator)).Sub(sdk.NewDecFromInt(pc.Amount)) + // TODO this will always round down, causing precision loss between the sum of all fees in CDPs and surplus coins in liquidator account + newFees = newFees.Add(sdk.NewCoins(sdk.NewCoin(pc.Denom, feesAccumulated.TruncateInt()))) + } + return newFees +} + +// IncrementTotalPrincipal increments the total amount of debt that has been drawn with that collateral type +func (k Keeper) IncrementTotalPrincipal(ctx sdk.Context, collateralDenom string, principal sdk.Coins) { + for _, pc := range principal { + total := k.GetTotalPrincipal(ctx, collateralDenom, pc.Denom) + total = total.Add(pc.Amount) + k.SetTotalPrincipal(ctx, collateralDenom, pc.Denom, total) + } +} + +// DecrementTotalPrincipal decrements the total amount of debt that has been drawn for a particular collateral type +func (k Keeper) DecrementTotalPrincipal(ctx sdk.Context, collateralDenom string, principal sdk.Coins) { + for _, pc := range principal { + total := k.GetTotalPrincipal(ctx, collateralDenom, pc.Denom) + total = total.Sub(pc.Amount) + if total.IsNegative() { + // can happen in tests due to rounding errors in fee calculation + total = sdk.ZeroInt() + } + k.SetTotalPrincipal(ctx, collateralDenom, pc.Denom, total) + } +} + +// GetTotalPrincipal returns the total amount of principal that has been drawn for a particular collateral +func (k Keeper) GetTotalPrincipal(ctx sdk.Context, collateralDenom string, principalDenom string) (total sdk.Int) { + store := prefix.NewStore(ctx.KVStore(k.key), types.PrincipalKeyPrefix) + bz := store.Get([]byte(collateralDenom + principalDenom)) + if bz == nil { + panic(fmt.Sprintf("total principal of %s for %s collateral not set in genesis", principalDenom, collateralDenom)) + } + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &total) + return total +} + +// SetTotalPrincipal sets the total amount of principal that has been drawn for the input collateral +func (k Keeper) SetTotalPrincipal(ctx sdk.Context, collateralDenom string, principalDenom string, total sdk.Int) { + store := prefix.NewStore(ctx.KVStore(k.key), types.PrincipalKeyPrefix) + store.Set([]byte(collateralDenom+principalDenom), k.cdc.MustMarshalBinaryLengthPrefixed(total)) +} + +// GetFeeRate returns the per second fee rate for the input denom +func (k Keeper) GetFeeRate(ctx sdk.Context, denom string) (fee sdk.Dec) { + store := prefix.NewStore(ctx.KVStore(k.key), types.AccumulatorKeyPrefix) + bz := store.Get([]byte(denom)) + if bz == nil { + panic(fmt.Sprintf("fee rate for %s not set in genesis", denom)) + } + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &fee) + return fee +} + +// SetFeeRate sets the per second fee rate for the input denom +func (k Keeper) SetFeeRate(ctx sdk.Context, denom string, fee sdk.Dec) { + store := prefix.NewStore(ctx.KVStore(k.key), types.AccumulatorKeyPrefix) + store.Set([]byte(denom), k.cdc.MustMarshalBinaryLengthPrefixed(fee)) +} + +// GetPreviousBlockTime get the blocktime for the previous block +func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time, found bool) { + store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey) + b := store.Get([]byte{}) + if b == nil { + return time.Time{}, false + } + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &blockTime) + return blockTime, true +} + +// SetPreviousBlockTime set the time of the previous block +func (k Keeper) SetPreviousBlockTime(ctx sdk.Context, blockTime time.Time) { + store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey) + store.Set([]byte{}, k.cdc.MustMarshalBinaryLengthPrefixed(blockTime)) +} diff --git a/x/cdp/keeper/fees_test.go b/x/cdp/keeper/fees_test.go new file mode 100644 index 00000000..48a5fd2d --- /dev/null +++ b/x/cdp/keeper/fees_test.go @@ -0,0 +1,95 @@ +package keeper_test + +import ( + "math/rand" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/cdp/keeper" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type FeeTestSuite struct { + suite.Suite + + keeper keeper.Keeper + app app.TestApp + ctx sdk.Context +} + +func (suite *FeeTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + tApp.InitializeFromGenesisStates( + NewPricefeedGenStateMulti(), + NewCDPGenStateMulti(), + ) + keeper := tApp.GetCDPKeeper() + suite.app = tApp + suite.ctx = ctx + suite.keeper = keeper +} + +func (suite *FeeTestSuite) TestCalculateFeesPrecisionLoss() { + // Calculates the difference between fees calculated on the total amount of debt, + // versus iterating over all the 1000 randomly generated cdps. + // Assumes 7 second block times, runs simulations for 100, 1000, 10000, 100000, and 1000000 + // blocks, where the bulk debt is updated each block, and the cdps are updated once. + coins := []sdk.Coins{} + total := sdk.NewCoins() + for i := 0; i < 1000; i++ { + ri, err := simulation.RandPositiveInt(rand.New(rand.NewSource(int64(i))), sdk.NewInt(100000000000)) + suite.NoError(err) + c := sdk.NewCoins(sdk.NewCoin("usdx", ri)) + coins = append(coins, c) + total = total.Add(cs(sdk.NewCoin("usdx", ri))) + } + + numBlocks := []int{100, 1000, 10000, 100000} + + for _, nb := range numBlocks { + bulkFees := sdk.NewCoins() + individualFees := sdk.NewCoins() + for x := 0; x < nb; x++ { + fee := suite.keeper.CalculateFees(suite.ctx, total.Add(bulkFees), i(7), "xrp") + bulkFees = bulkFees.Add(fee) + } + + for _, cns := range coins { + fee := suite.keeper.CalculateFees(suite.ctx, cns, i(int64(nb*7)), "xrp") + individualFees = individualFees.Add(fee) + } + + absError := (sdk.OneDec().Sub(sdk.NewDecFromInt(bulkFees[0].Amount).Quo(sdk.NewDecFromInt(individualFees[0].Amount)))).Abs() + + suite.T().Log(bulkFees) + suite.T().Log(individualFees) + suite.T().Log(absError) + + suite.True(d("0.00001").GTE(absError)) + } + +} + +func (suite *FeeTestSuite) TestGetSetPreviousBlockTime() { + now := tmtime.Now() + + _, f := suite.keeper.GetPreviousBlockTime(suite.ctx) + suite.False(f) + + suite.NotPanics(func() { suite.keeper.SetPreviousBlockTime(suite.ctx, now) }) + + bpt, f := suite.keeper.GetPreviousBlockTime(suite.ctx) + suite.True(f) + suite.Equal(now, bpt) + +} + +func TestFeeTestSuite(t *testing.T) { + suite.Run(t, new(FeeTestSuite)) +} diff --git a/x/cdp/keeper/integration_test.go b/x/cdp/keeper/integration_test.go index 23fd6722..fd8997f2 100644 --- a/x/cdp/keeper/integration_test.go +++ b/x/cdp/keeper/integration_test.go @@ -7,11 +7,11 @@ import ( "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/cdp" - "github.com/kava-labs/kava/x/cdp/types" "github.com/kava-labs/kava/x/pricefeed" + tmtime "github.com/tendermint/tendermint/types/time" ) -// Avoid cluttering test cases with long function name +// Avoid cluttering test cases with long function names 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) } @@ -21,12 +21,12 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { pfGenesis := pricefeed.GenesisState{ Params: pricefeed.Params{ Markets: []pricefeed.Market{ - pricefeed.Market{MarketID: asset, BaseAsset: asset, QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, + pricefeed.Market{MarketID: asset + ":usd", BaseAsset: asset, QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, }, }, PostedPrices: []pricefeed.PostedPrice{ pricefeed.PostedPrice{ - MarketID: asset, + MarketID: asset + ":usd", OracleAddress: sdk.AccAddress{}, Price: price, Expiry: time.Now().Add(1 * time.Hour), @@ -38,18 +38,33 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { cdpGenesis := cdp.GenesisState{ - Params: cdp.CdpParams{ - GlobalDebtLimit: sdk.NewInt(1000000), - CollateralParams: []cdp.CollateralParams{ + Params: cdp.Params{ + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + CollateralParams: cdp.CollateralParams{ { Denom: asset, LiquidationRatio: liquidationRatio, - DebtLimit: sdk.NewInt(500000), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr + Prefix: 0x20, + ConversionFactor: i(6), + MarketID: asset + ":usd", + }, + }, + DebtParams: cdp.DebtParams{ + { + Denom: "usdx", + ReferenceAsset: "usd", + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + ConversionFactor: i(6), + DebtFloor: i(10000000), }, }, }, - GlobalDebt: sdk.ZeroInt(), - CDPs: cdp.CDPs{}, + StartingCdpID: cdp.DefaultCdpStartingID, + DebtDenom: cdp.DefaultDebtDenom, + CDPs: cdp.CDPs{}, + PreviousBlockTime: cdp.DefaultPreviousBlockTime, } return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} } @@ -58,19 +73,19 @@ func NewPricefeedGenStateMulti() app.GenesisState { pfGenesis := pricefeed.GenesisState{ Params: pricefeed.Params{ Markets: []pricefeed.Market{ - pricefeed.Market{MarketID: "btc", BaseAsset: "btc", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, - pricefeed.Market{MarketID: "xrp", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, + pricefeed.Market{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, + pricefeed.Market{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, }, }, PostedPrices: []pricefeed.PostedPrice{ pricefeed.PostedPrice{ - MarketID: "btc", + MarketID: "btc:usd", OracleAddress: sdk.AccAddress{}, Price: sdk.MustNewDecFromStr("8000.00"), Expiry: time.Now().Add(1 * time.Hour), }, pricefeed.PostedPrice{ - MarketID: "xrp", + MarketID: "xrp:usd", OracleAddress: sdk.AccAddress{}, Price: sdk.MustNewDecFromStr("0.25"), Expiry: time.Now().Add(1 * time.Hour), @@ -81,23 +96,59 @@ func NewPricefeedGenStateMulti() app.GenesisState { } func NewCDPGenStateMulti() app.GenesisState { cdpGenesis := cdp.GenesisState{ - Params: cdp.CdpParams{ - GlobalDebtLimit: sdk.NewInt(1000000), - CollateralParams: []types.CollateralParams{ - { - Denom: "btc", - LiquidationRatio: sdk.MustNewDecFromStr("1.5"), - DebtLimit: sdk.NewInt(500000), - }, + Params: cdp.Params{ + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + CollateralParams: cdp.CollateralParams{ { Denom: "xrp", LiquidationRatio: sdk.MustNewDecFromStr("2.0"), - DebtLimit: sdk.NewInt(500000), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr + Prefix: 0x20, + MarketID: "xrp:usd", + ConversionFactor: i(6), + }, + { + Denom: "btc", + LiquidationRatio: sdk.MustNewDecFromStr("1.5"), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr + Prefix: 0x21, + MarketID: "btc:usd", + ConversionFactor: i(8), + }, + }, + DebtParams: cdp.DebtParams{ + { + Denom: "usdx", + ReferenceAsset: "usd", + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + ConversionFactor: i(6), + DebtFloor: i(10000000), + }, + { + Denom: "susd", + ReferenceAsset: "usd", + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("susd", 1000000000000)), + ConversionFactor: i(6), + DebtFloor: i(10000000), }, }, }, - GlobalDebt: sdk.ZeroInt(), - CDPs: cdp.CDPs{}, + StartingCdpID: cdp.DefaultCdpStartingID, + DebtDenom: cdp.DefaultDebtDenom, + CDPs: cdp.CDPs{}, + PreviousBlockTime: cdp.DefaultPreviousBlockTime, } return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} } + +func cdps() (cdps cdp.CDPs) { + _, addrs := app.GeneratePrivKeyAddressPairs(3) + c1 := cdp.NewCDP(uint64(1), addrs[0], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(10000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(8000000))), tmtime.Canonical(time.Now())) + c2 := cdp.NewCDP(uint64(2), addrs[1], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(100000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(10000000))), tmtime.Canonical(time.Now())) + c3 := cdp.NewCDP(uint64(3), addrs[1], sdk.NewCoins(sdk.NewCoin("btc", sdk.NewInt(1000000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(10000000))), tmtime.Canonical(time.Now())) + c4 := cdp.NewCDP(uint64(4), addrs[2], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(1000000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(500000000))), tmtime.Canonical(time.Now())) + cdps = append(cdps, c1, c2, c3, c4) + return +} diff --git a/x/cdp/keeper/keeper.go b/x/cdp/keeper/keeper.go index 83774e52..90c3410e 100644 --- a/x/cdp/keeper/keeper.go +++ b/x/cdp/keeper/keeper.go @@ -1,499 +1,103 @@ package keeper import ( - "bytes" "fmt" - "sort" "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" 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 +// Keeper keeper for the cdp module type Keeper struct { key sdk.StoreKey cdc *codec.Codec paramSubspace subspace.Subspace pricefeedKeeper types.PricefeedKeeper - bankKeeper types.BankKeeper + supplyKeeper types.SupplyKeeper + codespace sdk.CodespaceType } // NewKeeper creates a new keeper -func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, bk types.BankKeeper) Keeper { +func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, sk types.SupplyKeeper, codespace sdk.CodespaceType) Keeper { + + // ensure module account is set + if addr := sk.GetModuleAddress(types.ModuleName); addr == nil { + panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) + } + return Keeper{ key: key, cdc: cdc, paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()), pricefeedKeeper: pfk, - bankKeeper: bk, + supplyKeeper: sk, + codespace: codespace, } } -// 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 +// CdpDenomIndexIterator returns an sdk.Iterator for all cdps with matching collateral denom +func (k Keeper) CdpDenomIndexIterator(ctx sdk.Context, denom string) sdk.Iterator { + store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix) + db, _ := k.GetDenomPrefix(ctx, denom) + return sdk.KVStorePrefixIterator(store, types.DenomIterKey(db)) } -// 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 +// CdpCollateralRatioIndexIterator returns an sdk.Iterator for all cdps that have collateral denom +// matching denom and collateral:debt ratio LESS THAN targetRatio +func (k Keeper) CdpCollateralRatioIndexIterator(ctx sdk.Context, denom string, targetRatio sdk.Dec) sdk.Iterator { + store := prefix.NewStore(ctx.KVStore(k.key), types.CollateralRatioIndexPrefix) + db, _ := k.GetDenomPrefix(ctx, denom) + return store.Iterator(types.CollateralRatioIterKey(db, sdk.ZeroDec()), types.CollateralRatioIterKey(db, targetRatio)) } -// 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() { +// IterateAllCdps iterates over all cdps and performs a callback function +func (k Keeper) IterateAllCdps(ctx sdk.Context, cb func(cdp types.CDP) (stop bool)) { + store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix) + iterator := sdk.KVStorePrefixIterator(store, []byte{}) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { var cdp types.CDP - k.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &cdp) - cdps = append(cdps, cdp) - } + k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &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) + if cb(cdp) { + break } } - return filteredCoins +} + +// IterateCdpsByDenom iterates over cdps with matching denom and performs a callback function +func (k Keeper) IterateCdpsByDenom(ctx sdk.Context, denom string, cb func(cdp types.CDP) (stop bool)) { + iterator := k.CdpDenomIndexIterator(ctx, denom) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var cdp types.CDP + k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &cdp) + if cb(cdp) { + break + } + } +} + +// IterateCdpsByCollateralRatio iterate over cdps with collateral denom equal to denom and +// collateral:debt ratio LESS THAN targetRatio and performs a callback function. +func (k Keeper) IterateCdpsByCollateralRatio(ctx sdk.Context, denom string, targetRatio sdk.Dec, cb func(cdp types.CDP) (stop bool)) { + iterator := k.CdpCollateralRatioIndexIterator(ctx, denom, targetRatio) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + db, id, _ := types.SplitCollateralRatioKey(iterator.Key()) + d := k.getDenomFromByte(ctx, db) + cdp, found := k.GetCDP(ctx, d, id) + if !found { + panic(fmt.Sprintf("cdp %d does not exist", id)) + } + if cb(cdp) { + break + } + + } } diff --git a/x/cdp/keeper/keeper_test.go b/x/cdp/keeper/keeper_test.go deleted file mode 100644 index 35f2b696..00000000 --- a/x/cdp/keeper/keeper_test.go +++ /dev/null @@ -1,316 +0,0 @@ -package keeper_test - -import ( - "fmt" - "testing" - "time" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" - - "github.com/kava-labs/kava/app" - "github.com/kava-labs/kava/x/cdp/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 := app.GeneratePrivKeyAddressPairs(1) - ownerAddr := addrs[0] - - type state struct { - 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 test app - tApp := app.NewTestApp() - // initialize cdp owner account with coins, and collateral with price and params - tApp.InitializeFromGenesisStates( - app.NewAuthGenState([]sdk.AccAddress{ownerAddr}, []sdk.Coins{tc.priorState.OwnerCoins}), - NewPricefeedGenState("xrp", d(tc.price)), - NewCDPGenState("xrp", d("2.0")), - ) - // create a context for db access - ctx := tApp.NewContext(false, abci.Header{}) - - // setup store state - keeper := tApp.GetCDPKeeper() - 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) - - // get new state for verification - actualCDP, found := keeper.GetCDP(ctx, tc.args.owner, tc.args.collateralDenom) - // 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 - tApp.CheckBalance(t, ctx, ownerAddr, tc.expectedState.OwnerCoins) - }) - } -} - -func TestKeeper_PartialSeizeCDP(t *testing.T) { - // Setup - const collateral = "xrp" - _, addrs := app.GeneratePrivKeyAddressPairs(1) - testAddr := addrs[0] - - tApp := app.NewTestApp() - tApp.InitializeFromGenesisStates( - app.NewAuthGenState(addrs, []sdk.Coins{cs(c(collateral, 100))}), - NewPricefeedGenState(collateral, d("1.00")), - NewCDPGenState(collateral, d("2.00")), - ) - - ctx := tApp.NewContext(false, abci.Header{}) - keeper := tApp.GetCDPKeeper() - - // Create CDP - err := keeper.ModifyCDP(ctx, testAddr, collateral, i(10), i(5)) - require.NoError(t, err) - // Reduce price - tApp.GetPriceFeedKeeper().SetPrice( - ctx, sdk.AccAddress{}, collateral, - d("0.90"), time.Now().Add(1*time.Hour)) - tApp.GetPriceFeedKeeper().SetCurrentPrices(ctx, collateral) - - // 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 test app - tApp := app.NewTestApp().InitializeFromGenesisStates( - NewPricefeedGenStateMulti(), // collateral needs to be in pricefeed for cdp InitGenesis to validate - NewCDPGenStateMulti(), - ) - ctx := tApp.NewContext(true, abci.Header{}) - keeper := tApp.GetCDPKeeper() - - // setup CDPs - _, addrs := app.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 - tApp := app.NewTestApp() - ctx := tApp.NewContext(true, abci.Header{}) - keeper := tApp.GetCDPKeeper() - - _, addrs := app.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 - tApp := app.NewTestApp() - ctx := tApp.NewContext(true, abci.Header{}) - keeper := tApp.GetCDPKeeper() - - 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 - tApp := app.NewTestApp() - ctx := tApp.NewContext(true, abci.Header{}) - keeper := tApp.GetCDPKeeper() - - 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) -} diff --git a/x/cdp/keeper/params.go b/x/cdp/keeper/params.go index 4b12e9ae..d7d34156 100644 --- a/x/cdp/keeper/params.go +++ b/x/cdp/keeper/params.go @@ -1,19 +1,81 @@ package keeper import ( + "fmt" + 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 +func (k Keeper) GetParams(ctx sdk.Context) types.Params { + var p types.Params 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) -} \ No newline at end of file +func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { + k.paramSubspace.SetParamSet(ctx, ¶ms) +} + +// GetCollateral returns the collateral param with corresponding denom +func (k Keeper) GetCollateral(ctx sdk.Context, denom string) (types.CollateralParam, bool) { + params := k.GetParams(ctx) + for _, cp := range params.CollateralParams { + if cp.Denom == denom { + return cp, true + } + } + return types.CollateralParam{}, false +} + +// GetDebt returns the debt param with matching denom +func (k Keeper) GetDebt(ctx sdk.Context, denom string) (types.DebtParam, bool) { + params := k.GetParams(ctx) + for _, dp := range params.DebtParams { + if dp.Denom == denom { + return dp, true + } + } + return types.DebtParam{}, false +} + +// GetDenomPrefix returns the prefix of the matching denom +func (k Keeper) GetDenomPrefix(ctx sdk.Context, denom string) (byte, bool) { + params := k.GetParams(ctx) + for _, cp := range params.CollateralParams { + if cp.Denom == denom { + return cp.Prefix, true + } + } + return 0x00, false +} + +// private methods panic if the input is invalid + +func (k Keeper) getDenomFromByte(ctx sdk.Context, db byte) string { + params := k.GetParams(ctx) + for _, cp := range params.CollateralParams { + if cp.Prefix == db { + return cp.Denom + } + } + panic(fmt.Sprintf("no collateral denom with prefix %b", db)) +} + +func (k Keeper) getMarketID(ctx sdk.Context, denom string) string { + cp, found := k.GetCollateral(ctx, denom) + if !found { + panic(fmt.Sprintf("collateral not found: %s", denom)) + } + return cp.MarketID +} + +func (k Keeper) getLiquidationRatio(ctx sdk.Context, denom string) sdk.Dec { + cp, found := k.GetCollateral(ctx, denom) + if !found { + panic(fmt.Sprintf("collateral not found: %s", denom)) + } + return cp.LiquidationRatio +} diff --git a/x/cdp/keeper/querier.go b/x/cdp/keeper/querier.go index c8d3f04f..ec83986a 100644 --- a/x/cdp/keeper/querier.go +++ b/x/cdp/keeper/querier.go @@ -10,13 +10,16 @@ import ( "github.com/kava-labs/kava/x/cdp/types" ) - - +// NewQuerier returns a new querier function 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.QueryGetCdp: + return queryGetCdp(ctx, req, keeper) case types.QueryGetCdps: - return queryGetCdps(ctx, req, keeper) + return queryGetCdpsByDenom(ctx, req, keeper) + case types.QueryGetCdpsByCollateralization: + return queryGetCdpsByRatio(ctx, req, keeper) case types.QueryGetParams: return queryGetParams(ctx, req, keeper) default: @@ -25,42 +28,45 @@ func NewQuerier(keeper Keeper) sdk.Querier { } } - -// 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 +// query a specific cdp +func queryGetCdp(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + var requestParams types.QueryCdpParams 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 - } - + _, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom) + if !valid { + return nil, types.ErrInvalidCollateralDenom(keeper.codespace, requestParams.CollateralDenom) } - // Encode results + cdp, found := keeper.GetCdpByOwnerAndDenom(ctx, requestParams.Owner, requestParams.CollateralDenom) + if !found { + return nil, types.ErrCdpNotFound(keeper.codespace, requestParams.Owner, requestParams.CollateralDenom) + } + + bz, err := codec.MarshalJSONIndent(keeper.cdc, cdp) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + return bz, nil + +} + +// query cdps with matching denom and ratio LESS THAN the input ratio +func queryGetCdpsByRatio(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + var requestParams types.QueryCdpsByRatioParams + err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams) + if err != nil { + return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err)) + } + _, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom) + if !valid { + return nil, types.ErrInvalidCollateralDenom(keeper.codespace, requestParams.CollateralDenom) + } + + cdps := keeper.GetAllCdpsByDenomAndRatio(ctx, requestParams.CollateralDenom, requestParams.Ratio) bz, err := codec.MarshalJSONIndent(keeper.cdc, cdps) if err != nil { return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) @@ -68,8 +74,27 @@ func queryGetCdps(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte return bz, nil } -// queryGetParams fetches the cdp module parameters -// TODO does this need to exist? Can you use cliCtx.QueryStore instead? +// query all cdps with matching collateral denom +func queryGetCdpsByDenom(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + 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)) + } + _, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom) + if !valid { + return nil, types.ErrInvalidCollateralDenom(keeper.codespace, requestParams.CollateralDenom) + } + + cdps := keeper.GetAllCdpsByDenom(ctx, requestParams.CollateralDenom) + 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 +} + +// query params in the cdp store func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { // Get params params := keeper.GetParams(ctx) diff --git a/x/cdp/keeper/querier_test.go b/x/cdp/keeper/querier_test.go new file mode 100644 index 00000000..d13ec917 --- /dev/null +++ b/x/cdp/keeper/querier_test.go @@ -0,0 +1,226 @@ +package keeper_test + +import ( + "math/rand" + "sort" + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/cdp/keeper" + "github.com/kava-labs/kava/x/cdp/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +const ( + custom = "custom" +) + +type QuerierTestSuite struct { + suite.Suite + + keeper keeper.Keeper + addrs []sdk.AccAddress + app app.TestApp + cdps types.CDPs + ctx sdk.Context + querier sdk.Querier +} + +func (suite *QuerierTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + cdps := make(types.CDPs, 100) + _, addrs := app.GeneratePrivKeyAddressPairs(100) + coins := []sdk.Coins{} + + for j := 0; j < 100; j++ { + coins = append(coins, cs(c("btc", 10000000000), c("xrp", 10000000000))) + } + + authGS := app.NewAuthGenState( + addrs, coins) + tApp.InitializeFromGenesisStates( + authGS, + NewPricefeedGenStateMulti(), + NewCDPGenStateMulti(), + ) + + suite.ctx = ctx + suite.app = tApp + suite.keeper = tApp.GetCDPKeeper() + + for j := 0; j < 100; j++ { + collateral := "xrp" + amount := simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 2500000000, 9000000000) + debt := simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 50000000, 250000000) + if j%2 == 0 { + collateral = "btc" + amount = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 500000000, 5000000000) + debt = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 1000000000, 25000000000) + } + suite.Nil(suite.keeper.AddCdp(suite.ctx, addrs[j], cs(c(collateral, int64(amount))), cs(c("usdx", int64(debt))))) + c, f := suite.keeper.GetCDP(suite.ctx, collateral, uint64(j+1)) + suite.True(f) + cdps[j] = c + } + + suite.cdps = cdps + suite.querier = keeper.NewQuerier(suite.keeper) + suite.addrs = addrs +} + +func (suite *QuerierTestSuite) TestQueryCdp() { + ctx := suite.ctx.WithIsCheckTx(false) + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdp}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpParams(suite.cdps[0].Owner, suite.cdps[0].Collateral[0].Denom)), + } + bz, err := suite.querier(ctx, []string{types.QueryGetCdp}, query) + suite.Nil(err) + suite.NotNil(bz) + + var c types.CDP + suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c)) + suite.Equal(suite.cdps[0], c) + + query = abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdp}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpParams(suite.cdps[0].Owner, "lol")), + } + _, err = suite.querier(ctx, []string{types.QueryGetCdp}, query) + suite.Error(err) + + query = abci.RequestQuery{ + Path: strings.Join([]string{custom, "nonsense"}, "/"), + Data: []byte("nonsense"), + } + + _, err = suite.querier(ctx, []string{query.Path}, query) + suite.Error(err) + + _, err = suite.querier(ctx, []string{types.QueryGetCdp}, query) + suite.Error(err) + + query = abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdp}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpParams(suite.cdps[0].Owner, "xrp")), + } + _, err = suite.querier(ctx, []string{types.QueryGetCdp}, query) + suite.Error(err) + +} + +func (suite *QuerierTestSuite) TestQueryCdpsByDenom() { + ctx := suite.ctx.WithIsCheckTx(false) + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdps}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpsParams(suite.cdps[0].Collateral[0].Denom)), + } + bz, err := suite.querier(ctx, []string{types.QueryGetCdps}, query) + suite.Nil(err) + suite.NotNil(bz) + + var c types.CDPs + suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c)) + suite.Equal(50, len(c)) + + query = abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdps}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpsParams("lol")), + } + _, err = suite.querier(ctx, []string{types.QueryGetCdps}, query) + suite.Error(err) +} + +func (suite *QuerierTestSuite) TestQueryCdpsByRatio() { + ratioCountBtc := 0 + ratioCountXrp := 0 + xrpRatio := d("50.0") + btcRatio := d("0.003") + expectedXrpIds := []int{} + expectedBtcIds := []int{} + for _, cdp := range suite.cdps { + r := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Principal) + if cdp.Collateral[0].Denom == "xrp" { + if r.LT(xrpRatio) { + ratioCountXrp += 1 + expectedXrpIds = append(expectedXrpIds, int(cdp.ID)) + } + } else { + if r.LT(btcRatio) { + ratioCountBtc += 1 + expectedBtcIds = append(expectedBtcIds, int(cdp.ID)) + } + } + } + + ctx := suite.ctx.WithIsCheckTx(false) + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdpsByCollateralization}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpsByRatioParams("xrp", xrpRatio)), + } + bz, err := suite.querier(ctx, []string{types.QueryGetCdpsByCollateralization}, query) + suite.Nil(err) + suite.NotNil(bz) + + var c types.CDPs + actualXrpIds := []int{} + suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c)) + for _, k := range c { + actualXrpIds = append(actualXrpIds, int(k.ID)) + } + sort.Ints(actualXrpIds) + suite.Equal(expectedXrpIds, actualXrpIds) + + query = abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdpsByCollateralization}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpsByRatioParams("btc", btcRatio)), + } + bz, err = suite.querier(ctx, []string{types.QueryGetCdpsByCollateralization}, query) + suite.Nil(err) + suite.NotNil(bz) + + c = types.CDPs{} + actualBtcIds := []int{} + suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c)) + for _, k := range c { + actualBtcIds = append(actualBtcIds, int(k.ID)) + } + sort.Ints(actualBtcIds) + suite.Equal(expectedBtcIds, actualBtcIds) + + query = abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdpsByCollateralization}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpsByRatioParams("xrp", d("0.003"))), + } + bz, err = suite.querier(ctx, []string{types.QueryGetCdpsByCollateralization}, query) + suite.Nil(err) + suite.NotNil(bz) + c = types.CDPs{} + suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c)) + suite.Equal(0, len(c)) +} + +func (suite *QuerierTestSuite) TestQueryParams() { + ctx := suite.ctx.WithIsCheckTx(false) + bz, err := suite.querier(ctx, []string{types.QueryGetParams}, abci.RequestQuery{}) + suite.Nil(err) + suite.NotNil(bz) + + var p types.Params + suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &p)) + + cdpGS := NewCDPGenStateMulti() + gs := types.GenesisState{} + types.ModuleCdc.UnmarshalJSON(cdpGS["cdp"], &gs) + suite.Equal(gs.Params, p) +} + +func TestQuerierTestSuite(t *testing.T) { + suite.Run(t, new(QuerierTestSuite)) +} diff --git a/x/cdp/keeper/seize.go b/x/cdp/keeper/seize.go new file mode 100644 index 00000000..09598ecd --- /dev/null +++ b/x/cdp/keeper/seize.go @@ -0,0 +1,102 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/cdp/types" +) + +// SeizeCollateral liquidates the collateral in the input cdp. +// the following operations are performed: +// 1. updates the fees for the input cdp, +// 2. sends collateral for all deposits from the cdp module to the liquidator module, +// 3. moves debt coins from the cdp module to the liquidator module, +// 4. decrements the total amount of principal outstanding for that collateral type +// (this is the equivalent of saying that fees are no longer accumulated by a cdp once it +// gets liquidated) +func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) { + // Update fees + periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix())) + fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom) + cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees) + cdp.FeesUpdated = ctx.BlockTime() + + // Liquidate deposits + deposits := k.GetDeposits(ctx, cdp.ID) + for _, dep := range deposits { + if !dep.InLiquidation { + dep.InLiquidation = true + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeCdpLiquidation, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)), + sdk.NewAttribute(types.AttributeKeyDepositor, fmt.Sprintf("%s", dep.Depositor)), + ), + ) + k.DeleteDeposit(ctx, types.StatusNil, cdp.ID, dep.Depositor) + k.SetDeposit(ctx, dep) + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, dep.Amount) + if err != nil { + panic(err) + } + } else { + return + } + } + + // Transfer debt coins from cdp module account to liquidator module account + debtAmt := sdk.ZeroInt() + for _, dc := range cdp.Principal { + debtAmt = debtAmt.Add(dc.Amount) + } + for _, dc := range cdp.AccumulatedFees { + debtAmt = debtAmt.Add(dc.Amount) + } + debtCoins := sdk.NewCoins(sdk.NewCoin(k.GetDebtDenom(ctx), debtAmt)) + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, debtCoins) + if err != nil { + panic(err) + } + + // Decrement total principal for this collateral type + for _, dc := range cdp.Principal { + feeAmount := cdp.AccumulatedFees.AmountOf(dc.Denom) + coinsToDecrement := sdk.NewCoins(dc) + if feeAmount.IsPositive() { + feeCoins := sdk.NewCoins(sdk.NewCoin(dc.Denom, feeAmount)) + coinsToDecrement = coinsToDecrement.Add(feeCoins) + } + k.DecrementTotalPrincipal(ctx, cdp.Collateral[0].Denom, coinsToDecrement) + } +} + +// HandleNewDebt compounds the accumulated fees for the input collateral and principal coins. +// the following operations are performed: +// 1. mints the fee coins in the liquidator module account, +// 2. mints the same amount of debt coins in the cdp module account +// 3. updates the total amount of principal for the input collateral type in the store, +func (k Keeper) HandleNewDebt(ctx sdk.Context, collateralDenom string, principalDenom string, periods sdk.Int) { + previousDebt := k.GetTotalPrincipal(ctx, collateralDenom, principalDenom) + feeCoins := sdk.NewCoins(sdk.NewCoin(principalDenom, previousDebt)) + newFees := k.CalculateFees(ctx, feeCoins, periods, collateralDenom) + k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), newFees) + k.supplyKeeper.MintCoins(ctx, types.LiquidatorMacc, newFees) + k.SetTotalPrincipal(ctx, collateralDenom, principalDenom, feeCoins.Add(newFees).AmountOf(principalDenom)) +} + +// LiquidateCdps seizes collateral from all CDPs below the input liquidation ratio +func (k Keeper) LiquidateCdps(ctx sdk.Context, marketID string, denom string, liquidationRatio sdk.Dec) { + price, err := k.pricefeedKeeper.GetCurrentPrice(ctx, marketID) + if err != nil { + return + } + normalizedRatio := sdk.OneDec().Quo(price.Price.Quo(liquidationRatio)) + cdpsToLiquidate := k.GetAllCdpsByDenomAndRatio(ctx, denom, normalizedRatio) + for _, c := range cdpsToLiquidate { + k.SeizeCollateral(ctx, c) + } + return +} diff --git a/x/cdp/keeper/seize_test.go b/x/cdp/keeper/seize_test.go new file mode 100644 index 00000000..e40dc786 --- /dev/null +++ b/x/cdp/keeper/seize_test.go @@ -0,0 +1,141 @@ +package keeper_test + +import ( + "math/rand" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/cdp/keeper" + "github.com/kava-labs/kava/x/cdp/types" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type SeizeTestSuite struct { + suite.Suite + + keeper keeper.Keeper + addrs []sdk.AccAddress + app app.TestApp + cdps types.CDPs + ctx sdk.Context + liquidations liquidationTracker +} + +type liquidationTracker struct { + xrp []uint64 + btc []uint64 + debt int64 +} + +func (suite *SeizeTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + cdps := make(types.CDPs, 100) + _, addrs := app.GeneratePrivKeyAddressPairs(100) + coins := []sdk.Coins{} + tracker := liquidationTracker{} + + for j := 0; j < 100; j++ { + coins = append(coins, cs(c("btc", 100000000), c("xrp", 10000000000))) + } + + authGS := app.NewAuthGenState( + addrs, coins) + tApp.InitializeFromGenesisStates( + authGS, + NewPricefeedGenStateMulti(), + NewCDPGenStateMulti(), + ) + + suite.ctx = ctx + suite.app = tApp + suite.keeper = tApp.GetCDPKeeper() + + for j := 0; j < 100; j++ { + collateral := "xrp" + amount := 10000000000 + debt := simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 750000000, 1249000000) + if j%2 == 0 { + collateral = "btc" + amount = 100000000 + debt = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 2700000000, 5332000000) + if debt >= 4000000000 { + tracker.btc = append(tracker.btc, uint64(j+1)) + tracker.debt += int64(debt) + } + } else { + if debt >= 1000000000 { + tracker.xrp = append(tracker.xrp, uint64(j+1)) + tracker.debt += int64(debt) + } + } + err := suite.keeper.AddCdp(suite.ctx, addrs[j], cs(c(collateral, int64(amount))), cs(c("usdx", int64(debt)))) + suite.NoError(err) + c, f := suite.keeper.GetCDP(suite.ctx, collateral, uint64(j+1)) + suite.True(f) + cdps[j] = c + } + + suite.cdps = cdps + suite.addrs = addrs + suite.liquidations = tracker +} + +func (suite *SeizeTestSuite) setPrice(price sdk.Dec, market string) { + pfKeeper := suite.app.GetPriceFeedKeeper() + + pfKeeper.SetPrice(suite.ctx, sdk.AccAddress{}, market, price, suite.ctx.BlockTime().Add(time.Hour*3)) + err := pfKeeper.SetCurrentPrices(suite.ctx, market) + suite.NoError(err) + pp, err := pfKeeper.GetCurrentPrice(suite.ctx, market) + suite.NoError(err) + suite.Equal(price, pp.Price) +} + +func (suite *SeizeTestSuite) TestSeizeCollateral() { + sk := suite.app.GetSupplyKeeper() + cdp, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2)) + p := cdp.Principal[0].Amount + cl := cdp.Collateral[0].Amount + tpb := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") + suite.keeper.SeizeCollateral(suite.ctx, cdp) + tpa := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") + suite.Equal(tpb.Sub(tpa), p) + liqModAcc := sk.GetModuleAccount(suite.ctx, types.LiquidatorMacc) + suite.Equal(cs(c("debt", p.Int64()), c("xrp", cl.Int64())), liqModAcc.GetCoins()) + ak := suite.app.GetAccountKeeper() + acc := ak.GetAccount(suite.ctx, suite.addrs[1]) + suite.Equal(p.Int64(), acc.GetCoins().AmountOf("usdx").Int64()) + err := suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[1], cs(c("xrp", 10))) + suite.Equal(types.CodeCdpNotAvailable, err.Result().Code) +} + +func (suite *SeizeTestSuite) TestLiquidateCdps() { + sk := suite.app.GetSupplyKeeper() + acc := sk.GetModuleAccount(suite.ctx, types.ModuleName) + originalXrpCollateral := acc.GetCoins().AmountOf("xrp") + suite.setPrice(d("0.2"), "xrp:usd") + p, _ := suite.keeper.GetCollateral(suite.ctx, "xrp") + suite.keeper.LiquidateCdps(suite.ctx, "xrp:usd", "xrp", p.LiquidationRatio) + acc = sk.GetModuleAccount(suite.ctx, types.ModuleName) + finalXrpCollateral := acc.GetCoins().AmountOf("xrp") + seizedXrpCollateral := originalXrpCollateral.Sub(finalXrpCollateral) + xrpLiquidations := int(seizedXrpCollateral.Quo(i(10000000000)).Int64()) + suite.Equal(len(suite.liquidations.xrp), xrpLiquidations) +} + +func (suite *SeizeTestSuite) TestHandleNewDebt() { + tpb := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") + suite.keeper.HandleNewDebt(suite.ctx, "xrp", "usdx", i(31536000)) + tpa := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") + suite.Equal(sdk.NewDec(tpb.Int64()).Mul(d("1.05")).TruncateInt().Int64(), tpa.Int64()) +} + +func TestSeizeTestSuite(t *testing.T) { + suite.Run(t, new(SeizeTestSuite)) +} diff --git a/x/cdp/module.go b/x/cdp/module.go index 792b2cbe..99855986 100644 --- a/x/cdp/module.go +++ b/x/cdp/module.go @@ -41,12 +41,12 @@ func (AppModuleBasic) DefaultGenesis() json.RawMessage { // ValidateGenesis module validate genesis func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { - var data GenesisState - err := ModuleCdc.UnmarshalJSON(bz, &data) + var gs GenesisState + err := ModuleCdc.UnmarshalJSON(bz, &gs) if err != nil { return err } - return ValidateGenesis(data) + return gs.Validate() } // RegisterRESTRoutes registers the REST routes for the cdp module. @@ -124,9 +124,11 @@ func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { } // BeginBlock module begin-block -func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} +func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { + BeginBlocker(ctx, req, am.keeper) +} // EndBlock module end-block -func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { +func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { return []abci.ValidatorUpdate{} } diff --git a/x/cdp/types/cdp.go b/x/cdp/types/cdp.go new file mode 100644 index 00000000..c807b12a --- /dev/null +++ b/x/cdp/types/cdp.go @@ -0,0 +1,64 @@ +package types + +import ( + "fmt" + "strings" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// CDP is the state of a single collateralized debt position. +type CDP struct { + ID uint64 `json:"id" yaml:"id"` // unique id for cdp + Owner sdk.AccAddress `json:"owner" yaml:"owner"` // Account that authorizes changes to the CDP + Collateral sdk.Coins `json:"collateral" yaml:"collateral"` // Amount of collateral stored in this CDP + Principal sdk.Coins `json:"principal" yaml:"principal"` + AccumulatedFees sdk.Coins `json:"accumulated_fees" yaml:"accumulated_fees"` + FeesUpdated time.Time `json:"fees_updated" yaml:"fees_updated"` // Amount of stable coin drawn from this CDP +} + +// NewCDP creates a new CDP object +func NewCDP(id uint64, owner sdk.AccAddress, collateral sdk.Coins, principal sdk.Coins, time time.Time) CDP { + var fees sdk.Coins + return CDP{ + ID: id, + Owner: owner, + Collateral: collateral, + Principal: principal, + AccumulatedFees: fees, + FeesUpdated: time, + } +} + +// String implements fmt.stringer +func (cdp CDP) String() string { + return strings.TrimSpace(fmt.Sprintf(`CDP: + Owner: %s + ID: %d + Collateral Type: %s + Collateral: %s + Principal: %s + Fees: %s + Fees Last Updated: %s`, + cdp.Owner, + cdp.ID, + cdp.Collateral[0].Denom, + cdp.Collateral, + cdp.Principal, + cdp.AccumulatedFees, + cdp.FeesUpdated, + )) +} + +// CDPs a collection of CDP objects +type CDPs []CDP + +// String implements stringer +func (cdps CDPs) String() string { + out := "" + for _, cdp := range cdps { + out += cdp.String() + "\n" + } + return out +} diff --git a/x/cdp/types/codec.go b/x/cdp/types/codec.go index 118eb582..6db0831d 100644 --- a/x/cdp/types/codec.go +++ b/x/cdp/types/codec.go @@ -12,8 +12,12 @@ func init() { ModuleCdc = cdc.Seal() } -// RegisterCodec registers concrete types on the codec. +// RegisterCodec registers the necessary types for cdp module func RegisterCodec(cdc *codec.Codec) { - cdc.RegisterConcrete(MsgCreateOrModifyCDP{}, "cdp/MsgCreateOrModifyCDP", nil) + cdc.RegisterConcrete(MsgCreateCDP{}, "cdp/MsgCreateCDP", nil) + cdc.RegisterConcrete(MsgDeposit{}, "cdp/MsgDeposit", nil) + cdc.RegisterConcrete(MsgWithdraw{}, "cdp/MsgWithdraw", nil) + cdc.RegisterConcrete(MsgDrawDebt{}, "cdp/MsgDrawDebt", nil) + cdc.RegisterConcrete(MsgRepayDebt{}, "cdp/MsgRepayDebt", nil) cdc.RegisterConcrete(MsgTransferCDP{}, "cdp/MsgTransferCDP", nil) } diff --git a/x/cdp/types/deposit.go b/x/cdp/types/deposit.go new file mode 100644 index 00000000..5e6b0fae --- /dev/null +++ b/x/cdp/types/deposit.go @@ -0,0 +1,83 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Deposit defines an amount of coins deposited by an account to a cdp +type Deposit struct { + CdpID uint64 `json:"cdp_id" yaml:"cdp_id"` // cdpID of the cdp + Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"` // Address of the depositor + Amount sdk.Coins `json:"amount" yaml:"amount"` // Deposit amount + InLiquidation bool `json:"in_liquidation" yaml:"in_liquidation"` +} + +// DepositStatus is a type alias that represents a deposit status as a byte +type DepositStatus byte + +// Valid Deposit statuses +const ( + StatusNil DepositStatus = 0x00 + StatusLiquidated DepositStatus = 0x01 +) + +// AsByte returns the status as byte +func (ds DepositStatus) AsByte() byte { + return byte(ds) +} + +// StatusFromByte returns the status from its byte representation +func StatusFromByte(b byte) DepositStatus { + switch b { + case 0x00: + return StatusNil + case 0x01: + return StatusLiquidated + default: + panic(fmt.Sprintf("unrecognized deposit status, %v", b)) + } +} + +// NewDeposit creates a new Deposit object +func NewDeposit(cdpID uint64, depositor sdk.AccAddress, amount sdk.Coins) Deposit { + return Deposit{cdpID, depositor, amount, false} +} + +// String implements fmt.Stringer +func (d Deposit) String() string { + return fmt.Sprintf(`Deposit for CDP %d: + Depositor: %s + Amount: %s + In Liquidation: %t`, + d.CdpID, d.Depositor, d.Amount, d.InLiquidation) +} + +// Deposits a collection of Deposit objects +type Deposits []Deposit + +// String implements fmt.Stringer +func (ds Deposits) String() string { + if len(ds) == 0 { + return "[]" + } + out := fmt.Sprintf("Deposits for CDP %d:", ds[0].CdpID) + for _, dep := range ds { + out += fmt.Sprintf("\n %s: %s", dep.Depositor, dep.Amount) + if dep.InLiquidation { + out += fmt.Sprintf("(in liquidation)") + } + } + return out +} + +// Equals returns whether two deposits are equal. +func (d Deposit) Equals(comp Deposit) bool { + return d.Depositor.Equals(comp.Depositor) && d.CdpID == comp.CdpID && d.Amount.IsEqual(comp.Amount) +} + +// Empty returns whether a deposit is empty. +func (d Deposit) Empty() bool { + return d.Equals(Deposit{}) +} diff --git a/x/cdp/types/errors.go b/x/cdp/types/errors.go new file mode 100644 index 00000000..d7e14d73 --- /dev/null +++ b/x/cdp/types/errors.go @@ -0,0 +1,103 @@ +// DONTCOVER +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Error codes specific to cdp module +const ( + DefaultCodespace sdk.CodespaceType = ModuleName + CodeCdpAlreadyExists sdk.CodeType = 1 + CodeCollateralLengthInvalid sdk.CodeType = 2 + CodeCollateralNotSupported sdk.CodeType = 3 + CodeDebtNotSupported sdk.CodeType = 4 + CodeExceedsDebtLimit sdk.CodeType = 5 + CodeInvalidCollateralRatio sdk.CodeType = 6 + CodeCdpNotFound sdk.CodeType = 7 + CodeDepositNotFound sdk.CodeType = 8 + CodeInvalidDepositDenom sdk.CodeType = 9 + CodeInvalidPaymentDenom sdk.CodeType = 10 + CodeDepositNotAvailable sdk.CodeType = 11 + CodeInvalidCollateralDenom sdk.CodeType = 12 + CodeInvalidWithdrawAmount sdk.CodeType = 13 + CodeCdpNotAvailable sdk.CodeType = 14 + CodeBelowDebtFloor sdk.CodeType = 15 +) + +// ErrCdpAlreadyExists error for duplicate cdps +func ErrCdpAlreadyExists(codespace sdk.CodespaceType, owner sdk.AccAddress, denom string) sdk.Error { + return sdk.NewError(codespace, CodeCdpAlreadyExists, fmt.Sprintf("cdp for owner %s and collateral %s already exists", owner, denom)) +} + +// ErrInvalidCollateralLength error for invalid collateral input length +func ErrInvalidCollateralLength(codespace sdk.CodespaceType, length int) sdk.Error { + return sdk.NewError(codespace, CodeCollateralLengthInvalid, fmt.Sprintf("only one collateral type per cdp, has %d", length)) +} + +// ErrCollateralNotSupported error for unsupported collateral +func ErrCollateralNotSupported(codespace sdk.CodespaceType, denom string) sdk.Error { + return sdk.NewError(codespace, CodeCollateralNotSupported, fmt.Sprintf("collateral %s not supported", denom)) +} + +// ErrDebtNotSupported error for unsupported debt +func ErrDebtNotSupported(codespace sdk.CodespaceType, denom string) sdk.Error { + return sdk.NewError(codespace, CodeDebtNotSupported, fmt.Sprintf("collateral %s not supported", denom)) +} + +// ErrExceedsDebtLimit error for attempted draws that exceed debt limit +func ErrExceedsDebtLimit(codespace sdk.CodespaceType, proposed sdk.Coins, limit sdk.Coins) sdk.Error { + return sdk.NewError(codespace, CodeExceedsDebtLimit, fmt.Sprintf("proposed debt increase %s would exceed debt limit of %s", proposed, limit)) +} + +// ErrInvalidCollateralRatio error for attempted draws that are below liquidation ratio +func ErrInvalidCollateralRatio(codespace sdk.CodespaceType, denom string, collateralRatio sdk.Dec, liquidationRatio sdk.Dec) sdk.Error { + return sdk.NewError(codespace, CodeInvalidCollateralRatio, fmt.Sprintf("proposed collateral ratio of %s is below liqudation ratio of %s for collateral %s", collateralRatio, liquidationRatio, denom)) +} + +// ErrCdpNotFound error cdp not found +func ErrCdpNotFound(codespace sdk.CodespaceType, owner sdk.AccAddress, denom string) sdk.Error { + return sdk.NewError(codespace, CodeCdpNotFound, fmt.Sprintf("cdp for owner %s and collateral %s not found", owner, denom)) +} + +// ErrDepositNotFound error for deposit not found +func ErrDepositNotFound(codespace sdk.CodespaceType, depositor sdk.AccAddress, cdpID uint64) sdk.Error { + return sdk.NewError(codespace, CodeDepositNotFound, fmt.Sprintf("deposit for cdp %d not found for %s", cdpID, depositor)) +} + +// ErrInvalidDepositDenom error for invalid deposit denoms +func ErrInvalidDepositDenom(codespace sdk.CodespaceType, cdpID uint64, expected string, actual string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidDepositDenom, fmt.Sprintf("invalid deposit for cdp %d, expects %s, got %s", cdpID, expected, actual)) +} + +// ErrInvalidPaymentDenom error for invalid payment denoms +func ErrInvalidPaymentDenom(codespace sdk.CodespaceType, cdpID uint64, expected []string, actual []string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidPaymentDenom, fmt.Sprintf("invalid payment for cdp %d, expects %s, got %s", cdpID, expected, actual)) +} + +//ErrDepositNotAvailable error for withdrawing deposits in liquidation +func ErrDepositNotAvailable(codespace sdk.CodespaceType, cdpID uint64, depositor sdk.AccAddress) sdk.Error { + return sdk.NewError(codespace, CodeDepositNotAvailable, fmt.Sprintf("deposit from %s for cdp %d in liquidation", depositor, cdpID)) +} + +// ErrInvalidCollateralDenom error for invalid collateral denoms +func ErrInvalidCollateralDenom(codespace sdk.CodespaceType, denom string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidDepositDenom, fmt.Sprintf("invalid denom: %s", denom)) +} + +// ErrInvalidWithdrawAmount error for invalid withdrawal amount +func ErrInvalidWithdrawAmount(codespace sdk.CodespaceType, withdraw sdk.Coins, deposit sdk.Coins) sdk.Error { + return sdk.NewError(codespace, CodeInvalidWithdrawAmount, fmt.Sprintf("withdrawal amount of %s exceeds deposit of %s", withdraw, deposit)) +} + +//ErrCdpNotAvailable error for depositing to a CDP in liquidation +func ErrCdpNotAvailable(codespace sdk.CodespaceType, cdpID uint64) sdk.Error { + return sdk.NewError(codespace, CodeCdpNotAvailable, fmt.Sprintf("cannot modify cdp %d, in liquidation", cdpID)) +} + +// ErrBelowDebtFloor error for creating a cdp with debt below the minimum +func ErrBelowDebtFloor(codespace sdk.CodespaceType, debt sdk.Coins, floor sdk.Int) sdk.Error { + return sdk.NewError(codespace, CodeBelowDebtFloor, fmt.Sprintf("proposed cdp debt of %s is below the minimum of %s", debt, floor)) +} diff --git a/x/cdp/types/events.go b/x/cdp/types/events.go new file mode 100644 index 00000000..ba58c9d6 --- /dev/null +++ b/x/cdp/types/events.go @@ -0,0 +1,16 @@ +package types + +// Event types for cdp module +const ( + EventTypeCreateCdp = "create_cdp" + EventTypeCdpDeposit = "cdp_deposit" + EventTypeCdpDraw = "cdp_draw" + EventTypeCdpRepay = "cdp_repayment" + EventTypeCdpClose = "cdp_close" + EventTypeCdpWithdrawal = "cdp_withdrawal" + EventTypeCdpLiquidation = "cdp_liquidation" + + AttributeKeyCdpID = "cdp_id" + AttributeKeyDepositor = "depositor" + AttributeValueCategory = "cdp" +) diff --git a/x/cdp/types/expected_keepers.go b/x/cdp/types/expected_keepers.go index 41562789..c57ad94f 100644 --- a/x/cdp/types/expected_keepers.go +++ b/x/cdp/types/expected_keepers.go @@ -4,18 +4,28 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" + supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported" 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) +// SupplyKeeper defines the expected supply keeper for module accounts +type SupplyKeeper interface { + GetModuleAddress(name string) sdk.AccAddress + GetModuleAccount(ctx sdk.Context, name string) supplyexported.ModuleAccountI + + // TODO remove with genesis 2-phases refactor https://github.com/cosmos/cosmos-sdk/issues/2862 + SetModuleAccount(sdk.Context, supplyexported.ModuleAccountI) + + SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) sdk.Error + SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) sdk.Error + SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) sdk.Error + BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error + MintCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error } +// PricefeedKeeper defines the expected interface for the pricefeed type PricefeedKeeper interface { - GetCurrentPrice(sdk.Context, string) pftypes.CurrentPrice + GetCurrentPrice(sdk.Context, string) (pftypes.CurrentPrice, sdk.Error) GetParams(sdk.Context) pftypes.Params // These are used for testing TODO replace mockApp with keeper in tests to remove these SetParams(sdk.Context, pftypes.Params) diff --git a/x/cdp/types/genesis.go b/x/cdp/types/genesis.go index 04ac4654..5c4ebf61 100644 --- a/x/cdp/types/genesis.go +++ b/x/cdp/types/genesis.go @@ -1,34 +1,61 @@ package types -import sdk "github.com/cosmos/cosmos-sdk/types" +import ( + "bytes" + "fmt" + "time" +) // 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 + Params Params `json:"params" yaml:"params"` + CDPs CDPs `json:"cdps" yaml:"cdps"` + Deposits Deposits `json:"deposits" yaml:"deposits"` + StartingCdpID uint64 `json:"starting_cdp_id" yaml:"starting_cdp_id"` + DebtDenom string `json:"debt_denom" yaml:"debt_denom"` + PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"` } // DefaultGenesisState returns a default genesis state -// TODO make this empty, load test values independent func DefaultGenesisState() GenesisState { return GenesisState{ - Params: DefaultParams(), - CDPs: CDPs{}, + Params: DefaultParams(), + CDPs: CDPs{}, + Deposits: Deposits{}, + StartingCdpID: DefaultCdpStartingID, + DebtDenom: DefaultDebtDenom, + PreviousBlockTime: DefaultPreviousBlockTime, } } -// ValidateGenesis performs basic validation of genesis data returning an +// Validate performs basic validation of genesis data returning an // error for any failed validation criteria. -func ValidateGenesis(data GenesisState) error { +func (gs GenesisState) Validate() error { - if err := data.Params.Validate(); err != nil { + if err := gs.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. + if gs.PreviousBlockTime.Equal(time.Time{}) { + return fmt.Errorf("previous block time not set") + } + + if gs.DebtDenom == "" { + return fmt.Errorf("debt denom not set") + + } + return nil } + +// Equal checks whether two gov GenesisState structs are equivalent +func (gs GenesisState) Equal(gs2 GenesisState) bool { + b1 := ModuleCdc.MustMarshalBinaryBare(gs) + b2 := ModuleCdc.MustMarshalBinaryBare(gs2) + return bytes.Equal(b1, b2) +} + +// IsEmpty returns true if a GenesisState is empty +func (gs GenesisState) IsEmpty() bool { + return gs.Equal(GenesisState{}) +} diff --git a/x/cdp/types/genesis_test.go b/x/cdp/types/genesis_test.go new file mode 100644 index 00000000..f894492f --- /dev/null +++ b/x/cdp/types/genesis_test.go @@ -0,0 +1,22 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEqualProposalID(t *testing.T) { + state1 := GenesisState{} + state2 := GenesisState{} + require.Equal(t, state1, state2) + + // Proposals + state1.StartingCdpID = 1 + require.NotEqual(t, state1, state2) + require.False(t, state1.Equal(state2)) + + state2.StartingCdpID = 1 + require.Equal(t, state1, state2) + require.True(t, state1.Equal(state2)) +} \ No newline at end of file diff --git a/x/cdp/types/keys.go b/x/cdp/types/keys.go index e096801d..a128e387 100644 --- a/x/cdp/types/keys.go +++ b/x/cdp/types/keys.go @@ -1,5 +1,12 @@ package types +import ( + "bytes" + "encoding/binary" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + const ( // ModuleName The name that will be used throughout the module ModuleName = "cdp" @@ -10,6 +17,161 @@ const ( // RouterKey Top level router key RouterKey = ModuleName + // QuerierRoute Top level query string + QuerierRoute = ModuleName + // DefaultParamspace default name for parameter store DefaultParamspace = ModuleName -) \ No newline at end of file + + // LiquidatorMacc module account for liquidator + LiquidatorMacc = "liquidator" +) + +var sep = []byte(":") + +// Keys for cdp store +// Items are stored with the following key: values +// - 0x00: []cdpID +// - One cdp owner can control one cdp per collateral type +// - 0x01:: CDP +// - cdps are prefix by denom prefix so we can iterate over cdps of one type +// - uses : as separator +// - 0x02::: cdpID +// - Ox03: nextCdpID +// - 0x04: debtDenom +// - 0x05::: Deposit +// - 0x06:totalPrincipal +// - 0x07:feeRate +// - 0x08:previousBlockTime + +// KVStore key prefixes +var ( + CdpIDKeyPrefix = []byte{0x00} + CdpKeyPrefix = []byte{0x01} + CollateralRatioIndexPrefix = []byte{0x02} + CdpIDKey = []byte{0x03} + DebtDenomKey = []byte{0x04} + DepositKeyPrefix = []byte{0x05} + PrincipalKeyPrefix = []byte{0x06} + AccumulatorKeyPrefix = []byte{0x07} + PreviousBlockTimeKey = []byte{0x08} +) + +var lenPositiveDec = len(SortableDecBytes(sdk.OneDec())) +var lenNegativeDec = len(SortableDecBytes(sdk.OneDec().Neg())) + +// GetCdpIDBytes returns the byte representation of the cdpID +func GetCdpIDBytes(cdpID uint64) (cdpIDBz []byte) { + cdpIDBz = make([]byte, 8) + binary.BigEndian.PutUint64(cdpIDBz, cdpID) + return +} + +// GetCdpIDFromBytes returns cdpID in uint64 format from a byte array +func GetCdpIDFromBytes(bz []byte) (cdpID uint64) { + return binary.BigEndian.Uint64(bz) +} + +// CdpKey key of a specific cdp in the store +func CdpKey(denomByte byte, cdpID uint64) []byte { + return createKey([]byte{denomByte}, sep, GetCdpIDBytes(cdpID)) +} + +// SplitCdpKey returns the component parts of a cdp key +func SplitCdpKey(key []byte) (byte, uint64) { + split := bytes.Split(key, sep) + return split[0][0], GetCdpIDFromBytes(split[1]) +} + +// DenomIterKey returns the key for iterating over cdps of a certain denom in the store +func DenomIterKey(denomByte byte) []byte { + return append([]byte{denomByte}, sep...) +} + +// SplitDenomIterKey returns the component part of a key for iterating over cdps by denom +func SplitDenomIterKey(key []byte) byte { + split := bytes.Split(key, sep) + return split[0][0] +} + +// DepositKey key of a specific deposit in the store +func DepositKey(status DepositStatus, cdpID uint64, depositor sdk.AccAddress) []byte { + return createKey([]byte{status.AsByte()}, sep, GetCdpIDBytes(cdpID), sep, depositor) +} + +// SplitDepositKey returns the component parts of a deposit key +func SplitDepositKey(key []byte) (DepositStatus, uint64, sdk.AccAddress) { + status := StatusFromByte(key[0]) + cdpID := GetCdpIDFromBytes(key[2:10]) + addr := key[11:] + return status, cdpID, addr +} + +// DepositIterKey returns the prefix key for iterating over deposits to a cdp +func DepositIterKey(status DepositStatus, cdpID uint64) []byte { + return createKey([]byte{status.AsByte()}, sep, GetCdpIDBytes(cdpID)) +} + +// SplitDepositIterKey returns the component parts of a key for iterating over deposits on a cdp +func SplitDepositIterKey(key []byte) (status DepositStatus, cdpID uint64) { + status = StatusFromByte(key[0]) + cdpID = GetCdpIDFromBytes(key[2:]) + return status, cdpID +} + +// CollateralRatioBytes returns the liquidation ratio as sortable bytes +func CollateralRatioBytes(ratio sdk.Dec) []byte { + ok := ValidSortableDec(ratio) + if !ok { + // set to max sortable if input is too large. + ratio = sdk.OneDec().Quo(sdk.SmallestDec()) + } + return SortableDecBytes(ratio) +} + +// CollateralRatioKey returns the key for querying a cdp by its liquidation ratio +func CollateralRatioKey(denomByte byte, cdpID uint64, ratio sdk.Dec) []byte { + ratioBytes := CollateralRatioBytes(ratio) + idBytes := GetCdpIDBytes(cdpID) + + return createKey([]byte{denomByte}, sep, ratioBytes, sep, idBytes) +} + +// SplitCollateralRatioKey split the collateral ratio key and return the denom, cdp id, and collateral:debt ratio +func SplitCollateralRatioKey(key []byte) (denom byte, cdpID uint64, ratio sdk.Dec) { + + cdpID = GetCdpIDFromBytes(key[len(key)-8 : len(key)]) + split := bytes.Split(key[:len(key)-8], sep) + denom = split[0][0] + + ratio, err := ParseDecBytes(split[1]) + if err != nil { + panic(err) + } + return +} + +// CollateralRatioIterKey returns the key for iterating over cdps by denom and liquidation ratio +func CollateralRatioIterKey(denomByte byte, ratio sdk.Dec) []byte { + ratioBytes := CollateralRatioBytes(ratio) + return createKey([]byte{denomByte}, sep, ratioBytes) +} + +// SplitCollateralRatioIterKey split the collateral ratio key and return the denom, cdp id, and collateral:debt ratio +func SplitCollateralRatioIterKey(key []byte) (denom byte, ratio sdk.Dec) { + split := bytes.Split(key, sep) + denom = split[0][0] + + ratio, err := ParseDecBytes(split[1]) + if err != nil { + panic(err) + } + return +} + +func createKey(bytes ...[]byte) (r []byte) { + for _, b := range bytes { + r = append(r, b...) + } + return +} diff --git a/x/cdp/types/keys_test.go b/x/cdp/types/keys_test.go new file mode 100644 index 00000000..51d4de26 --- /dev/null +++ b/x/cdp/types/keys_test.go @@ -0,0 +1,65 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/ed25519" +) + +var addr = sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + +func TestKeys(t *testing.T) { + key := CdpKey(0x01, 2) + db, id := SplitCdpKey(key) + require.Equal(t, int(id), 2) + require.Equal(t, byte(0x01), db) + + denomKey := DenomIterKey(0x01) + db = SplitDenomIterKey(denomKey) + require.Equal(t, byte(0x01), db) + + depositKey := DepositKey(StatusNil, 2, addr) + status, id, a := SplitDepositKey(depositKey) + require.Equal(t, 2, int(id)) + require.Equal(t, a, addr) + require.Equal(t, StatusNil, status) + + depositIterKey := DepositIterKey(StatusLiquidated, 2) + status, id = SplitDepositIterKey(depositIterKey) + require.Equal(t, 2, int(id)) + require.Equal(t, StatusLiquidated, status) + + require.Panics(t, func() { SplitDepositIterKey(append([]byte{0x03}, GetCdpIDBytes(2)...)) }) + + collateralKey := CollateralRatioKey(0x01, 2, sdk.MustNewDecFromStr("1.50")) + db, id, ratio := SplitCollateralRatioKey(collateralKey) + require.Equal(t, byte(0x01), db) + require.Equal(t, int(id), 2) + require.Equal(t, ratio, sdk.MustNewDecFromStr("1.50")) + + bigRatio := sdk.OneDec().Quo(sdk.SmallestDec()).Mul(sdk.OneDec().Add(sdk.OneDec())) + collateralKey = CollateralRatioKey(0x01, 2, bigRatio) + db, id, ratio = SplitCollateralRatioKey(collateralKey) + require.Equal(t, ratio, MaxSortableDec) + + collateralIterKey := CollateralRatioIterKey(0x01, sdk.MustNewDecFromStr("1.50")) + db, ratio = SplitCollateralRatioIterKey(collateralIterKey) + require.Equal(t, byte(0x01), db) + require.Equal(t, ratio, sdk.MustNewDecFromStr("1.50")) + + require.Panics(t, func() { SplitCollateralRatioKey(badRatioKey()) }) + require.Panics(t, func() { SplitCollateralRatioIterKey(badRatioIterKey()) }) + +} + +func badRatioKey() []byte { + r := append(append(append(append([]byte{0x01}, sep...), []byte("nonsense")...), sep...), []byte{0xff}...) + return r +} + +func badRatioIterKey() []byte { + r := append(append([]byte{0x01}, sep...), []byte("nonsense")...) + return r +} diff --git a/x/cdp/types/msg.go b/x/cdp/types/msg.go index dc9bd18d..14d3a1e3 100644 --- a/x/cdp/types/msg.go +++ b/x/cdp/types/msg.go @@ -1,54 +1,327 @@ package types import ( + "fmt" + 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 +// ensure Msg interface compliance at compile time +var ( + _ sdk.Msg = &MsgCreateCDP{} + _ sdk.Msg = &MsgDeposit{} + _ sdk.Msg = &MsgWithdraw{} + _ sdk.Msg = &MsgDrawDebt{} + _ sdk.Msg = &MsgRepayDebt{} +) + +// MsgCreateCDP creates a cdp +type MsgCreateCDP struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Collateral sdk.Coins `json:"collateral" yaml:"collateral"` + Principal sdk.Coins `json:"principal" yaml:"principal"` } -// 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, +// NewMsgCreateCDP returns a new MsgPlaceBid. +func NewMsgCreateCDP(sender sdk.AccAddress, collateral sdk.Coins, principal sdk.Coins) MsgCreateCDP { + return MsgCreateCDP{ + Sender: sender, + Collateral: collateral, + Principal: principal, } } // Route return the message type used for routing the message. -func (msg MsgCreateOrModifyCDP) Route() string { return "cdp" } +func (msg MsgCreateCDP) Route() string { return RouterKey } // 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? +func (msg MsgCreateCDP) Type() string { return "create_cdp" } // ValidateBasic does a simple validation check that doesn't require access to any other information. -func (msg MsgCreateOrModifyCDP) ValidateBasic() sdk.Error { +func (msg MsgCreateCDP) ValidateBasic() sdk.Error { if msg.Sender.Empty() { return sdk.ErrInternal("invalid (empty) sender address") } - // TODO check coin denoms + if len(msg.Collateral) != 1 { + return sdk.ErrInvalidCoins(fmt.Sprintf("cdps do not support multiple collateral types: received %s", msg.Collateral)) + } + if !msg.Collateral.IsValid() { + return sdk.ErrInvalidCoins(msg.Collateral.String()) + } + if !msg.Collateral.IsAllPositive() { + return sdk.ErrInvalidCoins(msg.Collateral.String()) + } + if !msg.Principal.IsValid() { + return sdk.ErrInvalidCoins(msg.Principal.String()) + } + if !msg.Principal.IsAllPositive() { + return sdk.ErrInvalidCoins(msg.Collateral.String()) + } return nil } // GetSignBytes gets the canonical byte representation of the Msg. -func (msg MsgCreateOrModifyCDP) GetSignBytes() []byte { +func (msg MsgCreateCDP) 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 { +func (msg MsgCreateCDP) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Sender} } +// String implements the Stringer interface +func (msg MsgCreateCDP) String() string { + return fmt.Sprintf(`Create CDP Message: + Sender: %s + Collateral: %s + Principal: %s +`, msg.Sender, msg.Collateral, msg.Principal) +} + +// MsgDeposit deposit collateral to an existing cdp. +type MsgDeposit struct { + Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"` + Owner sdk.AccAddress `json:"owner" yaml:"owner"` + Collateral sdk.Coins `json:"collateral" yaml:"collateral"` +} + +// NewMsgDeposit returns a new MsgDeposit +func NewMsgDeposit(owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coins) MsgDeposit { + return MsgDeposit{ + Owner: owner, + Depositor: depositor, + Collateral: collateral, + } +} + +// Route return the message type used for routing the message. +func (msg MsgDeposit) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within tags. +func (msg MsgDeposit) Type() string { return "deposit_cdp" } + +// ValidateBasic does a simple validation check that doesn't require access to any other information. +func (msg MsgDeposit) ValidateBasic() sdk.Error { + if msg.Owner.Empty() { + return sdk.ErrInternal("invalid (empty) sender address") + } + if msg.Depositor.Empty() { + return sdk.ErrInternal("invalid (empty) owner address") + } + if len(msg.Collateral) != 1 { + return sdk.ErrInvalidCoins(fmt.Sprintf("cdps do not support multiple collateral types: received %s", msg.Collateral)) + } + if !msg.Collateral.IsValid() { + return sdk.ErrInvalidCoins(msg.Collateral.String()) + } + if !msg.Collateral.IsAllPositive() { + return sdk.ErrInvalidCoins(msg.Collateral.String()) + } + return nil +} + +// GetSignBytes gets the canonical byte representation of the Msg. +func (msg MsgDeposit) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgDeposit) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Depositor} +} + +// String implements the Stringer interface +func (msg MsgDeposit) String() string { + return fmt.Sprintf(`Deposit to CDP Message: + Sender: %s + Owner: %s + Collateral: %s +`, msg.Owner, msg.Owner, msg.Collateral) +} + +// MsgWithdraw withdraw collateral from an existing cdp. +type MsgWithdraw struct { + Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"` + Owner sdk.AccAddress `json:"owner" yaml:"owner"` + Collateral sdk.Coins `json:"collateral" yaml:"collateral"` +} + +// NewMsgWithdraw returns a new MsgDeposit +func NewMsgWithdraw(owner sdk.AccAddress, depositor sdk.AccAddress, collateral sdk.Coins) MsgWithdraw { + return MsgWithdraw{ + Owner: owner, + Depositor: depositor, + Collateral: collateral, + } +} + +// Route return the message type used for routing the message. +func (msg MsgWithdraw) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within tags. +func (msg MsgWithdraw) Type() string { return "withdraw_cdp" } + +// ValidateBasic does a simple validation check that doesn't require access to any other information. +func (msg MsgWithdraw) ValidateBasic() sdk.Error { + if msg.Owner.Empty() { + return sdk.ErrInternal("invalid (empty) sender address") + } + if msg.Depositor.Empty() { + return sdk.ErrInternal("invalid (empty) owner address") + } + if len(msg.Collateral) != 1 { + return sdk.ErrInvalidCoins(fmt.Sprintf("cdps do not support multiple collateral types: received %s", msg.Collateral)) + } + if !msg.Collateral.IsValid() { + return sdk.ErrInvalidCoins(msg.Collateral.String()) + } + if !msg.Collateral.IsAllPositive() { + return sdk.ErrInvalidCoins(msg.Collateral.String()) + } + return nil +} + +// GetSignBytes gets the canonical byte representation of the Msg. +func (msg MsgWithdraw) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgWithdraw) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Depositor} +} + +// String implements the Stringer interface +func (msg MsgWithdraw) String() string { + return fmt.Sprintf(`Withdraw from CDP Message: + Owner: %s + Depositor: %s + Collateral: %s +`, msg.Owner, msg.Depositor, msg.Collateral) +} + +// MsgDrawDebt draw coins off of collateral in cdp +type MsgDrawDebt struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + CdpDenom string `json:"cdp_denom" yaml:"cdp_denom"` + Principal sdk.Coins `json:"principal" yaml:"principal"` +} + +// NewMsgDrawDebt returns a new MsgDrawDebt +func NewMsgDrawDebt(sender sdk.AccAddress, denom string, principal sdk.Coins) MsgDrawDebt { + return MsgDrawDebt{ + Sender: sender, + CdpDenom: denom, + Principal: principal, + } +} + +// Route return the message type used for routing the message. +func (msg MsgDrawDebt) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within tags. +func (msg MsgDrawDebt) Type() string { return "draw_cdp" } + +// ValidateBasic does a simple validation check that doesn't require access to any other information. +func (msg MsgDrawDebt) ValidateBasic() sdk.Error { + if msg.Sender.Empty() { + return sdk.ErrInternal("invalid (empty) sender address") + } + if msg.CdpDenom == "" { + return sdk.ErrInternal("invalid (empty) cdp denom") + } + if !msg.Principal.IsValid() { + return sdk.ErrInvalidCoins(msg.Principal.String()) + } + if !msg.Principal.IsAllPositive() { + return sdk.ErrInvalidCoins(msg.Principal.String()) + } + return nil +} + +// GetSignBytes gets the canonical byte representation of the Msg. +func (msg MsgDrawDebt) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgDrawDebt) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} + +// String implements the Stringer interface +func (msg MsgDrawDebt) String() string { + return fmt.Sprintf(`Draw debt from CDP Message: + Sender: %s + CDP Denom: %s + Principal: %s +`, msg.Sender, msg.CdpDenom, msg.Principal) +} + +// MsgRepayDebt repay debt drawn off the collateral in a CDP +type MsgRepayDebt struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + CdpDenom string `json:"cdp_denom" yaml:"cdp_denom"` + Payment sdk.Coins `json:"payment" yaml:"payment"` +} + +// NewMsgRepayDebt returns a new MsgRepayDebt +func NewMsgRepayDebt(sender sdk.AccAddress, denom string, payment sdk.Coins) MsgRepayDebt { + return MsgRepayDebt{ + Sender: sender, + CdpDenom: denom, + Payment: payment, + } +} + +// Route return the message type used for routing the message. +func (msg MsgRepayDebt) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within tags. +func (msg MsgRepayDebt) Type() string { return "repay_cdp" } + +// ValidateBasic does a simple validation check that doesn't require access to any other information. +func (msg MsgRepayDebt) ValidateBasic() sdk.Error { + if msg.Sender.Empty() { + return sdk.ErrInternal("invalid (empty) sender address") + } + if msg.CdpDenom == "" { + return sdk.ErrInternal("invalid (empty) cdp denom") + } + if !msg.Payment.IsValid() { + return sdk.ErrInvalidCoins(msg.Payment.String()) + } + if !msg.Payment.IsAllPositive() { + return sdk.ErrInvalidCoins(msg.Payment.String()) + } + return nil +} + +// GetSignBytes gets the canonical byte representation of the Msg. +func (msg MsgRepayDebt) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgRepayDebt) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} + +// String implements the Stringer interface +func (msg MsgRepayDebt) String() string { + return fmt.Sprintf(`Draw debt from CDP Message: + Sender: %s + CDP Denom: %s + Payment: %s +`, msg.Sender, msg.CdpDenom, msg.Payment) +} + // MsgTransferCDP changes the ownership of a cdp type MsgTransferCDP struct { // TODO diff --git a/x/cdp/types/msg_test.go b/x/cdp/types/msg_test.go new file mode 100644 index 00000000..dd034a0d --- /dev/null +++ b/x/cdp/types/msg_test.go @@ -0,0 +1,166 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +var ( + coinsSingle = sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000)) + coinsZero = sdk.NewCoins() + coinsMulti = sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000), sdk.NewInt64Coin("foo", 10000)).Sort() + addrs = []sdk.AccAddress{ + sdk.AccAddress("test1"), + sdk.AccAddress("test2"), + } +) + +func TestMsgCreateCDP(t *testing.T) { + tests := []struct { + description string + sender sdk.AccAddress + collateral sdk.Coins + principal sdk.Coins + expectPass bool + }{ + {"create cdp", addrs[0], coinsSingle, coinsSingle, true}, + {"create cdp multi debt", addrs[0], coinsSingle, coinsMulti, true}, + {"create cdp no collateral", addrs[0], coinsZero, coinsSingle, false}, + {"create cdp no debt", addrs[0], coinsSingle, coinsZero, false}, + {"create cdp multi collateral", addrs[0], coinsMulti, coinsSingle, false}, + {"create cdp empty owner", sdk.AccAddress{}, coinsSingle, coinsSingle, false}, + } + + for i, tc := range tests { + msg := NewMsgCreateCDP( + tc.sender, + tc.collateral, + tc.principal, + ) + if tc.expectPass { + require.NoError(t, msg.ValidateBasic(), "test: %v", i) + } else { + require.Error(t, msg.ValidateBasic(), "test: %v", i) + } + } +} + +func TestMsgDeposit(t *testing.T) { + tests := []struct { + description string + sender sdk.AccAddress + depositor sdk.AccAddress + collateral sdk.Coins + expectPass bool + }{ + {"deposit", addrs[0], addrs[1], coinsSingle, true}, + {"deposit", addrs[0], addrs[0], coinsSingle, true}, + {"deposit no collateral", addrs[0], addrs[1], coinsZero, false}, + {"deposit multi collateral", addrs[0], addrs[1], coinsMulti, false}, + {"deposit empty owner", sdk.AccAddress{}, addrs[1], coinsSingle, false}, + {"deposit empty depositor", addrs[0], sdk.AccAddress{}, coinsSingle, false}, + } + + for i, tc := range tests { + msg := NewMsgDeposit( + tc.sender, + tc.depositor, + tc.collateral, + ) + if tc.expectPass { + require.NoError(t, msg.ValidateBasic(), "test: %v", i) + } else { + require.Error(t, msg.ValidateBasic(), "test: %v", i) + } + } +} + +func TestMsgWithdraw(t *testing.T) { + tests := []struct { + description string + sender sdk.AccAddress + depositor sdk.AccAddress + collateral sdk.Coins + expectPass bool + }{ + {"withdraw", addrs[0], addrs[1], coinsSingle, true}, + {"withdraw", addrs[0], addrs[0], coinsSingle, true}, + {"withdraw no collateral", addrs[0], addrs[1], coinsZero, false}, + {"withdraw multi collateral", addrs[0], addrs[1], coinsMulti, false}, + {"withdraw empty owner", sdk.AccAddress{}, addrs[1], coinsSingle, false}, + {"withdraw empty depositor", addrs[0], sdk.AccAddress{}, coinsSingle, false}, + } + + for i, tc := range tests { + msg := NewMsgWithdraw( + tc.sender, + tc.depositor, + tc.collateral, + ) + if tc.expectPass { + require.NoError(t, msg.ValidateBasic(), "test: %v", i) + } else { + require.Error(t, msg.ValidateBasic(), "test: %v", i) + } + } +} + +func TestMsgDrawDebt(t *testing.T) { + tests := []struct { + description string + sender sdk.AccAddress + denom string + principal sdk.Coins + expectPass bool + }{ + {"draw debt", addrs[0], sdk.DefaultBondDenom, coinsSingle, true}, + {"draw debt no debt", addrs[0], sdk.DefaultBondDenom, coinsZero, false}, + {"draw debt multi debt", addrs[0], sdk.DefaultBondDenom, coinsMulti, true}, + {"draw debt empty owner", sdk.AccAddress{}, sdk.DefaultBondDenom, coinsSingle, false}, + {"draw debt empty denom", sdk.AccAddress{}, "", coinsSingle, false}, + } + + for i, tc := range tests { + msg := NewMsgDrawDebt( + tc.sender, + tc.denom, + tc.principal, + ) + if tc.expectPass { + require.NoError(t, msg.ValidateBasic(), "test: %v", i) + } else { + require.Error(t, msg.ValidateBasic(), "test: %v", i) + } + } +} + +func TestMsgRepayDebt(t *testing.T) { + tests := []struct { + description string + sender sdk.AccAddress + denom string + payment sdk.Coins + expectPass bool + }{ + {"repay debt", addrs[0], sdk.DefaultBondDenom, coinsSingle, true}, + {"repay debt no payment", addrs[0], sdk.DefaultBondDenom, coinsZero, false}, + {"repay debt multi payment", addrs[0], sdk.DefaultBondDenom, coinsMulti, true}, + {"repay debt empty owner", sdk.AccAddress{}, sdk.DefaultBondDenom, coinsSingle, false}, + {"repay debt empty denom", sdk.AccAddress{}, "", coinsSingle, false}, + } + + for i, tc := range tests { + msg := NewMsgRepayDebt( + tc.sender, + tc.denom, + tc.payment, + ) + if tc.expectPass { + require.NoError(t, msg.ValidateBasic(), "test: %v", i) + } else { + require.Error(t, msg.ValidateBasic(), "test: %v", i) + } + } +} diff --git a/x/cdp/types/params.go b/x/cdp/types/params.go index d4afa0dd..c0a43271 100644 --- a/x/cdp/types/params.go +++ b/x/cdp/types/params.go @@ -2,137 +2,215 @@ package types import ( "fmt" + "time" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/params/subspace" + "github.com/cosmos/cosmos-sdk/x/params" + tmtime "github.com/tendermint/tendermint/types/time" ) -/* -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") + KeyGlobalDebtLimit = []byte("GlobalDebtLimit") + KeyCollateralParams = []byte("CollateralParams") + KeyDebtParams = []byte("DebtParams") + KeyCircuitBreaker = []byte("CircuitBreaker") + DefaultGlobalDebt = sdk.Coins{} + DefaultCircuitBreaker = false + DefaultCollateralParams = CollateralParams{} + DefaultDebtParams = DebtParams{} + DefaultCdpStartingID = uint64(1) + DefaultDebtDenom = "debt" + DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0)) + minCollateralPrefix = 0 + maxCollateralPrefix = 255 ) +// Params governance parameters for cdp module +type Params struct { + CollateralParams CollateralParams `json:"collateral_params" yaml:"collateral_params"` + DebtParams DebtParams `json:"debt_params" yaml:"debt_params"` + GlobalDebtLimit sdk.Coins `json:"global_debt_limit" yaml:"global_debt_limit"` + CircuitBreaker bool `json:"circuit_breaker" yaml:"circuit_breaker"` +} + +// String implements fmt.Stringer +func (p Params) String() string { + return fmt.Sprintf(`Params: + Global Debt Limit: %s + Collateral Params: %s + Debt Params: %s + Circuit Breaker: %t`, + p.GlobalDebtLimit, p.CollateralParams, p.DebtParams, p.CircuitBreaker, + ) +} + +// NewParams returns a new params object +func NewParams(debtLimit sdk.Coins, collateralParams CollateralParams, debtParams DebtParams, breaker bool) Params { + return Params{ + GlobalDebtLimit: debtLimit, + CollateralParams: collateralParams, + DebtParams: debtParams, + CircuitBreaker: breaker, + } +} + +// DefaultParams returns default params for cdp module +func DefaultParams() Params { + return NewParams(DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParams, DefaultCircuitBreaker) +} + +// CollateralParam governance parameters for each collateral type within the cdp module +type CollateralParam struct { + Denom string `json:"denom" yaml:"denom"` // Coin name of collateral type + LiquidationRatio sdk.Dec `json:"liquidation_ratio" yaml:"liquidation_ratio"` // The ratio (Collateral (priced in stable coin) / Debt) under which a CDP will be liquidated + DebtLimit sdk.Coins `json:"debt_limit" yaml:"debt_limit"` // Maximum amount of debt allowed to be drawn from this collateral type + StabilityFee sdk.Dec `json:"stability_fee" yaml:"stability_fee"` // per second stability fee for loans opened using this collateral + Prefix byte `json:"prefix" yaml:"prefix"` + MarketID string `json:"market_id" yaml:"market_id"` // marketID for fetching price of the asset from the pricefeed + ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` // factor for converting internal units to one base unit of collateral +} + +// String implements fmt.Stringer +func (cp CollateralParam) String() string { + return fmt.Sprintf(`Collateral: + Denom: %s + Liquidation Ratio: %s + Stability Fee: %s + Debt Limit: %s + Prefix: %b + Market ID: %s + Conversion Factor: %s`, + cp.Denom, cp.LiquidationRatio, cp.StabilityFee, cp.DebtLimit, cp.Prefix, cp.MarketID, cp.ConversionFactor) +} + +// CollateralParams array of CollateralParam +type CollateralParams []CollateralParam + +// String implements fmt.Stringer +func (cps CollateralParams) String() string { + out := "Collateral Params\n" + for _, cp := range cps { + out += fmt.Sprintf("%s\n", cp) + } + return out +} + +// DebtParam governance params for debt assets +type DebtParam struct { + Denom string `json:"denom" yaml:"denom"` + ReferenceAsset string `json:"reference_asset" yaml:"reference_asset"` + DebtLimit sdk.Coins `json:"debt_limit" yaml:"debt_limit"` + ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` + DebtFloor sdk.Int `json:"debt_floor" yaml:"debt_floor"` // minimum active loan size, used to prevent dust +} + +func (dp DebtParam) String() string { + return fmt.Sprintf(`Debt: + Denom: %s + Reference Asset: %s + Debt Limit: %s + Conversion Factor: %s + Debt Floot %s`, dp.Denom, dp.ReferenceAsset, dp.DebtLimit, dp.ConversionFactor, dp.DebtFloor) +} + +// DebtParams array of DebtParam +type DebtParams []DebtParam + +// String implements fmt.Stringer +func (dps DebtParams) String() string { + out := "Debt Params\n" + for _, dp := range dps { + out += fmt.Sprintf("%s\n", dp) + } + return out +} + // ParamKeyTable Key declaration for parameters -func ParamKeyTable() subspace.KeyTable { - return subspace.NewKeyTable().RegisterParamSet(&CdpParams{}) +func ParamKeyTable() params.KeyTable { + return params.NewKeyTable().RegisterParamSet(&Params{}) } // 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}, +func (p *Params) ParamSetPairs() params.ParamSetPairs { + return params.ParamSetPairs{ + {Key: KeyGlobalDebtLimit, Value: &p.GlobalDebtLimit}, + {Key: KeyCollateralParams, Value: &p.CollateralParams}, + {Key: KeyDebtParams, Value: &p.DebtParams}, + {Key: KeyCircuitBreaker, Value: &p.CircuitBreaker}, } } -// 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 { +func (p Params) Validate() error { + debtDenoms := make(map[string]int) + debtParamsDebtLimit := sdk.Coins{} + for _, dp := range p.DebtParams { + _, found := debtDenoms[dp.Denom] + if found { + return fmt.Errorf("duplicate debt denom: %s", dp.Denom) + } + debtDenoms[dp.Denom] = 1 + if dp.DebtLimit.IsAnyNegative() { + return fmt.Errorf("debt limit for all debt tokens should be positive, is %s for %s", dp.DebtLimit, dp.Denom) + } + debtParamsDebtLimit = debtParamsDebtLimit.Add(dp.DebtLimit) + } + if !debtParamsDebtLimit.DenomsSubsetOf(p.GlobalDebtLimit) { + return fmt.Errorf("debt denom not found in global debt limit:\n\tglobal debt limit: %s\n\tdebt limits: %s", + p.GlobalDebtLimit, debtParamsDebtLimit) + } + if debtParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) { + return fmt.Errorf("debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tdebt limits: %s", + p.GlobalDebtLimit, debtParamsDebtLimit) + } + collateralDupMap := make(map[string]int) - denomDupMap := make(map[string]int) - for _, collateral := range p.CollateralParams { - _, found := collateralDupMap[collateral.Denom] + prefixDupMap := make(map[int]int) + collateralParamsDebtLimit := sdk.Coins{} + for _, cp := range p.CollateralParams { + prefix := int(cp.Prefix) + if prefix < minCollateralPrefix || prefix > maxCollateralPrefix { + return fmt.Errorf("invalid prefix for collateral denom %s: %b", cp.Denom, cp.Prefix) + } + _, found := prefixDupMap[prefix] 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) + return fmt.Errorf("duplicate prefix for collateral denom %s: %v", cp.Denom, []byte{cp.Prefix}) } - // TODO do we want to enforce overcollateralization at this level? -- probably not, as it's technically a governance thing (kevin) + prefixDupMap[prefix] = 1 + _, found = collateralDupMap[cp.Denom] + + if found { + return fmt.Errorf("duplicate collateral denom: %s", cp.Denom) + } + collateralDupMap[cp.Denom] = 1 + + if cp.DebtLimit.IsAnyNegative() { + return fmt.Errorf("debt limit for all collaterals should be positive, is %s for %s", cp.DebtLimit, cp.Denom) + } + collateralParamsDebtLimit = collateralParamsDebtLimit.Add(cp.DebtLimit) + + for _, dc := range cp.DebtLimit { + _, found := debtDenoms[dc.Denom] + if !found { + return fmt.Errorf("debt limit for collateral %s contains invalid debt denom %s", cp.Denom, dc.Denom) + } + } + if cp.DebtLimit.IsAnyGT(p.GlobalDebtLimit) { + return fmt.Errorf("collateral debt limit for %s exceeds global debt limit: \n\tglobal debt limit: %s\n\tcollateral debt limits: %s", + cp.Denom, p.GlobalDebtLimit, cp.DebtLimit) + } } - if p.GlobalDebtLimit.IsNegative() { - return fmt.Errorf("global debt limit should be positive, is %s", p.GlobalDebtLimit) + if collateralParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) { + return fmt.Errorf("collateral debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tcollateral debt limits: %s", + p.GlobalDebtLimit, collateralParamsDebtLimit) } - for _, denom := range p.StableDenoms { - _, found := denomDupMap[denom] - if found { - return fmt.Errorf("duplicate stable denom: %s", denom) - } - denomDupMap[denom] = 1 + if p.GlobalDebtLimit.IsAnyNegative() { + return fmt.Errorf("global debt limit should be positive for all debt tokens, is %s", p.GlobalDebtLimit) } return nil } - -func DefaultParams() CdpParams { - return CdpParams{ - GlobalDebtLimit: sdk.NewInt(0), - CollateralParams: []CollateralParams{}, - StableDenoms: []string{"usdx"}, - } -} diff --git a/x/cdp/types/querier.go b/x/cdp/types/querier.go index 0f1c6fe1..368621e2 100644 --- a/x/cdp/types/querier.go +++ b/x/cdp/types/querier.go @@ -2,24 +2,55 @@ package types import ( sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/rest" ) +// Querier routes for the cdp module const ( - QueryGetCdps = "cdps" - QueryGetParams = "params" - RestOwner = "owner" - RestCollateralDenom = "collateralDenom" - RestUnderCollateralizedAt = "underCollateralizedAt" + QueryGetCdp = "cdp" + QueryGetCdps = "cdps" + QueryGetCdpsByCollateralization = "ratio" + QueryGetParams = "params" + RestOwner = "owner" + RestCollateralDenom = "collateral-denom" + RestRatio = "ratio" ) +// QueryCdpsParams params for query /cdp/cdps 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. + CollateralDenom string // get CDPs with this collateral denom } -type ModifyCdpRequestBody struct { - BaseReq rest.BaseReq `json:"base_req"` - Cdp CDP `json:"cdp"` +// NewQueryCdpsParams returns QueryCdpsParams +func NewQueryCdpsParams(denom string) QueryCdpsParams { + return QueryCdpsParams{ + CollateralDenom: denom, + } +} + +// NewQueryCdpParams returns QueryCdpParams +func NewQueryCdpParams(owner sdk.AccAddress, denom string) QueryCdpParams { + return QueryCdpParams{ + Owner: owner, + CollateralDenom: denom, + } +} + +// QueryCdpParams params for query /cdp/cdp +type QueryCdpParams struct { + CollateralDenom string // get CDPs with this collateral denom + Owner sdk.AccAddress // get CDPs belonging to this owner +} + +// QueryCdpsByRatioParams params for query /cdp/cdps/ratio +type QueryCdpsByRatioParams struct { + CollateralDenom string // get CDPs with this collateral denom + Ratio sdk.Dec // get CDPs below this collateral:debt ratio +} + +// NewQueryCdpsByRatioParams returns QueryCdpsByRatioParams +func NewQueryCdpsByRatioParams(denom string, ratio sdk.Dec) QueryCdpsByRatioParams { + return QueryCdpsByRatioParams{ + CollateralDenom: denom, + Ratio: ratio, + } } diff --git a/x/cdp/types/types.go b/x/cdp/types/types.go deleted file mode 100644 index 02051ed4..00000000 --- a/x/cdp/types/types.go +++ /dev/null @@ -1,77 +0,0 @@ -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 -} diff --git a/x/cdp/types/utils.go b/x/cdp/types/utils.go new file mode 100644 index 00000000..f5cb0aaf --- /dev/null +++ b/x/cdp/types/utils.go @@ -0,0 +1,99 @@ +package types + +import ( + "bytes" + "fmt" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// MaxSortableDec largest sortable sdk.Dec +var MaxSortableDec = sdk.OneDec().Quo(sdk.SmallestDec()) + +// ValidSortableDec sdk.Dec can't have precision of less than 10^-18 +func ValidSortableDec(dec sdk.Dec) bool { + return dec.Abs().LTE(MaxSortableDec) +} + +// SortableDecBytes returns a byte slice representation of a Dec that can be sorted. +// Left and right pads with 0s so there are 18 digits to left and right of the decimal point. +// For this reason, there is a maximum and minimum value for this, enforced by ValidSortableDec. +func SortableDecBytes(dec sdk.Dec) []byte { + if !ValidSortableDec(dec) { + panic("dec must be within bounds") + } + // Instead of adding an extra byte to all sortable decs in order to handle max sortable, we just + // makes its bytes be "max" which comes after all numbers in ASCIIbetical order + if dec.Equal(MaxSortableDec) { + return []byte("max") + } + // For the same reason, we make the bytes of minimum sortable dec be --, which comes before all numbers. + if dec.Equal(MaxSortableDec.Neg()) { + return []byte("--") + } + // We move the negative sign to the front of all the left padded 0s, to make negative numbers come before positive numbers + if dec.IsNegative() { + return append([]byte("-"), []byte(fmt.Sprintf(fmt.Sprintf("%%0%ds", sdk.Precision*2+1), dec.Abs().String()))...) + } + return []byte(fmt.Sprintf(fmt.Sprintf("%%0%ds", sdk.Precision*2+1), dec.String())) +} + +// ParseDecBytes parses a []byte encoded using SortableDecBytes back to sdk.Dec +func ParseDecBytes(db []byte) (sdk.Dec, error) { + strFromDecBytes := strings.Trim(string(db[:]), "0") + if string(strFromDecBytes[0]) == "." { + strFromDecBytes = "0" + strFromDecBytes + } + if string(strFromDecBytes[len(strFromDecBytes)-1]) == "." { + strFromDecBytes = strFromDecBytes + "0" + } + if bytes.Equal(db, []byte("max")) { + return MaxSortableDec, nil + } + if bytes.Equal(db, []byte("--")) { + return MaxSortableDec.Neg(), nil + } + dec, err := sdk.NewDecFromStr(strFromDecBytes) + if err != nil { + return sdk.Dec{}, err + } + return dec, nil +} + +// RelativePow raises x to the power of n, where x (and the result, z) are scaled by factor b. +// For example, RelativePow(210, 2, 100) = 441 (2.1^2 = 4.41) +// Only defined for positive ints. +func RelativePow(x sdk.Int, n sdk.Int, b sdk.Int) (z sdk.Int) { + if x.IsZero() { + if n.IsZero() { + z = b // 0^0 = 1 + return + } + z = sdk.ZeroInt() // otherwise 0^a = 0 + return + } + + z = x + if n.Mod(sdk.NewInt(2)).Equal(sdk.ZeroInt()) { + z = b + } + + halfOfB := b.Quo(sdk.NewInt(2)) + n = n.Quo(sdk.NewInt(2)) + + for n.GT(sdk.ZeroInt()) { + xSquared := x.Mul(x) + xSquaredRounded := xSquared.Add(halfOfB) + + x = xSquaredRounded.Quo(b) + + if n.Mod(sdk.NewInt(2)).Equal(sdk.OneInt()) { + zx := z.Mul(x) + zxRounded := zx.Add(halfOfB) + z = zxRounded.Quo(b) + } + n = n.Quo(sdk.NewInt(2)) + } + return +} diff --git a/x/cdp/types/utils_test.go b/x/cdp/types/utils_test.go new file mode 100644 index 00000000..7a32d4e8 --- /dev/null +++ b/x/cdp/types/utils_test.go @@ -0,0 +1,83 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + +) + +func TestSortableDecBytes(t *testing.T) { + tests := []struct { + d sdk.Dec + want []byte + }{ + {sdk.NewDec(0), []byte("000000000000000000.000000000000000000")}, + {sdk.NewDec(1), []byte("000000000000000001.000000000000000000")}, + {sdk.MustNewDecFromStr("2.0"), []byte("000000000000000002.000000000000000000")}, + {sdk.MustNewDecFromStr("-2.0"), []byte("-000000000000000002.000000000000000000")}, + {sdk.NewDec(10), []byte("000000000000000010.000000000000000000")}, + {sdk.NewDec(12340), []byte("000000000000012340.000000000000000000")}, + {sdk.NewDecWithPrec(12340, 4), []byte("000000000000000001.234000000000000000")}, + {sdk.NewDecWithPrec(12340, 5), []byte("000000000000000000.123400000000000000")}, + {sdk.NewDecWithPrec(12340, 8), []byte("000000000000000000.000123400000000000")}, + {sdk.NewDecWithPrec(1009009009009009009, 17), []byte("000000000000000010.090090090090090090")}, + {sdk.NewDecWithPrec(-1009009009009009009, 17), []byte("-000000000000000010.090090090090090090")}, + {sdk.NewDec(1000000000000000000), []byte("max")}, + {sdk.NewDec(-1000000000000000000), []byte("--")}, + } + for tcIndex, tc := range tests { + assert.Equal(t, tc.want, SortableDecBytes(tc.d), "bad String(), index: %v", tcIndex) + } + + assert.Panics(t, func() { SortableDecBytes(sdk.NewDec(1000000000000000001)) }) + assert.Panics(t, func() { SortableDecBytes(sdk.NewDec(-1000000000000000001)) }) +} + +func TestParseSortableDecBytes(t *testing.T) { + tests := []struct { + d sdk.Dec + want []byte + }{ + {sdk.NewDec(0), []byte("000000000000000000.000000000000000000")}, + {sdk.NewDec(1), []byte("000000000000000001.000000000000000000")}, + {sdk.MustNewDecFromStr("2.0"), []byte("000000000000000002.000000000000000000")}, + {sdk.MustNewDecFromStr("-2.0"), []byte("-000000000000000002.000000000000000000")}, + {sdk.NewDec(10), []byte("000000000000000010.000000000000000000")}, + {sdk.NewDec(12340), []byte("000000000000012340.000000000000000000")}, + {sdk.NewDecWithPrec(12340, 4), []byte("000000000000000001.234000000000000000")}, + {sdk.NewDecWithPrec(12340, 5), []byte("000000000000000000.123400000000000000")}, + {sdk.NewDecWithPrec(12340, 8), []byte("000000000000000000.000123400000000000")}, + {sdk.NewDecWithPrec(1009009009009009009, 17), []byte("000000000000000010.090090090090090090")}, + {sdk.NewDecWithPrec(-1009009009009009009, 17), []byte("-000000000000000010.090090090090090090")}, + {sdk.NewDec(1000000000000000000), []byte("max")}, + {sdk.NewDec(-1000000000000000000), []byte("--")}, + } + for tcIndex, tc := range tests { + b := SortableDecBytes(tc.d) + r, err := ParseDecBytes(b) + assert.NoError(t, err) + assert.Equal(t, tc.d, r, "bad Dec(), index: %v", tcIndex) + } +} + +func TestRelativePow(t *testing.T) { + tests := []struct { + args []sdk.Int + want sdk.Int + }{ + {[]sdk.Int{sdk.ZeroInt(), sdk.ZeroInt(), sdk.OneInt()}, sdk.OneInt()}, + {[]sdk.Int{sdk.ZeroInt(), sdk.ZeroInt(), sdk.NewInt(10)}, sdk.NewInt(10)}, + {[]sdk.Int{sdk.ZeroInt(), sdk.OneInt(), sdk.NewInt(10)}, sdk.ZeroInt()}, + {[]sdk.Int{sdk.NewInt(10), sdk.NewInt(2), sdk.OneInt()}, sdk.NewInt(100)}, + {[]sdk.Int{sdk.NewInt(210), sdk.NewInt(2), sdk.NewInt(100)}, sdk.NewInt(441)}, + {[]sdk.Int{sdk.NewInt(2100), sdk.NewInt(2), sdk.NewInt(1000)}, sdk.NewInt(4410)}, + {[]sdk.Int{sdk.NewInt(1000000001547125958), sdk.NewInt(600), sdk.NewInt(1000000000000000000)}, sdk.NewInt(1000000928276004850)}, + } + for i, tc := range tests { + res := RelativePow(tc.args[0], tc.args[1], tc.args[2]) + require.Equal(t, tc.want, res, "unexpected result for test case %d, input: %v, got: %v", i, tc.args, res) + } +} diff --git a/x/liquidator/alias.go b/x/liquidator/alias.go deleted file mode 100644 index da53e9d2..00000000 --- a/x/liquidator/alias.go +++ /dev/null @@ -1,52 +0,0 @@ -// 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 -) diff --git a/x/liquidator/client/cli/query.go b/x/liquidator/client/cli/query.go deleted file mode 100644 index 45ea4d91..00000000 --- a/x/liquidator/client/cli/query.go +++ /dev/null @@ -1,49 +0,0 @@ -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) - }, - } -} diff --git a/x/liquidator/client/cli/tx.go b/x/liquidator/client/cli/tx.go deleted file mode 100644 index bfd72d45..00000000 --- a/x/liquidator/client/cli/tx.go +++ /dev/null @@ -1,98 +0,0 @@ -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 -} diff --git a/x/liquidator/client/rest/query.go b/x/liquidator/client/rest/query.go deleted file mode 100644 index 7fea3a3d..00000000 --- a/x/liquidator/client/rest/query.go +++ /dev/null @@ -1,30 +0,0 @@ -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 - } -} diff --git a/x/liquidator/client/rest/rest.go b/x/liquidator/client/rest/rest.go deleted file mode 100644 index 6369945e..00000000 --- a/x/liquidator/client/rest/rest.go +++ /dev/null @@ -1,13 +0,0 @@ -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) -} \ No newline at end of file diff --git a/x/liquidator/client/rest/tx.go b/x/liquidator/client/rest/tx.go deleted file mode 100644 index e854fe93..00000000 --- a/x/liquidator/client/rest/tx.go +++ /dev/null @@ -1,73 +0,0 @@ -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}) - } -} diff --git a/x/liquidator/doc.go b/x/liquidator/doc.go deleted file mode 100644 index ee48245b..00000000 --- a/x/liquidator/doc.go +++ /dev/null @@ -1,21 +0,0 @@ -/* -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 diff --git a/x/liquidator/genesis.go b/x/liquidator/genesis.go deleted file mode 100644 index 7c3b5adf..00000000 --- a/x/liquidator/genesis.go +++ /dev/null @@ -1,19 +0,0 @@ -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, - } -} diff --git a/x/liquidator/handler.go b/x/liquidator/handler.go deleted file mode 100644 index ace3fb9d..00000000 --- a/x/liquidator/handler.go +++ /dev/null @@ -1,54 +0,0 @@ -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 -// } diff --git a/x/liquidator/keeper/integration_test.go b/x/liquidator/keeper/integration_test.go deleted file mode 100644 index 53f936ac..00000000 --- a/x/liquidator/keeper/integration_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package keeper_test - -import ( - "time" - - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/kava-labs/kava/app" - "github.com/kava-labs/kava/x/cdp" - "github.com/kava-labs/kava/x/liquidator" - "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 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...) } - -// Genesis states to initialize test apps - -func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { - pfGenesis := pricefeed.GenesisState{ - Params: pricefeed.Params{ - Markets: []pricefeed.Market{ - pricefeed.Market{MarketID: asset, BaseAsset: asset, QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}}, - }, - PostedPrices: []pricefeed.PostedPrice{ - pricefeed.PostedPrice{ - MarketID: asset, - OracleAddress: sdk.AccAddress{}, - Price: price, - Expiry: time.Now().Add(1 * time.Hour), - }, - }, - } - return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)} -} - -func NewCDPGenState() app.GenesisState { - cdpGenesis := cdp.GenesisState{ - Params: cdp.CdpParams{ - GlobalDebtLimit: sdk.NewInt(1000000), - CollateralParams: []cdp.CollateralParams{ - { - Denom: "btc", - LiquidationRatio: sdk.MustNewDecFromStr("1.5"), - DebtLimit: sdk.NewInt(500000), - }, - }, - }, - GlobalDebt: sdk.ZeroInt(), - CDPs: cdp.CDPs{}, - } - return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)} -} - -func NewLiquidatorGenState() app.GenesisState { - liquidatorGenesis := types.GenesisState{ - Params: types.LiquidatorParams{ - DebtAuctionSize: sdk.NewInt(1000), - CollateralParams: []types.CollateralParams{ - { - Denom: "btc", - AuctionSize: sdk.NewInt(1), - }, - }, - }, - } - return app.GenesisState{liquidator.ModuleName: liquidator.ModuleCdc.MustMarshalJSON(liquidatorGenesis)} -} diff --git a/x/liquidator/keeper/keeper.go b/x/liquidator/keeper/keeper.go deleted file mode 100644 index 5134d47e..00000000 --- a/x/liquidator/keeper/keeper.go +++ /dev/null @@ -1,203 +0,0 @@ -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/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) (uint64, 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) (uint64, 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) (uint64, 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) -} diff --git a/x/liquidator/keeper/keeper_test.go b/x/liquidator/keeper/keeper_test.go deleted file mode 100644 index 4541f3da..00000000 --- a/x/liquidator/keeper/keeper_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package keeper_test - -import ( - "testing" - "time" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" - tmtime "github.com/tendermint/tendermint/types/time" - - "github.com/kava-labs/kava/app" - "github.com/kava-labs/kava/x/liquidator/types" -) - -func TestKeeper_SeizeAndStartCollateralAuction(t *testing.T) { - // Setup - _, addrs := app.GeneratePrivKeyAddressPairs(1) - - tApp := app.NewTestApp() - tApp.InitializeFromGenesisStates( - app.NewAuthGenState(addrs, []sdk.Coins{cs(c("btc", 100))}), - NewPricefeedGenState("btc", d("8000.00")), - NewCDPGenState(), - NewLiquidatorGenState(), - ) - ctx := tApp.NewContext(false, abci.Header{}) - - require.NoError(t, tApp.GetCDPKeeper().ModifyCDP(ctx, addrs[0], "btc", i(3), i(16000))) - - _, err := tApp.GetPriceFeedKeeper().SetPrice(ctx, addrs[0], "btc", d("7999.99"), time.Now().Add(1*time.Hour)) - require.NoError(t, err) - require.NoError(t, tApp.GetPriceFeedKeeper().SetCurrentPrices(ctx, "btc")) - - // Run test function - auctionID, err := tApp.GetLiquidatorKeeper().SeizeAndStartCollateralAuction(ctx, addrs[0], "btc") - - // Check CDP - require.NoError(t, err) - cdp, found := tApp.GetCDPKeeper().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 = tApp.GetAuctionKeeper().GetAuction(ctx, auctionID) - require.True(t, found) -} - -func TestKeeper_StartDebtAuction(t *testing.T) { - // Setup - tApp := app.NewTestApp() - tApp.InitializeFromGenesisStates( - NewLiquidatorGenState(), - ) - keeper := tApp.GetLiquidatorKeeper() - ctx := tApp.NewContext(false, abci.Header{}) - - initSDebt := types.SeizedDebt{i(2000), i(0)} - keeper.SetSeizedDebt(ctx, initSDebt) - - // Execute - auctionID, err := keeper.StartDebtAuction(ctx) - - // Check - require.NoError(t, err) - require.Equal(t, - types.SeizedDebt{ - initSDebt.Total, - initSDebt.SentToAuction.Add(keeper.GetParams(ctx).DebtAuctionSize), - }, - keeper.GetSeizedDebt(ctx), - ) - _, found := tApp.GetAuctionKeeper().GetAuction(ctx, auctionID) - require.True(t, found) -} - -func TestKeeper_partialSeizeCDP(t *testing.T) { - // Setup - _, addrs := app.GeneratePrivKeyAddressPairs(1) - - tApp := app.NewTestApp() - tApp.InitializeFromGenesisStates( - app.NewAuthGenState(addrs, []sdk.Coins{cs(c("btc", 100))}), - NewPricefeedGenState("btc", d("8000.00")), - NewCDPGenState(), - NewLiquidatorGenState(), - ) - ctx := tApp.NewContext(false, abci.Header{}) - - tApp.GetCDPKeeper().ModifyCDP(ctx, addrs[0], "btc", i(3), i(16000)) - - tApp.GetPriceFeedKeeper().SetPrice(ctx, addrs[0], "btc", d("7999.99"), tmtime.Now().Add(time.Hour*1)) - tApp.GetPriceFeedKeeper().SetCurrentPrices(ctx, "btc") - - // Run test function - err := tApp.GetLiquidatorKeeper().PartialSeizeCDP(ctx, addrs[0], "btc", i(2), i(10000)) - - // Check - require.NoError(t, err) - cdp, found := tApp.GetCDPKeeper().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 - tApp := app.NewTestApp() - ctx := tApp.NewContext(true, abci.Header{}) - debt := types.SeizedDebt{i(234247645), i(2343)} - - // Run test function - tApp.GetLiquidatorKeeper().SetSeizedDebt(ctx, debt) - readDebt := tApp.GetLiquidatorKeeper().GetSeizedDebt(ctx) - - // Check - require.Equal(t, debt, readDebt) -} diff --git a/x/liquidator/keeper/params.go b/x/liquidator/keeper/params.go deleted file mode 100644 index 71d0f31b..00000000 --- a/x/liquidator/keeper/params.go +++ /dev/null @@ -1,18 +0,0 @@ -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, ¶ms) - return params -} - -// SetParams sets params for the liquidator module -func (k Keeper) SetParams(ctx sdk.Context, params types.LiquidatorParams) { - k.paramSubspace.SetParamSet(ctx, ¶ms) -} \ No newline at end of file diff --git a/x/liquidator/keeper/querier.go b/x/liquidator/keeper/querier.go deleted file mode 100644 index 4bff0ad5..00000000 --- a/x/liquidator/keeper/querier.go +++ /dev/null @@ -1,45 +0,0 @@ -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 -} diff --git a/x/liquidator/module.go b/x/liquidator/module.go deleted file mode 100644 index 7f163de8..00000000 --- a/x/liquidator/module.go +++ /dev/null @@ -1,130 +0,0 @@ -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{} -} diff --git a/x/liquidator/types/codec.go b/x/liquidator/types/codec.go deleted file mode 100644 index da70df34..00000000 --- a/x/liquidator/types/codec.go +++ /dev/null @@ -1,20 +0,0 @@ -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) -} diff --git a/x/liquidator/types/expected_keepers.go b/x/liquidator/types/expected_keepers.go deleted file mode 100644 index 39bede95..00000000 --- a/x/liquidator/types/expected_keepers.go +++ /dev/null @@ -1,31 +0,0 @@ -package types - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - - "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) (uint64, sdk.Error) - StartReverseAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin) (uint64, sdk.Error) - StartForwardReverseAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin, sdk.AccAddress) (uint64, sdk.Error) -} diff --git a/x/liquidator/types/genesis.go b/x/liquidator/types/genesis.go deleted file mode 100644 index 0b4bdf62..00000000 --- a/x/liquidator/types/genesis.go +++ /dev/null @@ -1,22 +0,0 @@ -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 -} diff --git a/x/liquidator/types/keys.go b/x/liquidator/types/keys.go deleted file mode 100644 index 9e4ffd6b..00000000 --- a/x/liquidator/types/keys.go +++ /dev/null @@ -1,18 +0,0 @@ -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 -) \ No newline at end of file diff --git a/x/liquidator/types/msg.go b/x/liquidator/types/msg.go deleted file mode 100644 index 3b5cbe81..00000000 --- a/x/liquidator/types/msg.go +++ /dev/null @@ -1,97 +0,0 @@ -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} } diff --git a/x/liquidator/types/params.go b/x/liquidator/types/params.go deleted file mode 100644 index c72c8285..00000000 --- a/x/liquidator/types/params.go +++ /dev/null @@ -1,100 +0,0 @@ -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 -} diff --git a/x/liquidator/types/querier.go b/x/liquidator/types/querier.go deleted file mode 100644 index 5fb8d6be..00000000 --- a/x/liquidator/types/querier.go +++ /dev/null @@ -1,22 +0,0 @@ -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? -} \ No newline at end of file diff --git a/x/liquidator/types/types.go b/x/liquidator/types/types.go deleted file mode 100644 index fdf1c536..00000000 --- a/x/liquidator/types/types.go +++ /dev/null @@ -1,30 +0,0 @@ -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 -} diff --git a/x/pricefeed/keeper/keeper.go b/x/pricefeed/keeper/keeper.go index b181f11b..ca76e5ae 100644 --- a/x/pricefeed/keeper/keeper.go +++ b/x/pricefeed/keeper/keeper.go @@ -95,10 +95,14 @@ func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) sdk.Error { }) } } - medianPrice, err := k.CalculateMedianPrice(ctx, notExpiredPrices) - if err != nil { - return err + if len(notExpiredPrices) == 0 { + store := ctx.KVStore(k.storeKey) + store.Set( + []byte(types.CurrentPricePrefix+marketID), k.cdc.MustMarshalBinaryBare(types.CurrentPrice{}), + ) + return types.ErrNoValidPrice(k.codespace) } + medianPrice := k.CalculateMedianPrice(ctx, notExpiredPrices) store := ctx.KVStore(k.storeKey) currentPrice := types.CurrentPrice{ @@ -113,28 +117,25 @@ func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) sdk.Error { } // CalculateMedianPrice calculates the median prices for the input prices. -func (k Keeper) CalculateMedianPrice(ctx sdk.Context, prices []types.CurrentPrice) (sdk.Dec, sdk.Error) { +func (k Keeper) CalculateMedianPrice(ctx sdk.Context, prices []types.CurrentPrice) sdk.Dec { l := len(prices) - if l == 0 { - // Error if there are no valid prices in the raw pricefeed - return sdk.Dec{}, types.ErrNoValidPrice(k.codespace) - } else if l == 1 { + if l == 1 { // Return immediately if there's only one price - return prices[0].Price, nil - } else { - // sort the prices - sort.Slice(prices, func(i, j int) bool { - return prices[i].Price.LT(prices[j].Price) - }) - // for even numbers of prices, the median is calculated as the mean of the two middle prices - if l%2 == 0 { - median := k.calculateMeanPrice(ctx, prices[l/2-1:l/2+1]) - return median, nil - } - // for odd numbers of prices, return the middle element - return prices[l/2].Price, nil + return prices[0].Price } + // sort the prices + sort.Slice(prices, func(i, j int) bool { + return prices[i].Price.LT(prices[j].Price) + }) + // for even numbers of prices, the median is calculated as the mean of the two middle prices + if l%2 == 0 { + median := k.calculateMeanPrice(ctx, prices[l/2-1:l/2+1]) + return median + } + // for odd numbers of prices, return the middle element + return prices[l/2].Price + } func (k Keeper) calculateMeanPrice(ctx sdk.Context, prices []types.CurrentPrice) sdk.Dec { @@ -144,13 +145,19 @@ func (k Keeper) calculateMeanPrice(ctx sdk.Context, prices []types.CurrentPrice) } // GetCurrentPrice fetches the current median price of all oracles for a specific asset -func (k Keeper) GetCurrentPrice(ctx sdk.Context, marketID string) types.CurrentPrice { +func (k Keeper) GetCurrentPrice(ctx sdk.Context, marketID string) (types.CurrentPrice, sdk.Error) { store := ctx.KVStore(k.storeKey) bz := store.Get([]byte(types.CurrentPricePrefix + marketID)) - // TODO panic or return error if not found + + if bz == nil { + return types.CurrentPrice{}, types.ErrNoValidPrice(k.codespace) + } var price types.CurrentPrice k.cdc.MustUnmarshalBinaryBare(bz, &price) - return price + if price.Price.Equal(sdk.ZeroDec()) { + return types.CurrentPrice{}, types.ErrNoValidPrice(k.codespace) + } + return price, nil } // GetRawPrices fetches the set of all prices posted by oracles for an asset diff --git a/x/pricefeed/keeper/keeper_test.go b/x/pricefeed/keeper/keeper_test.go index 7c06172b..d29298a3 100644 --- a/x/pricefeed/keeper/keeper_test.go +++ b/x/pricefeed/keeper/keeper_test.go @@ -120,7 +120,8 @@ func TestKeeper_GetSetCurrentPrice(t *testing.T) { err := keeper.SetCurrentPrices(ctx, "tstusd") require.NoError(t, err) // Get Current price - price := keeper.GetCurrentPrice(ctx, "tstusd") + price, err := keeper.GetCurrentPrice(ctx, "tstusd") + require.Nil(t, err) require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.34")), true) // Even number of oracles @@ -130,6 +131,7 @@ func TestKeeper_GetSetCurrentPrice(t *testing.T) { time.Now().Add(time.Hour*1)) err = keeper.SetCurrentPrices(ctx, "tstusd") require.NoError(t, err) - price = keeper.GetCurrentPrice(ctx, "tstusd") + price, err = keeper.GetCurrentPrice(ctx, "tstusd") + require.Nil(t, err) require.Equal(t, price.Price.Equal(sdk.MustNewDecFromStr("0.345")), true) } diff --git a/x/pricefeed/keeper/querier.go b/x/pricefeed/keeper/querier.go index a862b776..022011af 100644 --- a/x/pricefeed/keeper/querier.go +++ b/x/pricefeed/keeper/querier.go @@ -32,8 +32,10 @@ func queryCurrentPrice(ctx sdk.Context, path []string, req abci.RequestQuery, ke if !found { return []byte{}, sdk.ErrUnknownRequest("asset not found") } - currentPrice := keeper.GetCurrentPrice(ctx, marketID) - + currentPrice, err := keeper.GetCurrentPrice(ctx, marketID) + if err != nil { + return nil, err + } bz, err2 := codec.MarshalJSONIndent(keeper.cdc, currentPrice) if err2 != nil { panic("could not marshal result to JSON") From 22e168d06ae09e0a6f9aaafd331fafc3b7f9de8f Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Tue, 14 Jan 2020 15:00:37 +0100 Subject: [PATCH 52/99] Various Auction TODOs (#281) * make auctions not expire without bids * add events * improve genesis state validation * add genesis tests * update comment Co-Authored-By: Kevin Davis * add more events attributes Co-authored-by: Kevin Davis --- app/app.go | 2 +- x/auction/alias.go | 4 +- x/auction/genesis.go | 40 +++++++++-- x/auction/genesis_test.go | 109 +++++++++++++++++++++++++++++ x/auction/handler.go | 16 ++++- x/auction/keeper/auctions.go | 112 +++++++++++++++++++++++++++--- x/auction/keeper/auctions_test.go | 15 ++-- x/auction/module.go | 24 ++++--- x/auction/spec/04_events.md | 33 ++++++++- x/auction/types/auctions.go | 101 ++++++++++++++++++++------- x/auction/types/codec.go | 1 + x/auction/types/events.go | 17 +++++ x/auction/types/genesis.go | 55 +++++++++++---- x/auction/types/genesis_test.go | 46 ++++++++++++ 14 files changed, 493 insertions(+), 82 deletions(-) create mode 100644 x/auction/genesis_test.go create mode 100644 x/auction/types/events.go create mode 100644 x/auction/types/genesis_test.go diff --git a/app/app.go b/app/app.go index 9e9a1ea0..5ad36569 100644 --- a/app/app.go +++ b/app/app.go @@ -261,7 +261,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, slashing.NewAppModule(app.slashingKeeper, app.stakingKeeper), staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper), validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper), - auction.NewAppModule(app.auctionKeeper), + auction.NewAppModule(app.auctionKeeper, app.supplyKeeper), cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper), pricefeed.NewAppModule(app.pricefeedKeeper), ) diff --git a/x/auction/alias.go b/x/auction/alias.go index 99b63c4f..7b8ece33 100644 --- a/x/auction/alias.go +++ b/x/auction/alias.go @@ -29,7 +29,6 @@ var ( RegisterCodec = types.RegisterCodec NewGenesisState = types.NewGenesisState DefaultGenesisState = types.DefaultGenesisState - ValidateGenesis = types.ValidateGenesis GetAuctionKey = types.GetAuctionKey GetAuctionByTimeKey = types.GetAuctionByTimeKey Uint64FromBytes = types.Uint64FromBytes @@ -58,7 +57,8 @@ type ( CollateralAuction = types.CollateralAuction WeightedAddresses = types.WeightedAddresses SupplyKeeper = types.SupplyKeeper - Auctions = types.Auctions + GenesisAuctions = types.GenesisAuctions + GenesisAuction = types.GenesisAuction GenesisState = types.GenesisState MsgPlaceBid = types.MsgPlaceBid Params = types.Params diff --git a/x/auction/genesis.go b/x/auction/genesis.go index b5fad1ab..eedcdf1e 100644 --- a/x/auction/genesis.go +++ b/x/auction/genesis.go @@ -1,17 +1,39 @@ package auction import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/auction/types" ) -// InitGenesis initializes the store state from genesis data. -func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { - keeper.SetNextAuctionID(ctx, data.NextAuctionID) +// InitGenesis initializes the store state from a genesis state. +func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper types.SupplyKeeper, gs GenesisState) { + if err := gs.Validate(); err != nil { + panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err)) + } - keeper.SetParams(ctx, data.Params) + keeper.SetNextAuctionID(ctx, gs.NextAuctionID) - for _, a := range data.Auctions { + keeper.SetParams(ctx, gs.Params) + + totalAuctionCoins := sdk.NewCoins() + for _, a := range gs.Auctions { keeper.SetAuction(ctx, a) + // find the total coins that should be present in the module account + totalAuctionCoins.Add(a.GetModuleAccountCoins()) + } + + // check if the module account exists + moduleAcc := supplyKeeper.GetModuleAccount(ctx, ModuleName) + if moduleAcc == nil { + panic(fmt.Sprintf("%s module account has not been set", ModuleName)) + } + // check module coins match auction coins + // Note: Other sdk modules do not check this, instead just using the existing module account coins, or if zero, setting them. + if !moduleAcc.GetCoins().IsEqual(totalAuctionCoins) { + panic(fmt.Sprintf("total auction coins (%s) do not equal (%s) module account (%s) ", moduleAcc.GetCoins(), ModuleName, totalAuctionCoins)) } } @@ -24,9 +46,13 @@ func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { params := keeper.GetParams(ctx) - var genAuctions Auctions + genAuctions := GenesisAuctions{} // return empty list instead of nil if no auctions keeper.IterateAuctions(ctx, func(a Auction) bool { - genAuctions = append(genAuctions, a) + ga, ok := a.(types.GenesisAuction) + if !ok { + panic("could not convert stored auction to GenesisAuction type") + } + genAuctions = append(genAuctions, ga) return false }) diff --git a/x/auction/genesis_test.go b/x/auction/genesis_test.go new file mode 100644 index 00000000..a7352c52 --- /dev/null +++ b/x/auction/genesis_test.go @@ -0,0 +1,109 @@ +package auction_test + +import ( + "sort" + "testing" + "time" + + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/auction" +) + +var testTime = time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) +var testAuction = auction.NewCollateralAuction( + "seller", + c("lotdenom", 10), + testTime, + c("biddenom", 1000), + auction.WeightedAddresses{}, + c("debt", 1000), +).WithID(3).(auction.GenesisAuction) + +func TestInitGenesis(t *testing.T) { + t.Run("valid", func(t *testing.T) { + // setup keepers + tApp := app.NewTestApp() + keeper := tApp.GetAuctionKeeper() + ctx := tApp.NewContext(true, abci.Header{}) + // create genesis + gs := auction.NewGenesisState( + 10, + auction.DefaultParams(), + auction.GenesisAuctions{testAuction}, + ) + + // run init + require.NotPanics(t, func() { + auction.InitGenesis(ctx, keeper, tApp.GetSupplyKeeper(), gs) + }) + + // check state is as expected + actualID, err := keeper.GetNextAuctionID(ctx) + require.NoError(t, err) + require.Equal(t, gs.NextAuctionID, actualID) + + require.Equal(t, gs.Params, keeper.GetParams(ctx)) + + // TODO is there a nicer way of comparing state? + sort.Slice(gs.Auctions, func(i, j int) bool { + return gs.Auctions[i].GetID() > gs.Auctions[j].GetID() + }) + i := 0 + keeper.IterateAuctions(ctx, func(a auction.Auction) bool { + require.Equal(t, gs.Auctions[i], a) + i++ + return false + }) + }) + t.Run("invalid", func(t *testing.T) { + // setup keepers + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{}) + + // create invalid genesis + gs := auction.NewGenesisState( + 0, // next id < testAuction ID + auction.DefaultParams(), + auction.GenesisAuctions{testAuction}, + ) + + // check init fails + require.Panics(t, func() { + auction.InitGenesis(ctx, tApp.GetAuctionKeeper(), tApp.GetSupplyKeeper(), gs) + }) + }) +} + +func TestExportGenesis(t *testing.T) { + t.Run("default", func(t *testing.T) { + // setup state + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{}) + tApp.InitializeFromGenesisStates() + + // export + gs := auction.ExportGenesis(ctx, tApp.GetAuctionKeeper()) + + // check state matches + require.Equal(t, auction.DefaultGenesisState(), gs) + }) + t.Run("one auction", func(t *testing.T) { + // setup state + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{}) + tApp.InitializeFromGenesisStates() + tApp.GetAuctionKeeper().SetAuction(ctx, testAuction) + + // export + gs := auction.ExportGenesis(ctx, tApp.GetAuctionKeeper()) + + // check state matches + expectedGenesisState := auction.DefaultGenesisState() + expectedGenesisState.Auctions = append(expectedGenesisState.Auctions, testAuction) + require.Equal(t, expectedGenesisState, gs) + }) +} diff --git a/x/auction/handler.go b/x/auction/handler.go index f9b7c137..be5f9d30 100644 --- a/x/auction/handler.go +++ b/x/auction/handler.go @@ -4,11 +4,15 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/auction/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 { + ctx = ctx.WithEventManager(sdk.NewEventManager()) + switch msg := msg.(type) { case MsgPlaceBid: return handleMsgPlaceBid(ctx, keeper, msg) @@ -26,5 +30,15 @@ func handleMsgPlaceBid(ctx sdk.Context, keeper Keeper, msg MsgPlaceBid) sdk.Resu return err.Result() } - return sdk.Result{} + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Bidder.String()), + ), + ) + + return sdk.Result{ + Events: ctx.EventManager().Events(), + } } diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index 50979e26..b47313ce 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -16,7 +16,7 @@ func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin seller, lot, bidDenom, - ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration)) + types.DistantFuture) err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) if err != nil { @@ -27,6 +27,16 @@ func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin if err != nil { return 0, err } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeAuctionStart, + sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())), + sdk.NewAttribute(types.AttributeKeyAuctionType, auction.Name()), + sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom), + sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom), + ), + ) return auctionID, nil } @@ -37,7 +47,7 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in buyer, bid, initialLot, - ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration), + types.DistantFuture, debt) // This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker. @@ -55,6 +65,16 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in if err != nil { return 0, err } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeAuctionStart, + sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())), + sdk.NewAttribute(types.AttributeKeyAuctionType, auction.Name()), + sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom), + sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom), + ), + ) return auctionID, nil } @@ -68,7 +88,7 @@ func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.C auction := types.NewCollateralAuction( seller, lot, - ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), + types.DistantFuture, maxBid, weightedAddresses, debt) @@ -86,6 +106,16 @@ func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.C if err != nil { return 0, err } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeAuctionStart, + sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())), + sdk.NewAttribute(types.AttributeKeyAuctionType, auction.Name()), + sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom), + sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom), + ), + ) return auctionID, nil } @@ -128,6 +158,7 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddres } k.SetAuction(ctx, updatedAuction) + return nil } @@ -142,7 +173,7 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder } // New bidder pays back old bidder - // Catch edge cases of a bidder replacing their own bid, and the amount being zero (sending zero coins produces meaningless send events). + // Catch edge cases of a bidder replacing their own bid, or the amount being zero (sending zero coins produces meaningless send events). if !bidder.Equals(a.Bidder) && !a.Bid.IsZero() { err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid)) if err != nil { @@ -166,7 +197,21 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder // Update Auction a.Bidder = bidder a.Bid = bid - a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout + if !a.HasReceivedBids { + a.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid + } + a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout, up to MaxEndTime + a.HasReceivedBids = true + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeAuctionBid, + sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)), + sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()), + sdk.NewAttribute(types.AttributeKeyBidAmount, a.Bid.Amount.String()), + sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())), + ), + ) return a, nil } @@ -216,13 +261,26 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc return a, err } a.CorrespondingDebt = a.CorrespondingDebt.Sub(debtToReturn) // debtToReturn will always be ≤ a.CorrespondingDebt from the MinInt above - // TODO optionally burn out debt and stable just returned to liquidator } // Update Auction a.Bidder = bidder a.Bid = bid - a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout + if !a.HasReceivedBids { + a.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid + } + a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout, up to MaxEndTime + a.HasReceivedBids = true + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeAuctionBid, + sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)), + sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()), + sdk.NewAttribute(types.AttributeKeyBidAmount, a.Bid.Amount.String()), + sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())), + ), + ) return a, nil } @@ -271,7 +329,21 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc // Update Auction a.Bidder = bidder a.Lot = lot - a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout + if !a.HasReceivedBids { + a.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid + } + a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout, up to MaxEndTime + a.HasReceivedBids = true + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeAuctionBid, + sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)), + sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()), + sdk.NewAttribute(types.AttributeKeyLotAmount, a.Lot.Amount.String()), + sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())), + ), + ) return a, nil } @@ -312,13 +384,26 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac return a, err } a.CorrespondingDebt = a.CorrespondingDebt.Sub(debtToReturn) // debtToReturn will always be ≤ a.CorrespondingDebt from the MinInt above - // TODO optionally burn out debt and stable just returned to liquidator } // Update Auction a.Bidder = bidder a.Lot = lot - a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout + if !a.HasReceivedBids { + a.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid + } + a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout, up to MaxEndTime + a.HasReceivedBids = true + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeAuctionBid, + sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)), + sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()), + sdk.NewAttribute(types.AttributeKeyLotAmount, a.Lot.Amount.String()), + sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())), + ), + ) return a, nil } @@ -354,6 +439,13 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error { } k.DeleteAuction(ctx, auctionID) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeAuctionClose, + sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())), + ), + ) return nil } diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index 4ac3590d..3b0bc658 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -227,13 +227,14 @@ func TestStartSurplusAuction(t *testing.T) { // check auction in store and is correct require.True(t, found) expectedAuction := types.Auction(types.SurplusAuction{BaseAuction: types.BaseAuction{ - ID: 0, - Initiator: tc.args.seller, - Lot: tc.args.lot, - Bidder: nil, - Bid: c(tc.args.bidDenom, 0), - EndTime: tc.blockTime.Add(types.DefaultMaxAuctionDuration), - MaxEndTime: tc.blockTime.Add(types.DefaultMaxAuctionDuration), + ID: 0, + Initiator: tc.args.seller, + Lot: tc.args.lot, + Bidder: nil, + Bid: c(tc.args.bidDenom, 0), + HasReceivedBids: false, + EndTime: types.DistantFuture, + MaxEndTime: types.DistantFuture, }}) require.Equal(t, expectedAuction, actualAuc) } else { diff --git a/x/auction/module.go b/x/auction/module.go index 14c2d023..acd36010 100644 --- a/x/auction/module.go +++ b/x/auction/module.go @@ -2,18 +2,19 @@ package auction import ( "encoding/json" - - "github.com/gorilla/mux" - "github.com/spf13/cobra" + "fmt" "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" + "github.com/gorilla/mux" + "github.com/spf13/cobra" 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" + "github.com/kava-labs/kava/x/auction/types" ) var ( @@ -41,12 +42,12 @@ func (AppModuleBasic) DefaultGenesis() json.RawMessage { // ValidateGenesis performs genesis state validation for the auction module. func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { - var data GenesisState - err := ModuleCdc.UnmarshalJSON(bz, &data) + var gs GenesisState + err := ModuleCdc.UnmarshalJSON(bz, &gs) if err != nil { - return err + return fmt.Errorf("failed to unmarshal %s genesis state: %w", ModuleName, err) } - return ValidateGenesis(data) + return gs.Validate() } // RegisterRESTRoutes registers the REST routes for the auction module. @@ -67,14 +68,17 @@ func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { // AppModule implements the sdk.AppModule interface. type AppModule struct { AppModuleBasic - keeper Keeper + + keeper Keeper + supplyKeeper types.SupplyKeeper } // NewAppModule creates a new AppModule object -func NewAppModule(keeper Keeper) AppModule { +func NewAppModule(keeper Keeper, supplyKeeper types.SupplyKeeper) AppModule { return AppModule{ AppModuleBasic: AppModuleBasic{}, keeper: keeper, + supplyKeeper: supplyKeeper, } } @@ -106,7 +110,7 @@ func (am AppModule) NewQuerierHandler() sdk.Querier { func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { var genesisState GenesisState ModuleCdc.MustUnmarshalJSON(data, &genesisState) - InitGenesis(ctx, am.keeper, genesisState) + InitGenesis(ctx, am.keeper, am.supplyKeeper, genesisState) return []abci.ValidatorUpdate{} } diff --git a/x/auction/spec/04_events.md b/x/auction/spec/04_events.md index 0c6b3238..fae55424 100644 --- a/x/auction/spec/04_events.md +++ b/x/auction/spec/04_events.md @@ -1,5 +1,32 @@ # Events - +The `x/auction` module emits the following events: + +## Triggered By Other Modules + +| Type | Attribute Key | Attribute Value | +| ------------- | ------------- | ------------------- | +| auction_start | auction_id | {auction ID} | +| auction_start | auction_type | {auction type} | +| auction_start | lot_denom | {auction lot denom} | +| auction_start | bid_denom | {auction bid denom} | + +## Handlers + +### MsgPlaceBid + +| Type | Attribute Key | Attribute Value | +| ----------- | ------------- | ------------------ | +| auction_bid | auction_id | {auction ID} | +| auction_bid | bidder | {latest bidder} | +| auction_bid | bid_amount | {coin amount} | +| auction_bid | lot_amount | {coin amount} | +| auction_bid | end_time | {auction end time} | +| message | module | auction | +| message | sender | {sender address} | + +## EndBlock + +| Type | Attribute Key | Attribute Value | +| ------------- | ------------- | --------------- | +| auction_close | auction_id | {auction ID} | diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index beec6d24..c4b469f8 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -8,6 +8,11 @@ import ( "github.com/cosmos/cosmos-sdk/x/supply" ) +// distantFuture is a very large time value to use as initial the ending time for auctions. +// It is not set to the max time supported. This can cause problems with time comparisons, see https://stackoverflow.com/a/32620397. +// Also amino panics when encoding times ≥ the start of year 10000. +var DistantFuture time.Time = time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC) + // Auction is an interface for handling common actions on auctions. type Auction interface { GetID() uint64 @@ -17,13 +22,14 @@ type Auction interface { // BaseAuction is a common type shared by all Auctions. type BaseAuction struct { - ID uint64 - Initiator string // Module name that starts the auction. Pays out Lot. - Lot sdk.Coin // Coins that will paid out by Initiator to the winning bidder. - Bidder sdk.AccAddress // Latest bidder. Receiver of Lot. - Bid sdk.Coin // Coins paid into the auction the bidder. - EndTime time.Time // Current auction closing time. Triggers at the end of the block with time ≥ EndTime. - MaxEndTime time.Time // Maximum closing time. Auctions can close before this but never after. + ID uint64 + Initiator string // Module name that starts the auction. Pays out Lot. + Lot sdk.Coin // Coins that will paid out by Initiator to the winning bidder. + Bidder sdk.AccAddress // Latest bidder. Receiver of Lot. + Bid sdk.Coin // Coins paid into the auction the bidder. + HasReceivedBids bool // Whether the auction has received any bids or not. + EndTime time.Time // Current auction closing time. Triggers at the end of the block with time ≥ EndTime. + MaxEndTime time.Time // Maximum closing time. Auctions can close before this but never after. } // GetID is a getter for auction ID. @@ -32,6 +38,13 @@ func (a BaseAuction) GetID() uint64 { return a.ID } // GetEndTime is a getter for auction end time. func (a BaseAuction) GetEndTime() time.Time { return a.EndTime } +func (a BaseAuction) Validate() error { + if a.EndTime.After(a.MaxEndTime) { + return fmt.Errorf("MaxEndTime < EndTime (%s < %s)", a.MaxEndTime, a.EndTime) + } + return nil +} + func (a BaseAuction) String() string { return fmt.Sprintf(`Auction %d: Initiator: %s @@ -55,16 +68,27 @@ type SurplusAuction struct { // WithID returns an auction with the ID set. func (a SurplusAuction) WithID(id uint64) Auction { a.ID = id; return a } +// Name returns a name for this auction type. Used to identify auctions in event attributes. +func (a SurplusAuction) Name() string { return "surplus" } + +// GetModuleAccountCoins returns the total number of coins held in the module account for this auction. +// It is used in genesis initialize the module account correctly. +func (a SurplusAuction) GetModuleAccountCoins() sdk.Coins { + // a.Bid is paid out on bids, so is never stored in the module account + return sdk.NewCoins(a.Lot) +} + // NewSurplusAuction returns a new surplus auction. func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime time.Time) SurplusAuction { auction := SurplusAuction{BaseAuction{ // no ID - Initiator: seller, - Lot: lot, - Bidder: nil, - Bid: sdk.NewInt64Coin(bidDenom, 0), - EndTime: endTime, - MaxEndTime: endTime, + Initiator: seller, + Lot: lot, + Bidder: nil, + Bid: sdk.NewInt64Coin(bidDenom, 0), + HasReceivedBids: false, // new auctions don't have any bids + EndTime: endTime, + MaxEndTime: endTime, }} return auction } @@ -79,20 +103,32 @@ type DebtAuction struct { // WithID returns an auction with the ID set. func (a DebtAuction) WithID(id uint64) Auction { a.ID = id; return a } +// Name returns a name for this auction type. Used to identify auctions in event attributes. +func (a DebtAuction) Name() string { return "debt" } + +// GetModuleAccountCoins returns the total number of coins held in the module account for this auction. +// It is used in genesis initialize the module account correctly. +func (a DebtAuction) GetModuleAccountCoins() sdk.Coins { + // a.Lot is minted at auction close, so is never stored in the module account + // a.Bid is paid out on bids, so is never stored in the module account + return sdk.NewCoins(a.CorrespondingDebt) +} + // NewDebtAuction returns a new debt auction. -func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime time.Time, debt sdk.Coin) DebtAuction { +func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, endTime time.Time, debt sdk.Coin) DebtAuction { // Note: Bidder is set to the initiator's module account address instead of module name. (when the first bid is placed, it is paid out to the initiator) // Setting to the module account address bypasses calling supply.SendCoinsFromModuleToModule, instead calls SendCoinsFromModuleToAccount. // This isn't a problem currently, but if additional logic/validation was added for sending to coins to Module Accounts, it would be bypassed. auction := DebtAuction{ BaseAuction: BaseAuction{ // no ID - Initiator: buyerModAccName, - Lot: initialLot, - Bidder: supply.NewModuleAddress(buyerModAccName), // send proceeds from the first bid to the buyer. - Bid: bid, // amount that the buyer is buying - doesn't change over course of auction - EndTime: EndTime, - MaxEndTime: EndTime}, + Initiator: buyerModAccName, + Lot: initialLot, + Bidder: supply.NewModuleAddress(buyerModAccName), // send proceeds from the first bid to the buyer. + Bid: bid, // amount that the buyer is buying - doesn't change over course of auction + HasReceivedBids: false, // new auctions don't have any bids + EndTime: endTime, + MaxEndTime: endTime}, CorrespondingDebt: debt, } return auction @@ -113,6 +149,16 @@ type CollateralAuction struct { // WithID returns an auction with the ID set. func (a CollateralAuction) WithID(id uint64) Auction { a.ID = id; return a } +// Name returns a name for this auction type. Used to identify auctions in event attributes. +func (a CollateralAuction) Name() string { return "collateral" } + +// GetModuleAccountCoins returns the total number of coins held in the module account for this auction. +// It is used in genesis initialize the module account correctly. +func (a CollateralAuction) GetModuleAccountCoins() sdk.Coins { + // a.Bid is paid out on bids, so is never stored in the module account + return sdk.NewCoins(a.Lot).Add(sdk.NewCoins(a.CorrespondingDebt)) +} + // IsReversePhase returns whether the auction has switched over to reverse phase or not. // Auction initially start in forward phase. func (a CollateralAuction) IsReversePhase() bool { @@ -136,16 +182,17 @@ func (a CollateralAuction) String() string { } // NewCollateralAuction returns a new collateral auction. -func NewCollateralAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses, debt sdk.Coin) CollateralAuction { +func NewCollateralAuction(seller string, lot sdk.Coin, endTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses, debt sdk.Coin) CollateralAuction { auction := CollateralAuction{ BaseAuction: BaseAuction{ // no ID - Initiator: seller, - Lot: lot, - Bidder: nil, - Bid: sdk.NewInt64Coin(maxBid.Denom, 0), - EndTime: EndTime, - MaxEndTime: EndTime}, + Initiator: seller, + Lot: lot, + Bidder: nil, + Bid: sdk.NewInt64Coin(maxBid.Denom, 0), + HasReceivedBids: false, // new auctions don't have any bids + EndTime: endTime, + MaxEndTime: endTime}, CorrespondingDebt: debt, MaxBid: maxBid, LotReturns: lotReturns, diff --git a/x/auction/types/codec.go b/x/auction/types/codec.go index 1904f432..e185552b 100644 --- a/x/auction/types/codec.go +++ b/x/auction/types/codec.go @@ -15,6 +15,7 @@ func init() { func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(MsgPlaceBid{}, "auction/MsgPlaceBid", nil) + cdc.RegisterInterface((*GenesisAuction)(nil), nil) cdc.RegisterInterface((*Auction)(nil), nil) cdc.RegisterConcrete(SurplusAuction{}, "auction/SurplusAuction", nil) cdc.RegisterConcrete(DebtAuction{}, "auction/DebtAuction", nil) diff --git a/x/auction/types/events.go b/x/auction/types/events.go new file mode 100644 index 00000000..032882c7 --- /dev/null +++ b/x/auction/types/events.go @@ -0,0 +1,17 @@ +package types + +const ( + EventTypeAuctionStart = "auction_start" + EventTypeAuctionBid = "auction_bid" + EventTypeAuctionClose = "auction_close" + + AttributeValueCategory = ModuleName + AttributeKeyAuctionID = "auction_id" + AttributeKeyAuctionType = "auction_type" + AttributeKeyBidder = "bidder" + AttributeKeyBidDenom = "bid_denom" + AttributeKeyLotDenom = "lot_denom" + AttributeKeyBidAmount = "bid_amount" + AttributeKeyLotAmount = "lot_amount" + AttributeKeyEndTime = "end_time" +) diff --git a/x/auction/types/genesis.go b/x/auction/types/genesis.go index 808ac050..ccf75c8e 100644 --- a/x/auction/types/genesis.go +++ b/x/auction/types/genesis.go @@ -2,20 +2,30 @@ package types import ( "bytes" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" ) -// Auctions is a slice of auctions. -type Auctions []Auction +// GenesisAuction is an interface that extends the auction interface to add functionality needed for initializing auctions from genesis. +type GenesisAuction interface { + Auction + GetModuleAccountCoins() sdk.Coins + Validate() error +} + +// GenesisAuctions is a slice of genesis auctions. +type GenesisAuctions []GenesisAuction // GenesisState is auction state that must be provided at chain genesis. type GenesisState struct { - NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` - Params Params `json:"auction_params" yaml:"auction_params"` - Auctions Auctions `json:"genesis_auctions" yaml:"genesis_auctions"` + NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` + Params Params `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(nextID uint64, ap Params, ga Auctions) GenesisState { +func NewGenesisState(nextID uint64, ap Params, ga GenesisAuctions) GenesisState { return GenesisState{ NextAuctionID: nextID, Params: ap, @@ -25,25 +35,42 @@ func NewGenesisState(nextID uint64, ap Params, ga Auctions) GenesisState { // DefaultGenesisState returns the default genesis state for auction module. func DefaultGenesisState() GenesisState { - return NewGenesisState(0, DefaultParams(), Auctions{}) + return NewGenesisState(0, DefaultParams(), GenesisAuctions{}) } // Equal checks whether two GenesisState structs are equivalent. -func (data GenesisState) Equal(data2 GenesisState) bool { - b1 := ModuleCdc.MustMarshalBinaryBare(data) - b2 := ModuleCdc.MustMarshalBinaryBare(data2) +func (gs GenesisState) Equal(gs2 GenesisState) bool { + b1 := ModuleCdc.MustMarshalBinaryBare(gs) + b2 := ModuleCdc.MustMarshalBinaryBare(gs2) return bytes.Equal(b1, b2) } // IsEmpty returns true if a GenesisState is empty. -func (data GenesisState) IsEmpty() bool { - return data.Equal(GenesisState{}) +func (gs GenesisState) IsEmpty() bool { + return gs.Equal(GenesisState{}) } // ValidateGenesis validates genesis inputs. It returns error if validation of any input fails. -func ValidateGenesis(data GenesisState) error { - if err := data.Params.Validate(); err != nil { +func (gs GenesisState) Validate() error { + if err := gs.Params.Validate(); err != nil { return err } + + ids := map[uint64]bool{} + for _, a := range gs.Auctions { + + if err := a.Validate(); err != nil { + return fmt.Errorf("found invalid auction: %w", err) + } + + if ids[a.GetID()] { + return fmt.Errorf("found duplicate auction ID (%d)", a.GetID()) + } + ids[a.GetID()] = true + + if a.GetID() >= gs.NextAuctionID { + return fmt.Errorf("found auction ID >= the nextAuctionID (%d >= %d)", a.GetID(), gs.NextAuctionID) + } + } return nil } diff --git a/x/auction/types/genesis_test.go b/x/auction/types/genesis_test.go new file mode 100644 index 00000000..4810741e --- /dev/null +++ b/x/auction/types/genesis_test.go @@ -0,0 +1,46 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +var testCoin = sdk.NewInt64Coin("test", 20) + +func TestGenesisState_Validate(t *testing.T) { + testCases := []struct { + name string + nextID uint64 + auctions GenesisAuctions + expectPass bool + }{ + {"default", DefaultGenesisState().NextAuctionID, DefaultGenesisState().Auctions, true}, + {"invalid next ID", 54, GenesisAuctions{SurplusAuction{BaseAuction{ID: 105}}}, false}, + { + "repeated ID", + 1000, + GenesisAuctions{ + SurplusAuction{BaseAuction{ID: 105}}, + DebtAuction{BaseAuction{ID: 105}, testCoin}, + }, + false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gs := NewGenesisState(tc.nextID, DefaultParams(), tc.auctions) + + err := gs.Validate() + + if tc.expectPass { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } + +} From 8128a680cc8c36c23d28c2ec183aea2377047419 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Tue, 14 Jan 2020 16:04:47 +0100 Subject: [PATCH 53/99] Ro address auction todos (#284) * make auctions not expire without bids * add events * improve genesis state validation * add genesis tests * Keeper auctions test, types auctions test, keeper bidding test * Resolved TODOs, added querier test * Removed 'import x/liquidator' from keeper_test package for circleci * Fixes for lack of liquidator module account in tests * update comment Co-Authored-By: Kevin Davis * add more events attributes * feat: add back bidding on closed auction test * feat: test failed debt/collateral auctions Co-authored-by: Ruaridh Co-authored-by: Denali Marsh --- x/auction/keeper/auctions.go | 6 - x/auction/keeper/auctions_test.go | 160 ++++++++++++++ x/auction/keeper/bidding_test.go | 349 ++++++++++++++++++++++++++++++ x/auction/keeper/querier.go | 5 +- x/auction/keeper/querier_test.go | 104 +++++++++ x/auction/types/auctions.go | 19 ++ x/auction/types/auctions_test.go | 195 +++++++++++++++++ x/auction/types/keys.go | 2 + x/auction/types/querier.go | 14 ++ 9 files changed, 845 insertions(+), 9 deletions(-) create mode 100644 x/auction/keeper/bidding_test.go create mode 100644 x/auction/keeper/querier_test.go create mode 100644 x/auction/types/auctions_test.go diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index b47313ce..e1465668 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -294,9 +294,6 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc if !a.IsReversePhase() { return a, sdk.ErrInternal("auction not in reverse phase") } - if lot.IsNegative() { - return a, sdk.ErrInternal("can't bid negative amount") - } if !lot.IsLT(a.Lot) { return a, sdk.ErrInternal("auction in reverse phase, new bid not less than previous amount") } @@ -354,9 +351,6 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac if lot.Denom != a.Lot.Denom { return a, sdk.ErrInternal("lot denom doesn't match auction") } - if lot.IsNegative() { - return a, sdk.ErrInternal("lot less than 0") - } if !lot.IsLT(a.Lot) { return a, sdk.ErrInternal("lot not smaller than last lot") } diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index 3b0bc658..3b7fd5d4 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -100,6 +100,48 @@ func TestDebtAuctionBasic(t *testing.T) { tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) } +func TestDebtAuctionDebtRemaining(t *testing.T) { + // Setup + _, addrs := app.GeneratePrivKeyAddressPairs(1) + seller := addrs[0] + buyerModName := cdp.LiquidatorMacc + buyerAddr := supply.NewModuleAddress(buyerModName) + + tApp := app.NewTestApp() + + buyerAcc := supply.NewEmptyModuleAccount(buyerModName, supply.Minter) // reverse auctions mint payout + require.NoError(t, buyerAcc.SetCoins(cs(c("debt", 100)))) + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(seller, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + buyerAcc, + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Start auction + auctionID, err := keeper.StartDebtAuction(ctx, buyerModName, c("token1", 10), c("token2", 99999), c("debt", 20)) + require.NoError(t, err) + // Check buyer's coins have not decreased (except for debt), as lot is minted at the end + tApp.CheckBalance(t, ctx, buyerAddr, cs(c("debt", 80))) + + // Place a bid + require.NoError(t, keeper.PlaceBid(ctx, 0, seller, c("token2", 10))) + // Check seller's coins have decreased + tApp.CheckBalance(t, ctx, seller, cs(c("token1", 90), c("token2", 100))) + // Check buyer's coins have increased + tApp.CheckBalance(t, ctx, buyerAddr, cs(c("token1", 10), c("debt", 90))) + + // Close auction at just after auction expiry + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration)) + require.NoError(t, keeper.CloseAuction(ctx, auctionID)) + // Check seller's coins increased + tApp.CheckBalance(t, ctx, seller, cs(c("token1", 90), c("token2", 110))) + // check that debt has increased due to corresponding debt being greater than bid + tApp.CheckBalance(t, ctx, buyerAddr, cs(c("token1", 10), c("debt", 100))) +} + func TestCollateralAuctionBasic(t *testing.T) { // Setup _, addrs := app.GeneratePrivKeyAddressPairs(4) @@ -160,6 +202,59 @@ func TestCollateralAuctionBasic(t *testing.T) { tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 115), c("token2", 50))) } +func TestCollateralAuctionDebtRemaining(t *testing.T) { + // Setup + _, addrs := app.GeneratePrivKeyAddressPairs(4) + buyer := addrs[0] + returnAddrs := addrs[1:] + returnWeights := is(30, 20, 10) + sellerModName := cdp.LiquidatorMacc + sellerAddr := supply.NewModuleAddress(sellerModName) + + tApp := app.NewTestApp() + sellerAcc := supply.NewEmptyModuleAccount(sellerModName) + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100), c("debt", 100)))) + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + auth.NewBaseAccount(returnAddrs[0], cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + auth.NewBaseAccount(returnAddrs[1], cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + auth.NewBaseAccount(returnAddrs[2], cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + sellerAcc, + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Start auction + auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights, c("debt", 40)) + require.NoError(t, err) + // Check seller's coins have decreased + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100), c("debt", 60))) + + // Place a forward bid + require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 10))) + // Check bidder's coins have decreased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) + // Check seller's coins have increased + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 110), c("debt", 70))) + // Check return addresses have not received coins + for _, ra := range returnAddrs { + tApp.CheckBalance(t, ctx, ra, cs(c("token1", 100), c("token2", 100))) + } + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration)) + require.NoError(t, keeper.CloseAuction(ctx, auctionID)) + + // check that buyers coins have increased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90))) + // Check return addresses have not received coins + for _, ra := range returnAddrs { + tApp.CheckBalance(t, ctx, ra, cs(c("token1", 100), c("token2", 100))) + } + // check that token2 has increased by 10, debt by 40, for a net debt increase of 30 debt + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 110), c("debt", 100))) +} + func TestStartSurplusAuction(t *testing.T) { someTime := time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC) type args struct { @@ -247,3 +342,68 @@ func TestStartSurplusAuction(t *testing.T) { }) } } + +func TestCloseAuction(t *testing.T) { + // Set up + _, addrs := app.GeneratePrivKeyAddressPairs(1) + buyer := addrs[0] + sellerModName := cdp.LiquidatorMacc + + tApp := app.NewTestApp() + + sellerAcc := supply.NewEmptyModuleAccount(sellerModName, supply.Burner) // forward auctions burn proceeds + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100)))) + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + sellerAcc, + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Create an auction (lot: 20 token1, initialBid: 0 token2) + id, err := keeper.StartSurplusAuction(ctx, sellerModName, c("token1", 20), "token2") // lot, bid denom + require.NoError(t, err) + + // Attempt to close the auction before EndTime + require.Error(t, keeper.CloseAuction(ctx, id)) + + // Attempt to close auction that does not exist + require.Error(t, keeper.CloseAuction(ctx, 999)) +} + +func TestCloseExpiredAuctions(t *testing.T) { + // Set up + _, addrs := app.GeneratePrivKeyAddressPairs(1) + buyer := addrs[0] + sellerModName := "liquidator" + + tApp := app.NewTestApp() + + sellerAcc := supply.NewEmptyModuleAccount(sellerModName, supply.Burner) // forward auctions burn proceeds + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100)))) + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + sellerAcc, + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Start auction 1 + _, err := keeper.StartSurplusAuction(ctx, sellerModName, c("token1", 20), "token2") // lot, bid denom + require.NoError(t, err) + + // Start auction 2 + _, err = keeper.StartSurplusAuction(ctx, sellerModName, c("token1", 20), "token2") // lot, bid denom + require.NoError(t, err) + + // Fast forward the block time + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultMaxAuctionDuration).Add(1)) + + // Close expired auctions + err = keeper.CloseExpiredAuctions(ctx) + require.NoError(t, err) +} diff --git a/x/auction/keeper/bidding_test.go b/x/auction/keeper/bidding_test.go new file mode 100644 index 00000000..7703f86e --- /dev/null +++ b/x/auction/keeper/bidding_test.go @@ -0,0 +1,349 @@ +package keeper_test + +import ( + "strings" + "testing" + "time" + + 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/supply" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/auction/types" +) + +type AuctionType int + +const ( + Invalid AuctionType = 0 + Surplus AuctionType = 1 + Debt AuctionType = 2 + CollateralPhase1 AuctionType = 3 + CollateralPhase2 AuctionType = 4 +) + +func TestAuctionBidding(t *testing.T) { + someTime := time.Date(0001, time.January, 1, 0, 0, 0, 0, time.UTC) + + _, addrs := app.GeneratePrivKeyAddressPairs(5) + buyer := addrs[0] + secondBuyer := addrs[1] + modName := "liquidator" + collateralAddrs := addrs[2:] + collateralWeights := is(30, 20, 10) + + tApp := app.NewTestApp() + + // Set up seller account + sellerAcc := supply.NewEmptyModuleAccount(modName, supply.Minter, supply.Burner) + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 1000), c("token2", 1000), c("debt", 1000)))) + + // Initialize genesis accounts + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + auth.NewBaseAccount(secondBuyer, cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + auth.NewBaseAccount(collateralAddrs[0], cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + auth.NewBaseAccount(collateralAddrs[1], cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + auth.NewBaseAccount(collateralAddrs[2], cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + sellerAcc, + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + type auctionArgs struct { + auctionType AuctionType + seller string + lot sdk.Coin + bid sdk.Coin + debt sdk.Coin + addresses []sdk.AccAddress + weights []sdk.Int + } + + type bidArgs struct { + bidder sdk.AccAddress + amount sdk.Coin + secondBidder sdk.AccAddress + } + + tests := []struct { + name string + auctionArgs auctionArgs + bidArgs bidArgs + expectedError string + expectedEndTime time.Time + expectedBidder sdk.AccAddress + expectedBid sdk.Coin + expectpass bool + }{ + { + "basic: auction doesn't exist", + auctionArgs{Surplus, "", c("token1", 1), c("token2", 1), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, + bidArgs{buyer, c("token2", 10), nil}, + "auction doesn't exist", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + false, + }, + { + "surplus: normal", + auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, + bidArgs{buyer, c("token2", 10), nil}, + "", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + true, + }, + { + "surplus: second bidder", + auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, + bidArgs{buyer, c("token2", 10), secondBuyer}, + "", + someTime.Add(types.DefaultBidDuration), + secondBuyer, + c("token2", 11), + true, + }, + { + "surplus: invalid bid denom", + auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, + bidArgs{buyer, c("badtoken", 10), nil}, + "bid denom doesn't match auction", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + false, + }, + { + "surplus: invalid bid (equal)", + auctionArgs{Surplus, modName, c("token1", 100), c("token2", 0), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, + bidArgs{buyer, c("token2", 0), nil}, + "bid not greater than last bid", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + false, + }, + { + "debt: normal", + auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot + bidArgs{buyer, c("token1", 10), nil}, + "", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 100), + true, + }, + { + "debt: second bidder", + auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot + bidArgs{buyer, c("token1", 10), secondBuyer}, + "", + someTime.Add(types.DefaultBidDuration), + secondBuyer, + c("token2", 100), + true, + }, + { + "debt: invalid lot denom", + auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot + bidArgs{buyer, c("badtoken", 10), nil}, + "lot denom doesn't match auction", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token1", 20), + false, + }, + { + "debt: invalid lot size (larger)", + auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, + bidArgs{buyer, c("token1", 21), nil}, + "lot not smaller than last lot", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token1", 20), + false, + }, + { + "collateral [forward]: normal", + auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token2", 10), nil}, + "", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + true, + }, + { + "collateral [forward]: second bidder", + auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token2", 10), secondBuyer}, + "", + someTime.Add(types.DefaultBidDuration), + secondBuyer, + c("token2", 11), + true, + }, + { + "collateral [forward]: invalid bid denom", + auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("badtoken", 10), nil}, + "bid denom doesn't match auction", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + false, + }, + { + "collateral [forward]: invalid bid size (smaller)", + auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token2", 0), nil}, // lot, bid + "auction in forward phase, new bid not higher than last bid", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + false, + }, + { + "collateral [forward]: invalid bid size (greater than max)", + auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token2", 101), nil}, // lot, bid + "bid higher than max bid", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + false, + }, + { + "collateral [reverse]: normal", + auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token1", 15), nil}, + "", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 50), + true, + }, + { + "collateral [reverse]: second bidder", + auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token1", 15), secondBuyer}, + "", + someTime.Add(types.DefaultBidDuration), + secondBuyer, + c("token2", 50), + true, + }, + { + "collateral [reverse]: invalid lot denom", + auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("badtoken", 15), nil}, + "lot denom doesn't match auction", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 50), + false, + }, + { + "collateral [reverse]: invalid lot size (equal)", + auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token1", 20), nil}, + "auction in reverse phase, new bid not less than previous amount", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 50), + false, + }, + { + "collateral [reverse]: invalid lot size (greater)", + auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token1", 21), nil}, + "auction in reverse phase, new bid not less than previous amount", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 50), + false, + }, + { + "basic: closed auction", + auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, + bidArgs{buyer, c("token2", 10), nil}, + "auction has closed", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Start Auction + var id uint64 + var err error + switch tc.auctionArgs.auctionType { + case Surplus: + id, _ = keeper.StartSurplusAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid.Denom) + case Debt: + id, _ = keeper.StartDebtAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.bid, tc.auctionArgs.lot, tc.auctionArgs.debt) + case CollateralPhase1, CollateralPhase2: + id, _ = keeper.StartCollateralAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid, tc.auctionArgs.addresses, tc.auctionArgs.weights, tc.auctionArgs.debt) // seller, lot, maxBid, otherPerson + // Move CollateralAuction to debt phase by placing max bid + if tc.auctionArgs.auctionType == CollateralPhase2 { + err = keeper.PlaceBid(ctx, id, tc.bidArgs.bidder, tc.auctionArgs.bid) + require.NoError(t, err) + } + default: + t.Fail() + } + + // Close the auction early to test late bidding (if applicable) + if strings.Contains(tc.name, "closed") { + ctx = ctx.WithBlockTime(types.DistantFuture.Add(1)) + } + + // Place bid on auction + err = keeper.PlaceBid(ctx, id, tc.bidArgs.bidder, tc.bidArgs.amount) + + // Place second bid from new bidder + if tc.bidArgs.secondBidder != nil { + // Set bid increase/decrease based on auction type, phase + var secondBid sdk.Coin + switch tc.auctionArgs.auctionType { + case Surplus, CollateralPhase1: + secondBid = tc.bidArgs.amount.Add(c(tc.bidArgs.amount.Denom, 1)) + case Debt, CollateralPhase2: + secondBid = tc.bidArgs.amount.Sub(c(tc.bidArgs.amount.Denom, 1)) + default: + t.Fail() + } + // Place the second bid + err2 := keeper.PlaceBid(ctx, id, tc.bidArgs.secondBidder, secondBid) + require.NoError(t, err2) + } + + // Check success/failure + if tc.expectpass { + require.Nil(t, err) + // Get auction from store + auction, found := keeper.GetAuction(ctx, id) + require.True(t, found) + // Check auction values + require.Equal(t, modName, auction.GetInitiator()) + require.Equal(t, tc.expectedBidder, auction.GetBidder()) + require.Equal(t, tc.expectedBid, auction.GetBid()) + require.Equal(t, tc.expectedEndTime, auction.GetEndTime()) + } else { + // Check expected error message + require.Contains(t, err.Error(), tc.expectedError) + } + }) + } +} diff --git a/x/auction/keeper/querier.go b/x/auction/keeper/querier.go index 3b92476e..50afa363 100644 --- a/x/auction/keeper/querier.go +++ b/x/auction/keeper/querier.go @@ -1,7 +1,6 @@ package keeper import ( - "fmt" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/x/auction/types" @@ -21,10 +20,10 @@ func NewQuerier(keeper Keeper) sdk.Querier { } func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { - var auctionsList types.QueryResAuctions + var auctionsList types.Auctions keeper.IterateAuctions(ctx, func(a types.Auction) bool { - auctionsList = append(auctionsList, fmt.Sprintf("%+v", a)) // TODO formatting + auctionsList = append(auctionsList, a) return false }) diff --git a/x/auction/keeper/querier_test.go b/x/auction/keeper/querier_test.go new file mode 100644 index 00000000..c1ec1f7f --- /dev/null +++ b/x/auction/keeper/querier_test.go @@ -0,0 +1,104 @@ +package keeper_test + +import ( + "math/rand" + "strings" + "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/simulation" + "github.com/cosmos/cosmos-sdk/x/supply" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/auction/keeper" + "github.com/kava-labs/kava/x/auction/types" + "github.com/kava-labs/kava/x/cdp" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +const ( + custom = "custom" + TestAuctionCount = 10 +) + +type QuerierTestSuite struct { + suite.Suite + + keeper keeper.Keeper + app app.TestApp + auctions types.Auctions + ctx sdk.Context + querier sdk.Querier +} + +func (suite *QuerierTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + + _, addrs := app.GeneratePrivKeyAddressPairs(1) + buyer := addrs[0] + modName := cdp.LiquidatorMacc + + // Set up seller account + sellerAcc := supply.NewEmptyModuleAccount(modName, supply.Minter, supply.Burner) + sellerAcc.SetCoins(cs(c("token1", 1000), c("token2", 1000), c("debt", 1000))) + + // Initialize genesis accounts + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + sellerAcc, + }), + ) + + suite.ctx = ctx + suite.app = tApp + suite.keeper = tApp.GetAuctionKeeper() + + // Populate with auctions + for j := 0; j < TestAuctionCount; j++ { + lotAmount := simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 10, 100) + id, err := suite.keeper.StartSurplusAuction(suite.ctx, modName, c("token1", int64(lotAmount)), "token2") + suite.Nil(err) + + auc, found := suite.keeper.GetAuction(suite.ctx, id) + suite.True(found) + suite.auctions = append(suite.auctions, auc) + } + + suite.querier = keeper.NewQuerier(suite.keeper) +} + +func (suite *QuerierTestSuite) TestQueryAuctions() { + ctx := suite.ctx.WithIsCheckTx(false) + // Set up request query + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAuction}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryAllAuctionParams(1, TestAuctionCount)), + } + + // Execute query and check the []byte result + bz, err := suite.querier(ctx, []string{types.QueryGetAuction}, query) + suite.NoError(err) + suite.NotNil(bz) + + // Unmarshal the bytes into type Auctions + var auctions types.Auctions + suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &auctions)) + + // Check that each Auction has correct values + for i := 0; i < TestAuctionCount; i++ { + suite.Equal(suite.auctions[i].GetID(), auctions[i].GetID()) + suite.Equal(suite.auctions[i].GetInitiator(), auctions[i].GetInitiator()) + suite.Equal(suite.auctions[i].GetLot(), auctions[i].GetLot()) + suite.Equal(suite.auctions[i].GetBid(), auctions[i].GetBid()) + suite.Equal(suite.auctions[i].GetEndTime(), auctions[i].GetEndTime()) + } +} + +func TestQuerierTestSuite(t *testing.T) { + suite.Run(t, new(QuerierTestSuite)) +} diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index c4b469f8..203d9fec 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -17,9 +17,16 @@ var DistantFuture time.Time = time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC) type Auction interface { GetID() uint64 WithID(uint64) Auction + GetInitiator() string + GetLot() sdk.Coin + GetBidder() sdk.AccAddress + GetBid() sdk.Coin GetEndTime() time.Time } +// Auctions is a slice of auctions. +type Auctions []Auction + // BaseAuction is a common type shared by all Auctions. type BaseAuction struct { ID uint64 @@ -35,6 +42,18 @@ type BaseAuction struct { // GetID is a getter for auction ID. func (a BaseAuction) GetID() uint64 { return a.ID } +// GetInitiator is a getter for auction Initiator. +func (a BaseAuction) GetInitiator() string { return a.Initiator } + +// GetLot is a getter for auction Lot. +func (a BaseAuction) GetLot() sdk.Coin { return a.Lot } + +// GetBidder is a getter for auction Bidder. +func (a BaseAuction) GetBidder() sdk.AccAddress { return a.Bidder } + +// GetBid is a getter for auction Bid. +func (a BaseAuction) GetBid() sdk.Coin { return a.Bid } + // GetEndTime is a getter for auction end time. func (a BaseAuction) GetEndTime() time.Time { return a.EndTime } diff --git a/x/auction/types/auctions_test.go b/x/auction/types/auctions_test.go new file mode 100644 index 00000000..f46968d6 --- /dev/null +++ b/x/auction/types/auctions_test.go @@ -0,0 +1,195 @@ +package types + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +const ( + TestInitiatorModuleName = "liquidator" + TestLotDenom = "usdx" + TestLotAmount = 100 + TestBidDenom = "kava" + TestBidAmount = 20 + TestDebtDenom = "debt" + TestDebtAmount1 = 20 + TestDebtAmount2 = 15 + TestExtraEndTime = 10000 + TestAuctionID = 9999123 + TestAccAddress1 = "kava1qcfdf69js922qrdr4yaww3ax7gjml6pd39p8lj" + TestAccAddress2 = "kava1pdfav2cjhry9k79nu6r8kgknnjtq6a7rcr0qlr" +) + +func TestNewWeightedAddresses(t *testing.T) { + + tests := []struct { + name string + addresses []sdk.AccAddress + weights []sdk.Int + expectpass bool + }{ + { + "normal", + []sdk.AccAddress{ + sdk.AccAddress([]byte(TestAccAddress1)), + sdk.AccAddress([]byte(TestAccAddress2)), + }, + []sdk.Int{ + sdk.NewInt(6), + sdk.NewInt(8), + }, + true, + }, + { + "mismatched", + []sdk.AccAddress{ + sdk.AccAddress([]byte(TestAccAddress1)), + sdk.AccAddress([]byte(TestAccAddress2)), + }, + []sdk.Int{ + sdk.NewInt(6), + }, + false, + }, + { + "negativeWeight", + []sdk.AccAddress{ + sdk.AccAddress([]byte(TestAccAddress1)), + sdk.AccAddress([]byte(TestAccAddress2)), + }, + []sdk.Int{ + sdk.NewInt(6), + sdk.NewInt(-8), + }, + false, + }, + } + + // Run NewWeightedAdresses tests + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Attempt to instantiate new WeightedAddresses + weightedAddresses, err := NewWeightedAddresses(tc.addresses, tc.weights) + + if tc.expectpass { + // Confirm there is no error + require.Nil(t, err) + + // Check addresses, weights + require.Equal(t, tc.addresses, weightedAddresses.Addresses) + require.Equal(t, tc.weights, weightedAddresses.Weights) + } else { + // Confirm that there is an error + require.NotNil(t, err) + + switch tc.name { + case "mismatched": + require.Contains(t, err.Error(), "number of addresses doesn't match number of weights") + case "negativeWeight": + require.Contains(t, err.Error(), "weights contain a negative amount") + default: + // Unexpected error state + t.Fail() + } + } + }) + } +} + +func TestBaseAuctionGetters(t *testing.T) { + endTime := time.Now().Add(TestExtraEndTime) + + // Create a new BaseAuction (via SurplusAuction) + auction := NewSurplusAuction( + TestInitiatorModuleName, + c(TestLotDenom, TestLotAmount), + TestBidDenom, endTime, + ) + + auctionID := auction.GetID() + auctionBid := auction.GetBid() + auctionLot := auction.GetLot() + auctionEndTime := auction.GetEndTime() + auctionString := auction.String() + + require.Equal(t, auction.ID, auctionID) + require.Equal(t, auction.Bid, auctionBid) + require.Equal(t, auction.Lot, auctionLot) + require.Equal(t, auction.EndTime, auctionEndTime) + require.NotNil(t, auctionString) +} + +func TestNewSurplusAuction(t *testing.T) { + endTime := time.Now().Add(TestExtraEndTime) + + // Create a new SurplusAuction + surplusAuction := NewSurplusAuction( + TestInitiatorModuleName, + c(TestLotDenom, TestLotAmount), + TestBidDenom, endTime, + ) + + require.Equal(t, surplusAuction.Initiator, TestInitiatorModuleName) + require.Equal(t, surplusAuction.Lot, c(TestLotDenom, TestLotAmount)) + require.Equal(t, surplusAuction.Bid, c(TestBidDenom, 0)) + require.Equal(t, surplusAuction.EndTime, endTime) + require.Equal(t, surplusAuction.MaxEndTime, endTime) +} + +func TestNewDebtAuction(t *testing.T) { + endTime := time.Now().Add(TestExtraEndTime) + + // Create a new DebtAuction + debtAuction := NewDebtAuction( + TestInitiatorModuleName, + c(TestBidDenom, TestBidAmount), + c(TestLotDenom, TestLotAmount), + endTime, + c(TestDebtDenom, TestDebtAmount1), + ) + + require.Equal(t, debtAuction.Initiator, TestInitiatorModuleName) + require.Equal(t, debtAuction.Lot, c(TestLotDenom, TestLotAmount)) + require.Equal(t, debtAuction.Bid, c(TestBidDenom, TestBidAmount)) + require.Equal(t, debtAuction.EndTime, endTime) + require.Equal(t, debtAuction.MaxEndTime, endTime) + require.Equal(t, debtAuction.CorrespondingDebt, c(TestDebtDenom, TestDebtAmount1)) +} + +func TestNewCollateralAuction(t *testing.T) { + // Set up WeightedAddresses + addresses := []sdk.AccAddress{ + sdk.AccAddress([]byte(TestAccAddress1)), + sdk.AccAddress([]byte(TestAccAddress2)), + } + + weights := []sdk.Int{ + sdk.NewInt(6), + sdk.NewInt(8), + } + + weightedAddresses, _ := NewWeightedAddresses(addresses, weights) + + endTime := time.Now().Add(TestExtraEndTime) + + collateralAuction := NewCollateralAuction( + TestInitiatorModuleName, + c(TestLotDenom, TestLotAmount), + endTime, + c(TestBidDenom, TestBidAmount), + weightedAddresses, + c(TestDebtDenom, TestDebtAmount2), + ) + + require.Equal(t, collateralAuction.BaseAuction.Initiator, TestInitiatorModuleName) + require.Equal(t, collateralAuction.BaseAuction.Lot, c(TestLotDenom, TestLotAmount)) + require.Equal(t, collateralAuction.BaseAuction.Bid, c(TestBidDenom, 0)) + require.Equal(t, collateralAuction.BaseAuction.EndTime, endTime) + require.Equal(t, collateralAuction.BaseAuction.MaxEndTime, endTime) + require.Equal(t, collateralAuction.MaxBid, c(TestBidDenom, TestBidAmount)) + require.Equal(t, collateralAuction.LotReturns, weightedAddresses) + require.Equal(t, collateralAuction.CorrespondingDebt, c(TestDebtDenom, TestDebtAmount2)) +} diff --git a/x/auction/types/keys.go b/x/auction/types/keys.go index aca635a9..03c8cc29 100644 --- a/x/auction/types/keys.go +++ b/x/auction/types/keys.go @@ -19,6 +19,8 @@ const ( // DefaultParamspace default name for parameter store DefaultParamspace = ModuleName + + QuerierRoute = ModuleName ) var ( diff --git a/x/auction/types/querier.go b/x/auction/types/querier.go index 571c3908..3b373317 100644 --- a/x/auction/types/querier.go +++ b/x/auction/types/querier.go @@ -16,3 +16,17 @@ type QueryResAuctions []string func (n QueryResAuctions) String() string { return strings.Join(n[:], "\n") } + +// QueryAllAuctionParams is the params for an auctions query +type QueryAllAuctionParams struct { + Page int `json"page:" yaml:"page"` + Limit int `json"limit:" yaml:"limit"` +} + +// NewQueryAllAuctionParams creates a new QueryAllAuctionParams +func NewQueryAllAuctionParams(page int, limit int) QueryAllAuctionParams { + return QueryAllAuctionParams{ + Page: page, + Limit: limit, + } +} From ba80b508ab1f5bc58a50e1b26c4036f92efd390b Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Wed, 15 Jan 2020 11:39:55 +0100 Subject: [PATCH 54/99] [R4R] Custom sdk.Error types for the auction module (#285) * Custom sdk.Error types for the auction module * Requested changes: naming conventions, error message text --- x/auction/alias.go | 27 +++++++--- x/auction/handler.go | 3 +- x/auction/keeper/auctions.go | 36 ++++++------- x/auction/keeper/bidding_test.go | 48 ++++++++--------- x/auction/keeper/keeper.go | 3 +- x/auction/keeper/querier.go | 2 +- x/auction/types/errors.go | 92 ++++++++++++++++++++++++++++++++ x/auction/types/msg.go | 2 +- 8 files changed, 159 insertions(+), 54 deletions(-) create mode 100644 x/auction/types/errors.go diff --git a/x/auction/alias.go b/x/auction/alias.go index 7b8ece33..dc79f6fa 100644 --- a/x/auction/alias.go +++ b/x/auction/alias.go @@ -11,13 +11,26 @@ import ( ) const ( - ModuleName = types.ModuleName - StoreKey = types.StoreKey - RouterKey = types.RouterKey - DefaultParamspace = types.DefaultParamspace - DefaultMaxAuctionDuration = types.DefaultMaxAuctionDuration - DefaultBidDuration = types.DefaultBidDuration - QueryGetAuction = types.QueryGetAuction + DefaultCodespace = types.DefaultCodespace + CodeInvalidInitialAuctionID = types.CodeInvalidInitialAuctionID + CodeUnrecognizedAuctionType = types.CodeUnrecognizedAuctionType + CodeAuctionNotFound = types.CodeAuctionNotFound + CodeAuctionHasNotExpired = types.CodeAuctionHasNotExpired + CodeAuctionHasExpired = types.CodeAuctionHasExpired + CodeInvalidBidDenom = types.CodeInvalidBidDenom + CodeInvalidLotDenom = types.CodeInvalidLotDenom + CodeBidTooSmall = types.CodeBidTooSmall + CodeBidTooLarge = types.CodeBidTooLarge + CodeLotTooLarge = types.CodeLotTooLarge + CodeCollateralAuctionIsInReversePhase = types.CodeCollateralAuctionIsInReversePhase + CodeCollateralAuctionIsInForwardPhase = types.CodeCollateralAuctionIsInForwardPhase + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey + DefaultParamspace = types.DefaultParamspace + DefaultMaxAuctionDuration = types.DefaultMaxAuctionDuration + DefaultBidDuration = types.DefaultBidDuration + QueryGetAuction = types.QueryGetAuction ) var ( diff --git a/x/auction/handler.go b/x/auction/handler.go index be5f9d30..2ac42e36 100644 --- a/x/auction/handler.go +++ b/x/auction/handler.go @@ -17,8 +17,7 @@ func NewHandler(keeper Keeper) sdk.Handler { case MsgPlaceBid: return handleMsgPlaceBid(ctx, keeper, msg) default: - errMsg := fmt.Sprintf("Unrecognized auction msg type: %T", msg) - return sdk.ErrUnknownRequest(errMsg).Result() + return sdk.ErrUnknownRequest(fmt.Sprintf("Unrecognized auction msg type: %T", msg)).Result() } } } diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index e1465668..0c69c231 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -53,7 +53,7 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in // This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker. macc := k.supplyKeeper.GetModuleAccount(ctx, buyer) if !macc.HasPermission(supply.Minter) { - return 0, sdk.ErrInternal("module does not have minting permissions") + return 0, types.ErrInvalidModulePermissions(k.codespace, supply.Minter) } err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, buyer, types.ModuleName, sdk.NewCoins(debt)) @@ -124,12 +124,12 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddres auction, found := k.GetAuction(ctx, auctionID) if !found { - return sdk.ErrInternal("auction doesn't exist") + return types.ErrAuctionNotFound(k.codespace, auctionID) } // validation common to all auctions if ctx.BlockTime().After(auction.GetEndTime()) { - return sdk.ErrInternal("auction has closed") + return types.ErrAuctionHasExpired(k.codespace, auctionID) } // move coins and return updated auction @@ -154,7 +154,7 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddres return err } default: - panic(fmt.Sprintf("unrecognized auction type: %T", auction)) + return types.ErrUnrecognizedAuctionType(k.codespace) } k.SetAuction(ctx, updatedAuction) @@ -166,10 +166,10 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddres func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.SurplusAuction, sdk.Error) { // Validate new bid if bid.Denom != a.Bid.Denom { - return a, sdk.ErrInternal("bid denom doesn't match auction") + return a, types.ErrInvalidBidDenom(k.codespace, bid.Denom, a.Bid.Denom) } if !a.Bid.IsLT(bid) { - return a, sdk.ErrInternal("bid not greater than last bid") + return a, types.ErrBidTooSmall(k.codespace, bid, a.Bid) } // New bidder pays back old bidder @@ -220,16 +220,16 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.CollateralAuction, sdk.Error) { // Validate new bid if bid.Denom != a.Bid.Denom { - return a, sdk.ErrInternal("bid denom doesn't match auction") + return a, types.ErrInvalidBidDenom(k.codespace, bid.Denom, a.Bid.Denom) } if a.IsReversePhase() { - return a, sdk.ErrInternal("auction is not in forward phase") + return a, types.ErrCollateralAuctionIsInReversePhase(k.codespace, a.ID) } if !a.Bid.IsLT(bid) { - return a, sdk.ErrInternal("auction in forward phase, new bid not higher than last bid") + return a, types.ErrBidTooSmall(k.codespace, bid, a.Bid) } if a.MaxBid.IsLT(bid) { - return a, sdk.ErrInternal("bid higher than max bid") + return a, types.ErrBidTooLarge(k.codespace, bid, a.MaxBid) } // New bidder pays back old bidder @@ -289,13 +289,13 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.CollateralAuction, sdk.Error) { // Validate new bid if lot.Denom != a.Lot.Denom { - return a, sdk.ErrInternal("lot denom doesn't match auction") + return a, types.ErrInvalidLotDenom(k.codespace, lot.Denom, a.Lot.Denom) } if !a.IsReversePhase() { - return a, sdk.ErrInternal("auction not in reverse phase") + return a, types.ErrCollateralAuctionIsInForwardPhase(k.codespace, a.ID) } if !lot.IsLT(a.Lot) { - return a, sdk.ErrInternal("auction in reverse phase, new bid not less than previous amount") + return a, types.ErrLotTooLarge(k.codespace, lot, a.Lot) } // New bidder pays back old bidder @@ -349,10 +349,10 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.DebtAuction, sdk.Error) { // Validate new bid if lot.Denom != a.Lot.Denom { - return a, sdk.ErrInternal("lot denom doesn't match auction") + return a, types.ErrInvalidLotDenom(k.codespace, lot.Denom, a.Lot.Denom) } if !lot.IsLT(a.Lot) { - return a, sdk.ErrInternal("lot not smaller than last lot") + return a, types.ErrLotTooLarge(k.codespace, lot, a.Lot) } // New bidder pays back old bidder @@ -407,11 +407,11 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error { auction, found := k.GetAuction(ctx, auctionID) if !found { - return sdk.ErrInternal("auction doesn't exist") + return types.ErrAuctionNotFound(k.codespace, auctionID) } if ctx.BlockTime().Before(auction.GetEndTime()) { - return sdk.ErrInternal(fmt.Sprintf("auction can't be closed as curent block time (%v) is under auction end time (%v)", ctx.BlockTime(), auction.GetEndTime())) + return types.ErrAuctionHasNotExpired(k.codespace, ctx.BlockTime(), auction.GetEndTime()) } // payout to the last bidder @@ -429,7 +429,7 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error { return err } default: - panic("unrecognized auction type") + return types.ErrUnrecognizedAuctionType(k.codespace) } k.DeleteAuction(ctx, auctionID) diff --git a/x/auction/keeper/bidding_test.go b/x/auction/keeper/bidding_test.go index 7703f86e..56cea3f2 100644 --- a/x/auction/keeper/bidding_test.go +++ b/x/auction/keeper/bidding_test.go @@ -76,7 +76,7 @@ func TestAuctionBidding(t *testing.T) { name string auctionArgs auctionArgs bidArgs bidArgs - expectedError string + expectedError sdk.CodeType expectedEndTime time.Time expectedBidder sdk.AccAddress expectedBid sdk.Coin @@ -86,7 +86,7 @@ func TestAuctionBidding(t *testing.T) { "basic: auction doesn't exist", auctionArgs{Surplus, "", c("token1", 1), c("token2", 1), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, bidArgs{buyer, c("token2", 10), nil}, - "auction doesn't exist", + types.CodeAuctionNotFound, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 10), @@ -96,7 +96,7 @@ func TestAuctionBidding(t *testing.T) { "surplus: normal", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, bidArgs{buyer, c("token2", 10), nil}, - "", + sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), buyer, c("token2", 10), @@ -106,7 +106,7 @@ func TestAuctionBidding(t *testing.T) { "surplus: second bidder", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, bidArgs{buyer, c("token2", 10), secondBuyer}, - "", + sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), secondBuyer, c("token2", 11), @@ -116,7 +116,7 @@ func TestAuctionBidding(t *testing.T) { "surplus: invalid bid denom", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, bidArgs{buyer, c("badtoken", 10), nil}, - "bid denom doesn't match auction", + types.CodeInvalidBidDenom, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 10), @@ -126,7 +126,7 @@ func TestAuctionBidding(t *testing.T) { "surplus: invalid bid (equal)", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 0), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, bidArgs{buyer, c("token2", 0), nil}, - "bid not greater than last bid", + types.CodeBidTooSmall, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 10), @@ -136,7 +136,7 @@ func TestAuctionBidding(t *testing.T) { "debt: normal", auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot bidArgs{buyer, c("token1", 10), nil}, - "", + sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), buyer, c("token2", 100), @@ -146,7 +146,7 @@ func TestAuctionBidding(t *testing.T) { "debt: second bidder", auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot bidArgs{buyer, c("token1", 10), secondBuyer}, - "", + sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), secondBuyer, c("token2", 100), @@ -156,7 +156,7 @@ func TestAuctionBidding(t *testing.T) { "debt: invalid lot denom", auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot bidArgs{buyer, c("badtoken", 10), nil}, - "lot denom doesn't match auction", + types.CodeInvalidLotDenom, someTime.Add(types.DefaultBidDuration), buyer, c("token1", 20), @@ -166,7 +166,7 @@ func TestAuctionBidding(t *testing.T) { "debt: invalid lot size (larger)", auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, bidArgs{buyer, c("token1", 21), nil}, - "lot not smaller than last lot", + types.CodeLotTooLarge, someTime.Add(types.DefaultBidDuration), buyer, c("token1", 20), @@ -176,7 +176,7 @@ func TestAuctionBidding(t *testing.T) { "collateral [forward]: normal", auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid bidArgs{buyer, c("token2", 10), nil}, - "", + sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), buyer, c("token2", 10), @@ -186,7 +186,7 @@ func TestAuctionBidding(t *testing.T) { "collateral [forward]: second bidder", auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid bidArgs{buyer, c("token2", 10), secondBuyer}, - "", + sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), secondBuyer, c("token2", 11), @@ -196,7 +196,7 @@ func TestAuctionBidding(t *testing.T) { "collateral [forward]: invalid bid denom", auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid bidArgs{buyer, c("badtoken", 10), nil}, - "bid denom doesn't match auction", + types.CodeInvalidBidDenom, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 10), @@ -206,7 +206,7 @@ func TestAuctionBidding(t *testing.T) { "collateral [forward]: invalid bid size (smaller)", auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid bidArgs{buyer, c("token2", 0), nil}, // lot, bid - "auction in forward phase, new bid not higher than last bid", + types.CodeBidTooSmall, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 10), @@ -216,7 +216,7 @@ func TestAuctionBidding(t *testing.T) { "collateral [forward]: invalid bid size (greater than max)", auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid bidArgs{buyer, c("token2", 101), nil}, // lot, bid - "bid higher than max bid", + types.CodeBidTooLarge, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 10), @@ -226,7 +226,7 @@ func TestAuctionBidding(t *testing.T) { "collateral [reverse]: normal", auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid bidArgs{buyer, c("token1", 15), nil}, - "", + sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), buyer, c("token2", 50), @@ -236,7 +236,7 @@ func TestAuctionBidding(t *testing.T) { "collateral [reverse]: second bidder", auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid bidArgs{buyer, c("token1", 15), secondBuyer}, - "", + sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), secondBuyer, c("token2", 50), @@ -246,7 +246,7 @@ func TestAuctionBidding(t *testing.T) { "collateral [reverse]: invalid lot denom", auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid bidArgs{buyer, c("badtoken", 15), nil}, - "lot denom doesn't match auction", + types.CodeInvalidLotDenom, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 50), @@ -256,7 +256,7 @@ func TestAuctionBidding(t *testing.T) { "collateral [reverse]: invalid lot size (equal)", auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid bidArgs{buyer, c("token1", 20), nil}, - "auction in reverse phase, new bid not less than previous amount", + types.CodeLotTooLarge, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 50), @@ -266,7 +266,7 @@ func TestAuctionBidding(t *testing.T) { "collateral [reverse]: invalid lot size (greater)", auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid bidArgs{buyer, c("token1", 21), nil}, - "auction in reverse phase, new bid not less than previous amount", + types.CodeLotTooLarge, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 50), @@ -276,7 +276,7 @@ func TestAuctionBidding(t *testing.T) { "basic: closed auction", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, bidArgs{buyer, c("token2", 10), nil}, - "auction has closed", + types.CodeAuctionHasExpired, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 10), @@ -287,7 +287,7 @@ func TestAuctionBidding(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Start Auction var id uint64 - var err error + var err sdk.Error switch tc.auctionArgs.auctionType { case Surplus: id, _ = keeper.StartSurplusAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid.Denom) @@ -341,8 +341,8 @@ func TestAuctionBidding(t *testing.T) { require.Equal(t, tc.expectedBid, auction.GetBid()) require.Equal(t, tc.expectedEndTime, auction.GetEndTime()) } else { - // Check expected error message - require.Contains(t, err.Error(), tc.expectedError) + // Check expected error code type + require.Equal(t, tc.expectedError, err.Result().Code) } }) } diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index cd0b01c2..49229461 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -18,6 +18,7 @@ type Keeper struct { storeKey sdk.StoreKey cdc *codec.Codec paramSubspace subspace.Subspace + codespace sdk.CodespaceType } // NewKeeper returns a new auction keeper. @@ -46,7 +47,7 @@ func (k Keeper) GetNextAuctionID(ctx sdk.Context) (uint64, sdk.Error) { store := ctx.KVStore(k.storeKey) bz := store.Get(types.NextAuctionIDKey) if bz == nil { - return 0, sdk.ErrInternal("initial auction ID hasn't been set") + return 0, types.ErrInvalidInitialAuctionID(k.codespace) } return types.Uint64FromBytes(bz), nil } diff --git a/x/auction/keeper/querier.go b/x/auction/keeper/querier.go index 50afa363..9fe384c8 100644 --- a/x/auction/keeper/querier.go +++ b/x/auction/keeper/querier.go @@ -29,7 +29,7 @@ func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res [ bz, err2 := codec.MarshalJSONIndent(keeper.cdc, auctionsList) if err2 != nil { - panic("could not marshal result to JSON") + return nil, sdk.ErrInternal("could not marshal result to JSON") } return bz, nil diff --git a/x/auction/types/errors.go b/x/auction/types/errors.go new file mode 100644 index 00000000..f5b427aa --- /dev/null +++ b/x/auction/types/errors.go @@ -0,0 +1,92 @@ +// DONTCOVER +package types + +import ( + "fmt" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Error codes specific to auction module +const ( + DefaultCodespace sdk.CodespaceType = ModuleName + CodeInvalidInitialAuctionID sdk.CodeType = 1 + CodeInvalidModulePermissions sdk.CodeType = 2 + CodeUnrecognizedAuctionType sdk.CodeType = 3 + CodeAuctionNotFound sdk.CodeType = 4 + CodeAuctionHasNotExpired sdk.CodeType = 5 + CodeAuctionHasExpired sdk.CodeType = 6 + CodeInvalidBidDenom sdk.CodeType = 7 + CodeInvalidLotDenom sdk.CodeType = 8 + CodeBidTooSmall sdk.CodeType = 9 + CodeBidTooLarge sdk.CodeType = 10 + CodeLotTooLarge sdk.CodeType = 11 + CodeCollateralAuctionIsInReversePhase sdk.CodeType = 12 + CodeCollateralAuctionIsInForwardPhase sdk.CodeType = 13 +) + +// ErrInvalidInitialAuctionID error for when the initial auction ID hasn't been set +func ErrInvalidInitialAuctionID(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidInitialAuctionID, fmt.Sprintf("initial auction ID hasn't been set")) +} + +// ErrInvalidModulePermissions error for when module doesn't have valid permissions +func ErrInvalidModulePermissions(codespace sdk.CodespaceType, permission string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidModulePermissions, fmt.Sprintf("module does not have required permission '%s'", permission)) +} + +// ErrUnrecognizedAuctionType error for unrecognized auction type +func ErrUnrecognizedAuctionType(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeUnrecognizedAuctionType, fmt.Sprintf("unrecognized auction type")) +} + +// ErrAuctionNotFound error for when an auction is not found +func ErrAuctionNotFound(codespace sdk.CodespaceType, id uint64) sdk.Error { + return sdk.NewError(codespace, CodeAuctionNotFound, fmt.Sprintf("auction %d was not found", id)) +} + +// ErrAuctionHasNotExpired error for attempting to close an auction that has not passed its end time +func ErrAuctionHasNotExpired(codespace sdk.CodespaceType, blockTime time.Time, endTime time.Time) sdk.Error { + return sdk.NewError(codespace, CodeAuctionHasNotExpired, fmt.Sprintf("auction can't be closed as curent block time (%v) has not passed auction end time (%v)", blockTime, endTime)) +} + +// ErrAuctionHasExpired error for when an auction is closed and unavailable for bidding +func ErrAuctionHasExpired(codespace sdk.CodespaceType, id uint64) sdk.Error { + return sdk.NewError(codespace, CodeAuctionHasExpired, fmt.Sprintf("auction %d has closed", id)) +} + +// ErrInvalidBidDenom error for when bid denom doesn't match auction bid denom +func ErrInvalidBidDenom(codespace sdk.CodespaceType, bidDenom string, auctionBidDenom string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidBidDenom, fmt.Sprintf("bid denom %s doesn't match auction bid denom %s", bidDenom, auctionBidDenom)) +} + +// ErrInvalidLotDenom error for when lot denom doesn't match auction lot denom +func ErrInvalidLotDenom(codespace sdk.CodespaceType, lotDenom string, auctionLotDenom string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidLotDenom, fmt.Sprintf("lot denom %s doesn't match auction lot denom %s", lotDenom, auctionLotDenom)) +} + +// ErrBidTooSmall error for when bid is not greater than auction's last bid +func ErrBidTooSmall(codespace sdk.CodespaceType, bid sdk.Coin, lastBid sdk.Coin) sdk.Error { + return sdk.NewError(codespace, CodeBidTooSmall, fmt.Sprintf("bid %s is smaller than auction's last bid %s", bid.String(), lastBid.String())) +} + +// ErrBidTooLarge error for when bid is larger than auction's maximum allowed bid +func ErrBidTooLarge(codespace sdk.CodespaceType, bid sdk.Coin, maxBid sdk.Coin) sdk.Error { + return sdk.NewError(codespace, CodeBidTooLarge, fmt.Sprintf("bid %s is greater than auction's max bid %s", bid.String(), maxBid.String())) +} + +// ErrLotTooLarge error for when lot is not smaller than auction's last lot +func ErrLotTooLarge(codespace sdk.CodespaceType, lot sdk.Coin, lastLot sdk.Coin) sdk.Error { + return sdk.NewError(codespace, CodeLotTooLarge, fmt.Sprintf("lot %s is not less than auction's last lot %s", lot.String(), lastLot.String())) +} + +// ErrCollateralAuctionIsInReversePhase error for when attempting to place a forward bid on a collateral auction in reverse phase +func ErrCollateralAuctionIsInReversePhase(codespace sdk.CodespaceType, id uint64) sdk.Error { + return sdk.NewError(codespace, CodeCollateralAuctionIsInReversePhase, fmt.Sprintf("invalid bid - auction %d is in reverse phase", id)) +} + +// ErrCollateralAuctionIsInForwardPhase error for when attempting to place a reverse bid on a collateral auction in forward phase +func ErrCollateralAuctionIsInForwardPhase(codespace sdk.CodespaceType, id uint64) sdk.Error { + return sdk.NewError(codespace, CodeCollateralAuctionIsInForwardPhase, fmt.Sprintf("invalid bid - auction %d is in forward phase", id)) +} diff --git a/x/auction/types/msg.go b/x/auction/types/msg.go index 976aedb1..ed18d84b 100644 --- a/x/auction/types/msg.go +++ b/x/auction/types/msg.go @@ -33,7 +33,7 @@ func (msg MsgPlaceBid) ValidateBasic() sdk.Error { return sdk.ErrInternal("invalid (empty) bidder address") } if !msg.Amount.IsValid() { - return sdk.ErrInternal("invalid bid amount") + return sdk.ErrInvalidCoins(msg.Amount.String()) } return nil } From 9b1bf55be78bd8005b47c55bc6ad5e39719ecdac Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Wed, 15 Jan 2020 15:19:33 +0100 Subject: [PATCH 55/99] R4R: Move liquidator functions to cdp module (#280) * wip: tpyes and keeper methods * wip: iterators * wip: types and keeper methods * wip: add msgs * wip: client methods * wip: rebase develop * wip: types tests * wip: keeper tests, small fixes * wip: add cdp tests * wip: deposit tests * wip: keeper tests * wip: tests and module methods * feat: error when fetching expired price * feat: conversion factor for external assets * feat: debt floor for new cdps * feat: save deposits on export genesis * feat: ensure messages implement msg * feat: index deposits by status * fix: stray comment * wip: address review comments * address review comments * wip: move liquidation to cdp module * wip: handle liquidations directly * wip: use new auction interface * feat: auction collateral in cdp begin block * feat: update param validation * feat: surplus and debt auctions * address review comments * address review comments * fix: auction multiple deposits * clean up netting function --- .gitignore | 3 + app/app.go | 15 +- app/test_common.go | 28 ++-- x/auction/keeper/keeper.go | 3 + x/auction/types/expected_keepers.go | 1 + x/cdp/abci.go | 26 +++- x/cdp/abci_test.go | 3 +- x/cdp/alias.go | 14 +- x/cdp/client/cli/query.go | 7 +- x/cdp/genesis.go | 1 + x/cdp/integration_test.go | 75 ++++++--- x/cdp/keeper/auctions.go | 231 ++++++++++++++++++++++++++++ x/cdp/keeper/auctions_test.go | 76 +++++++++ x/cdp/keeper/cdp.go | 18 +++ x/cdp/keeper/deposit.go | 79 ++-------- x/cdp/keeper/deposit_test.go | 46 +----- x/cdp/keeper/draw.go | 14 +- x/cdp/keeper/integration_test.go | 58 ++++--- x/cdp/keeper/keeper.go | 11 +- x/cdp/keeper/params.go | 19 ++- x/cdp/keeper/seize.go | 82 +++++----- x/cdp/keeper/seize_test.go | 42 ++++- x/cdp/types/deposit.go | 53 ++----- x/cdp/types/events.go | 16 +- x/cdp/types/expected_keepers.go | 7 + x/cdp/types/genesis.go | 2 + x/cdp/types/keys.go | 32 ++-- x/cdp/types/keys_test.go | 12 +- x/cdp/types/params.go | 68 +++++--- 29 files changed, 706 insertions(+), 336 deletions(-) create mode 100644 x/cdp/keeper/auctions.go create mode 100644 x/cdp/keeper/auctions_test.go diff --git a/.gitignore b/.gitignore index ae38c8a5..648f2b9b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ # Exclude build files vendor build + +# IDE files +*.vscode \ No newline at end of file diff --git a/app/app.go b/app/app.go index 5ad36569..70ea5031 100644 --- a/app/app.go +++ b/app/app.go @@ -229,18 +229,19 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, pricefeedSubspace, pricefeed.DefaultCodespace) // NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, sk types.SupplyKeeper, codespace sdk.CodespaceType) - app.cdpKeeper = cdp.NewKeeper( - app.cdc, - keys[cdp.StoreKey], - cdpSubspace, - app.pricefeedKeeper, - app.supplyKeeper, - cdp.DefaultCodespace) app.auctionKeeper = auction.NewKeeper( app.cdc, keys[auction.StoreKey], app.supplyKeeper, auctionSubspace) + app.cdpKeeper = cdp.NewKeeper( + app.cdc, + keys[cdp.StoreKey], + cdpSubspace, + app.pricefeedKeeper, + app.auctionKeeper, + app.supplyKeeper, + cdp.DefaultCodespace) // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks diff --git a/app/test_common.go b/app/test_common.go index 2b598bb5..7bb0da05 100644 --- a/app/test_common.go +++ b/app/test_common.go @@ -53,20 +53,20 @@ func NewTestApp() TestApp { return TestApp{App: *app} } -func (tApp TestApp) GetAccountKeeper() auth.AccountKeeper { return tApp.accountKeeper } -func (tApp TestApp) GetBankKeeper() bank.Keeper { return tApp.bankKeeper } -func (tApp TestApp) GetSupplyKeeper() supply.Keeper { return tApp.supplyKeeper } -func (tApp TestApp) GetStakingKeeper() staking.Keeper { return tApp.stakingKeeper } -func (tApp TestApp) GetSlashingKeeper() slashing.Keeper { return tApp.slashingKeeper } -func (tApp TestApp) GetMintKeeper() mint.Keeper { return tApp.mintKeeper } -func (tApp TestApp) GetDistrKeeper() distribution.Keeper { return tApp.distrKeeper } -func (tApp TestApp) GetGovKeeper() gov.Keeper { return tApp.govKeeper } -func (tApp TestApp) GetCrisisKeeper() crisis.Keeper { return tApp.crisisKeeper } -func (tApp TestApp) GetParamsKeeper() params.Keeper { return tApp.paramsKeeper } -func (tApp TestApp) GetVVKeeper() validatorvesting.Keeper { return tApp.vvKeeper } -func (tApp TestApp) GetAuctionKeeper() auction.Keeper { return tApp.auctionKeeper } -func (tApp TestApp) GetCDPKeeper() cdp.Keeper { return tApp.cdpKeeper } -func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefeedKeeper } +func (tApp TestApp) GetAccountKeeper() auth.AccountKeeper { return tApp.accountKeeper } +func (tApp TestApp) GetBankKeeper() bank.Keeper { return tApp.bankKeeper } +func (tApp TestApp) GetSupplyKeeper() supply.Keeper { return tApp.supplyKeeper } +func (tApp TestApp) GetStakingKeeper() staking.Keeper { return tApp.stakingKeeper } +func (tApp TestApp) GetSlashingKeeper() slashing.Keeper { return tApp.slashingKeeper } +func (tApp TestApp) GetMintKeeper() mint.Keeper { return tApp.mintKeeper } +func (tApp TestApp) GetDistrKeeper() distribution.Keeper { return tApp.distrKeeper } +func (tApp TestApp) GetGovKeeper() gov.Keeper { return tApp.govKeeper } +func (tApp TestApp) GetCrisisKeeper() crisis.Keeper { return tApp.crisisKeeper } +func (tApp TestApp) GetParamsKeeper() params.Keeper { return tApp.paramsKeeper } +func (tApp TestApp) GetVVKeeper() validatorvesting.Keeper { return tApp.vvKeeper } +func (tApp TestApp) GetAuctionKeeper() auction.Keeper { return tApp.auctionKeeper } +func (tApp TestApp) GetCDPKeeper() cdp.Keeper { return tApp.cdpKeeper } +func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefeedKeeper } // This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp { diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index 49229461..7a51302b 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -23,6 +23,9 @@ type Keeper struct { // NewKeeper returns a new auction keeper. func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, supplyKeeper types.SupplyKeeper, paramstore subspace.Subspace) Keeper { + if addr := supplyKeeper.GetModuleAddress(types.ModuleName); addr == nil { + panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) + } return Keeper{ supplyKeeper: supplyKeeper, storeKey: storeKey, diff --git a/x/auction/types/expected_keepers.go b/x/auction/types/expected_keepers.go index 021b8918..83141c9e 100644 --- a/x/auction/types/expected_keepers.go +++ b/x/auction/types/expected_keepers.go @@ -7,6 +7,7 @@ import ( // SupplyKeeper defines the expected supply Keeper type SupplyKeeper interface { + GetModuleAddress(name string) sdk.AccAddress GetModuleAccount(ctx sdk.Context, moduleName string) supplyexported.ModuleAccountI SendCoinsFromModuleToModule(ctx sdk.Context, sender, recipient string, amt sdk.Coins) sdk.Error diff --git a/x/cdp/abci.go b/x/cdp/abci.go index 5c3e1bb9..cc36bd74 100644 --- a/x/cdp/abci.go +++ b/x/cdp/abci.go @@ -1,11 +1,14 @@ package cdp import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/cdp/types" abci "github.com/tendermint/tendermint/abci/types" ) -// BeginBlock compounds the debt in outstanding cdps and liquidates cdps that are below the required collateralization ratio +// BeginBlocker compounds the debt in outstanding cdps and liquidates cdps that are below the required collateralization ratio func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) { params := k.GetParams(ctx) previousBlockTime, found := k.GetPreviousBlockTime(ctx) @@ -18,7 +21,26 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) { k.HandleNewDebt(ctx, cp.Denom, dp.Denom, timeElapsed) } - k.LiquidateCdps(ctx, cp.MarketID, cp.Denom, cp.LiquidationRatio) + err := k.LiquidateCdps(ctx, cp.MarketID, cp.Denom, cp.LiquidationRatio) + if err != nil { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + EventTypeBeginBlockerFatal, + sdk.NewAttribute(sdk.AttributeKeyModule, fmt.Sprintf("%s", ModuleName)), + sdk.NewAttribute(types.AttributeKeyError, fmt.Sprintf("%s", err)), + ), + ) + } + } + err := k.RunSurplusAndDebtAuctions(ctx) + if err != nil { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + EventTypeBeginBlockerFatal, + sdk.NewAttribute(sdk.AttributeKeyModule, fmt.Sprintf("%s", ModuleName)), + sdk.NewAttribute(types.AttributeKeyError, fmt.Sprintf("%s", err)), + ), + ) } k.SetPreviousBlockTime(ctx, ctx.BlockTime()) return diff --git a/x/cdp/abci_test.go b/x/cdp/abci_test.go index f9249856..e8c9573f 100644 --- a/x/cdp/abci_test.go +++ b/x/cdp/abci_test.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/simulation" "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/cdp" "github.com/stretchr/testify/suite" abci "github.com/tendermint/tendermint/abci/types" @@ -116,7 +117,7 @@ func (suite *ModuleTestSuite) TestBeginBlock() { btcLiquidations := int(seizedBtcCollateral.Quo(i(100000000)).Int64()) suite.Equal(len(suite.liquidations.btc), btcLiquidations) - acc = sk.GetModuleAccount(suite.ctx, cdp.LiquidatorMacc) + acc = sk.GetModuleAccount(suite.ctx, auction.ModuleName) suite.Equal(suite.liquidations.debt, acc.GetCoins().AmountOf("debt").Int64()) } diff --git a/x/cdp/alias.go b/x/cdp/alias.go index 19618d6a..951dff00 100644 --- a/x/cdp/alias.go +++ b/x/cdp/alias.go @@ -11,8 +11,6 @@ import ( ) const ( - StatusNil = types.StatusNil - StatusLiquidated = types.StatusLiquidated DefaultCodespace = types.DefaultCodespace CodeCdpAlreadyExists = types.CodeCdpAlreadyExists CodeCollateralLengthInvalid = types.CodeCollateralLengthInvalid @@ -36,15 +34,17 @@ const ( EventTypeCdpClose = types.EventTypeCdpClose EventTypeCdpWithdrawal = types.EventTypeCdpWithdrawal EventTypeCdpLiquidation = types.EventTypeCdpLiquidation + EventTypeBeginBlockerFatal = types.EventTypeBeginBlockerFatal AttributeKeyCdpID = types.AttributeKeyCdpID AttributeKeyDepositor = types.AttributeKeyDepositor AttributeValueCategory = types.AttributeValueCategory - LiquidatorMacc = types.LiquidatorMacc + AttributeKeyError = types.AttributeKeyError ModuleName = types.ModuleName StoreKey = types.StoreKey RouterKey = types.RouterKey QuerierRoute = types.QuerierRoute DefaultParamspace = types.DefaultParamspace + LiquidatorMacc = types.LiquidatorMacc QueryGetCdp = types.QueryGetCdp QueryGetCdps = types.QueryGetCdps QueryGetCdpsByCollateralization = types.QueryGetCdpsByCollateralization @@ -58,7 +58,6 @@ var ( // functions aliases NewCDP = types.NewCDP RegisterCodec = types.RegisterCodec - StatusFromByte = types.StatusFromByte NewDeposit = types.NewDeposit ErrCdpAlreadyExists = types.ErrCdpAlreadyExists ErrInvalidCollateralLength = types.ErrInvalidCollateralLength @@ -116,6 +115,7 @@ var ( CollateralRatioIndexPrefix = types.CollateralRatioIndexPrefix CdpIDKey = types.CdpIDKey DebtDenomKey = types.DebtDenomKey + GovDenomKey = types.GovDenomKey DepositKeyPrefix = types.DepositKeyPrefix PrincipalKeyPrefix = types.PrincipalKeyPrefix AccumulatorKeyPrefix = types.AccumulatorKeyPrefix @@ -124,12 +124,17 @@ var ( KeyCollateralParams = types.KeyCollateralParams KeyDebtParams = types.KeyDebtParams KeyCircuitBreaker = types.KeyCircuitBreaker + KeyDebtThreshold = types.KeyDebtThreshold + KeySurplusThreshold = types.KeySurplusThreshold DefaultGlobalDebt = types.DefaultGlobalDebt DefaultCircuitBreaker = types.DefaultCircuitBreaker DefaultCollateralParams = types.DefaultCollateralParams DefaultDebtParams = types.DefaultDebtParams DefaultCdpStartingID = types.DefaultCdpStartingID DefaultDebtDenom = types.DefaultDebtDenom + DefaultGovDenom = types.DefaultGovDenom + DefaultSurplusThreshold = types.DefaultSurplusThreshold + DefaultDebtThreshold = types.DefaultDebtThreshold DefaultPreviousBlockTime = types.DefaultPreviousBlockTime MaxSortableDec = types.MaxSortableDec ) @@ -138,7 +143,6 @@ type ( CDP = types.CDP CDPs = types.CDPs Deposit = types.Deposit - DepositStatus = types.DepositStatus Deposits = types.Deposits SupplyKeeper = types.SupplyKeeper PricefeedKeeper = types.PricefeedKeeper diff --git a/x/cdp/client/cli/query.go b/x/cdp/client/cli/query.go index eedd74bc..45b768ec 100644 --- a/x/cdp/client/cli/query.go +++ b/x/cdp/client/cli/query.go @@ -47,8 +47,9 @@ func QueryCdpCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { return err } collateralType := args[1] // TODO validation? - bz, err := cdc.MarshalJSON(types.QueryCdpsParams{ + bz, err := cdc.MarshalJSON(types.QueryCdpParams{ CollateralDenom: collateralType, + Owner: ownerAddress, }) if err != nil { return err @@ -58,8 +59,6 @@ func QueryCdpCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdp) 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 } @@ -78,7 +77,7 @@ func QueryCdpsByDenomCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { Short: "Query cdps by collateral type", Long: strings.TrimSpace(`Query cdps by a specific collateral type, or query all cdps if none is specifed: -$ query cdp cdps atom +$ query cdp cdps uatom `), Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/x/cdp/genesis.go b/x/cdp/genesis.go index 10de0f86..ba6fa416 100644 --- a/x/cdp/genesis.go +++ b/x/cdp/genesis.go @@ -51,6 +51,7 @@ func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, gs GenesisState) k.SetNextCdpID(ctx, gs.StartingCdpID) k.SetDebtDenom(ctx, gs.DebtDenom) + k.SetGovDenom(ctx, gs.GovDenom) for _, d := range gs.Deposits { k.SetDeposit(ctx, d) diff --git a/x/cdp/integration_test.go b/x/cdp/integration_test.go index 0a6eafcd..7cd40fcf 100644 --- a/x/cdp/integration_test.go +++ b/x/cdp/integration_test.go @@ -39,16 +39,20 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { cdpGenesis := cdp.GenesisState{ Params: cdp.Params{ - GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, CollateralParams: cdp.CollateralParams{ { - Denom: asset, - LiquidationRatio: liquidationRatio, - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), - StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr - Prefix: 0x20, - ConversionFactor: i(6), - MarketID: asset + ":usd", + Denom: asset, + LiquidationRatio: liquidationRatio, + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr + LiquidationPenalty: d("0.05"), + AuctionSize: i(1000000000), + Prefix: 0x20, + ConversionFactor: i(6), + MarketID: asset + ":usd", }, }, DebtParams: cdp.DebtParams{ @@ -63,6 +67,7 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { }, StartingCdpID: cdp.DefaultCdpStartingID, DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, CDPs: cdp.CDPs{}, PreviousBlockTime: cdp.DefaultPreviousBlockTime, } @@ -97,25 +102,31 @@ func NewPricefeedGenStateMulti() app.GenesisState { func NewCDPGenStateMulti() app.GenesisState { cdpGenesis := cdp.GenesisState{ Params: cdp.Params{ - GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, CollateralParams: cdp.CollateralParams{ { - Denom: "xrp", - LiquidationRatio: sdk.MustNewDecFromStr("2.0"), - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), - StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr - Prefix: 0x20, - MarketID: "xrp:usd", - ConversionFactor: i(6), + Denom: "xrp", + LiquidationRatio: sdk.MustNewDecFromStr("2.0"), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr + LiquidationPenalty: d("0.05"), + AuctionSize: i(7000000000), + Prefix: 0x20, + MarketID: "xrp:usd", + ConversionFactor: i(6), }, { - Denom: "btc", - LiquidationRatio: sdk.MustNewDecFromStr("1.5"), - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), - StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr - Prefix: 0x21, - MarketID: "btc:usd", - ConversionFactor: i(8), + Denom: "btc", + LiquidationRatio: sdk.MustNewDecFromStr("1.5"), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr + LiquidationPenalty: d("0.025"), + AuctionSize: i(10000000), + Prefix: 0x21, + MarketID: "btc:usd", + ConversionFactor: i(8), }, }, DebtParams: cdp.DebtParams{ @@ -137,6 +148,7 @@ func NewCDPGenStateMulti() app.GenesisState { }, StartingCdpID: cdp.DefaultCdpStartingID, DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, CDPs: cdp.CDPs{}, PreviousBlockTime: cdp.DefaultPreviousBlockTime, } @@ -193,6 +205,15 @@ func badGenStates() []badGenState { g10 := baseGenState() g10.PreviousBlockTime = time.Time{} + g11 := baseGenState() + g11.Params.CollateralParams[0].AuctionSize = i(-10) + + g12 := baseGenState() + g12.Params.CollateralParams[0].LiquidationPenalty = d("5.0") + + g13 := baseGenState() + g13.GovDenom = "" + return []badGenState{ badGenState{Genesis: g1, Reason: "duplicate collateral denom"}, badGenState{Genesis: g2, Reason: "duplicate collateral prefix"}, @@ -204,13 +225,18 @@ func badGenStates() []badGenState { badGenState{Genesis: g8, Reason: "debt param not found in global debt limit"}, badGenState{Genesis: g9, Reason: "debt denom not set"}, badGenState{Genesis: g10, Reason: "previous block time not set"}, + badGenState{Genesis: g11, Reason: "negative auction size"}, + badGenState{Genesis: g12, Reason: "invalid liquidation penalty"}, + badGenState{Genesis: g13, Reason: "gov denom not set"}, } } func baseGenState() cdp.GenesisState { return cdp.GenesisState{ Params: cdp.Params{ - GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, CollateralParams: cdp.CollateralParams{ { Denom: "xrp", @@ -250,6 +276,7 @@ func baseGenState() cdp.GenesisState { }, StartingCdpID: cdp.DefaultCdpStartingID, DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, CDPs: cdp.CDPs{}, PreviousBlockTime: cdp.DefaultPreviousBlockTime, } diff --git a/x/cdp/keeper/auctions.go b/x/cdp/keeper/auctions.go new file mode 100644 index 00000000..7c8397b9 --- /dev/null +++ b/x/cdp/keeper/auctions.go @@ -0,0 +1,231 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/x/cdp/types" +) + +const ( + // factor for setting the initial value of gov tokens to sell at debt auctions -- assuming stable token is ~1 usd, this starts the auction with a price of $0.01 KAVA + dump = 100 +) + +type partialDeposit struct { + Depositor sdk.AccAddress + Amount sdk.Coins + DebtShare sdk.Int +} + +func newPartialDeposit(depositor sdk.AccAddress, amount sdk.Coins, ds sdk.Int) partialDeposit { + return partialDeposit{ + Depositor: depositor, + Amount: amount, + DebtShare: ds, + } +} + +type partialDeposits []partialDeposit + +func (pd partialDeposits) SumCollateral() (sum sdk.Int) { + sum = sdk.ZeroInt() + for _, d := range pd { + sum = sum.Add(d.Amount[0].Amount) + } + return +} + +func (pd partialDeposits) SumDebt() (sum sdk.Int) { + sum = sdk.ZeroInt() + for _, d := range pd { + sum = sum.Add(d.DebtShare) + } + return +} + +// AuctionCollateral creates auctions from the input deposits which attempt to raise the corresponding amount of debt +func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt sdk.Int, bidDenom string) sdk.Error { + auctionSize := k.getAuctionSize(ctx, deposits[0].Amount[0].Denom) + partialAuctionDeposits := partialDeposits{} + totalCollateral := deposits.SumCollateral() + for totalCollateral.GT(sdk.ZeroInt()) { + for i, dep := range deposits { + if dep.Amount.IsZero() { + continue + } + collateralAmount := dep.Amount[0].Amount + collateralDenom := dep.Amount[0].Denom + // create auctions from individual deposits that are larger than the auction size + debtChange, collateralChange, err := k.CreateAuctionsFromDeposit(ctx, dep, debt, totalCollateral, auctionSize, bidDenom) + if err != nil { + return err + } + debt = debt.Sub(debtChange) + totalCollateral = totalCollateral.Sub(collateralChange) + dep.Amount = sdk.NewCoins(sdk.NewCoin(collateralDenom, collateralAmount.Sub(collateralChange))) + collateralAmount = collateralAmount.Sub(collateralChange) + // if there is leftover collateral that is less than a lot + if !dep.Amount.IsZero() { + // figure out how much debt this deposit accounts for + // (depositCollateral / totalCollateral) * totalDebtFromCDP + debtCoveredByDeposit := (collateralAmount.Quo(totalCollateral)).Mul(debt) + // if adding this deposit to the other partial deposits is less than a lot + if (partialAuctionDeposits.SumCollateral().Add(collateralAmount)).LT(auctionSize) { + // append the deposit to the partial deposits and zero out the deposit + pd := newPartialDeposit(dep.Depositor, dep.Amount, debtCoveredByDeposit) + partialAuctionDeposits = append(partialAuctionDeposits, pd) + dep.Amount = sdk.NewCoins(sdk.NewCoin(collateralDenom, sdk.ZeroInt())) + } else { + // if the sum of partial deposits now makes a lot + partialCollateral := sdk.NewCoins(sdk.NewCoin(collateralDenom, auctionSize.Sub(partialAuctionDeposits.SumCollateral()))) + partialAmount := partialCollateral[0].Amount + partialDebt := (partialAmount.Quo(collateralAmount)).Mul(debtCoveredByDeposit) + + // create a partial deposit from the deposit + partialDep := newPartialDeposit(dep.Depositor, partialCollateral, partialDebt) + // append it to the partial deposits + partialAuctionDeposits = append(partialAuctionDeposits, partialDep) + // create an auction from the partial deposits + debtChange, collateralChange, err := k.CreateAuctionFromPartialDeposits(ctx, partialAuctionDeposits, debt, totalCollateral, auctionSize, bidDenom) + if err != nil { + return err + } + debt = debt.Sub(debtChange) + totalCollateral = totalCollateral.Sub(collateralChange) + // reset partial deposits and update the deposit amount + partialAuctionDeposits = partialDeposits{} + dep.Amount = sdk.NewCoins(sdk.NewCoin(collateralDenom, collateralAmount.Sub(partialAmount))) + } + } + deposits[i] = dep + totalCollateral = deposits.SumCollateral() + } + } + if partialAuctionDeposits.SumCollateral().GT(sdk.ZeroInt()) { + _, _, err := k.CreateAuctionFromPartialDeposits(ctx, partialAuctionDeposits, debt, totalCollateral, partialAuctionDeposits.SumCollateral(), bidDenom) + if err != nil { + return err + } + } + return nil +} + +// CreateAuctionsFromDeposit creates auctions from the input deposit until there is less than auctionSize left on the deposit +func (k Keeper) CreateAuctionsFromDeposit(ctx sdk.Context, dep types.Deposit, debt sdk.Int, totalCollateral sdk.Int, auctionSize sdk.Int, principalDenom string) (debtChange sdk.Int, collateralChange sdk.Int, err sdk.Error) { + debtChange = sdk.ZeroInt() + collateralChange = sdk.ZeroInt() + depositAmount := dep.Amount[0].Amount + depositDenom := dep.Amount[0].Denom + for depositAmount.GTE(auctionSize) { + // figure out how much debt is covered by one lots worth of collateral + depositDebtAmount := (sdk.NewDecFromInt(auctionSize).Quo(sdk.NewDecFromInt(totalCollateral))).Mul(sdk.NewDecFromInt(debt)).RoundInt() + // start an auction for one lot, attempting to raise depositDebtAmount + _, err := k.auctionKeeper.StartCollateralAuction( + ctx, types.LiquidatorMacc, sdk.NewCoin(depositDenom, auctionSize), sdk.NewCoin(principalDenom, depositDebtAmount), []sdk.AccAddress{dep.Depositor}, + []sdk.Int{auctionSize}, sdk.NewCoin(k.GetDebtDenom(ctx), depositDebtAmount)) + if err != nil { + return sdk.ZeroInt(), sdk.ZeroInt(), err + } + depositAmount = depositAmount.Sub(auctionSize) + totalCollateral = totalCollateral.Sub(auctionSize) + debt = debt.Sub(depositDebtAmount) + // subtract one lot's worth of debt from the total debt covered by this deposit + debtChange = debtChange.Add(depositDebtAmount) + collateralChange = collateralChange.Add(auctionSize) + + } + return debtChange, collateralChange, nil +} + +// CreateAuctionFromPartialDeposits creates an auction from the input partial deposits +func (k Keeper) CreateAuctionFromPartialDeposits(ctx sdk.Context, partialDeps partialDeposits, debt sdk.Int, collateral sdk.Int, auctionSize sdk.Int, bidDenom string) (debtChange, collateralChange sdk.Int, err sdk.Error) { + + returnAddrs := []sdk.AccAddress{} + returnWeights := []sdk.Int{} + for _, pd := range partialDeps { + returnAddrs = append(returnAddrs, pd.Depositor) + returnWeights = append(returnWeights, pd.DebtShare) + } + _, err = k.auctionKeeper.StartCollateralAuction(ctx, types.LiquidatorMacc, sdk.NewCoin(partialDeps[0].Amount[0].Denom, auctionSize), sdk.NewCoin(bidDenom, partialDeps.SumDebt()), returnAddrs, returnWeights, sdk.NewCoin(k.GetDebtDenom(ctx), partialDeps.SumDebt())) + if err != nil { + return sdk.ZeroInt(), sdk.ZeroInt(), err + } + debtChange = partialDeps.SumDebt() + collateralChange = partialDeps.SumCollateral() + return debtChange, collateralChange, nil +} + +// NetSurplusAndDebt burns surplus and debt coins equal to the minimum of surplus and debt balances held by the liquidator module account +// for example, if there is 1000 debt and 100 surplus, 100 surplus and 100 debt are burned, netting to 900 debt +func (k Keeper) NetSurplusAndDebt(ctx sdk.Context) sdk.Error { + totalSurplus := k.GetTotalSurplus(ctx, types.LiquidatorMacc) + debt := k.GetTotalDebt(ctx, types.LiquidatorMacc) + netAmount := sdk.MinInt(totalSurplus, debt) + if netAmount.IsZero() { + return nil + } + err := k.supplyKeeper.BurnCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(k.GetDebtDenom(ctx), netAmount))) + if err != nil { + return err + } + for netAmount.GT(sdk.ZeroInt()) { + for _, dp := range k.GetParams(ctx).DebtParams { + balance := k.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc).GetCoins().AmountOf(dp.Denom) + if balance.LT(netAmount) { + err = k.supplyKeeper.BurnCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, balance))) + if err != nil { + return err + } + netAmount = netAmount.Sub(balance) + } else { + err = k.supplyKeeper.BurnCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, netAmount))) + if err != nil { + return err + } + netAmount = sdk.ZeroInt() + } + } + } + return nil +} + +// GetTotalSurplus returns the total amount of surplus tokens held by the liquidator module account +func (k Keeper) GetTotalSurplus(ctx sdk.Context, accountName string) sdk.Int { + acc := k.supplyKeeper.GetModuleAccount(ctx, accountName) + totalSurplus := sdk.ZeroInt() + for _, dp := range k.GetParams(ctx).DebtParams { + surplus := acc.GetCoins().AmountOf(dp.Denom) + totalSurplus = totalSurplus.Add(surplus) + } + return totalSurplus +} + +// GetTotalDebt returns the total amount of debt tokens held by the liquidator module account +func (k Keeper) GetTotalDebt(ctx sdk.Context, accountName string) sdk.Int { + acc := k.supplyKeeper.GetModuleAccount(ctx, accountName) + debt := acc.GetCoins().AmountOf(k.GetDebtDenom(ctx)) + return debt +} + +// RunSurplusAndDebtAuctions nets the surplus and debt balances and then creates surplus or debt auctions if the remaining balance is above the auction threshold parameter +func (k Keeper) RunSurplusAndDebtAuctions(ctx sdk.Context) sdk.Error { + k.NetSurplusAndDebt(ctx) + remainingDebt := k.GetTotalDebt(ctx, types.LiquidatorMacc) + params := k.GetParams(ctx) + if remainingDebt.GTE(params.DebtAuctionThreshold) { + _, err := k.auctionKeeper.StartDebtAuction(ctx, types.LiquidatorMacc, sdk.NewCoin("usdx", remainingDebt), sdk.NewCoin(k.GetGovDenom(ctx), remainingDebt.Mul(sdk.NewInt(dump))), sdk.NewCoin(k.GetDebtDenom(ctx), remainingDebt)) + if err != nil { + return err + } + } + remainingSurplus := k.GetTotalSurplus(ctx, types.LiquidatorMacc) + if remainingSurplus.GTE(params.SurplusAuctionThreshold) { + for _, dp := range params.DebtParams { + surplusLot := k.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc).GetCoins().AmountOf(dp.Denom) + _, err := k.auctionKeeper.StartSurplusAuction(ctx, types.LiquidatorMacc, sdk.NewCoin(dp.Denom, surplusLot), k.GetGovDenom(ctx)) + if err != nil { + return err + } + } + } + return nil +} diff --git a/x/cdp/keeper/auctions_test.go b/x/cdp/keeper/auctions_test.go new file mode 100644 index 00000000..bfa6b94a --- /dev/null +++ b/x/cdp/keeper/auctions_test.go @@ -0,0 +1,76 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/auction" + "github.com/kava-labs/kava/x/cdp/keeper" + "github.com/kava-labs/kava/x/cdp/types" + + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type AuctionTestSuite struct { + suite.Suite + + keeper keeper.Keeper + app app.TestApp + ctx sdk.Context +} + +func (suite *AuctionTestSuite) SetupTest() { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + tApp.InitializeFromGenesisStates( + NewPricefeedGenStateMulti(), + NewCDPGenStateMulti(), + ) + keeper := tApp.GetCDPKeeper() + suite.app = tApp + suite.ctx = ctx + suite.keeper = keeper + return +} + +func (suite *AuctionTestSuite) TestNetDebtSurplus() { + sk := suite.app.GetSupplyKeeper() + err := sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("debt", 100))) + suite.NoError(err) + err = sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("usdx", 10))) + suite.NoError(err) + suite.NotPanics(func() { suite.keeper.NetSurplusAndDebt(suite.ctx) }) + acc := sk.GetModuleAccount(suite.ctx, types.LiquidatorMacc) + suite.Equal(cs(c("debt", 90)), acc.GetCoins()) +} + +func (suite *AuctionTestSuite) TestSurplusAuction() { + sk := suite.app.GetSupplyKeeper() + err := sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("usdx", 10000))) + suite.NoError(err) + err = sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("debt", 1000))) + suite.NoError(err) + suite.keeper.RunSurplusAndDebtAuctions(suite.ctx) + acc := sk.GetModuleAccount(suite.ctx, auction.ModuleName) + suite.Equal(cs(c("usdx", 9000)), acc.GetCoins()) +} + +func (suite *AuctionTestSuite) TestDebtAuction() { + sk := suite.app.GetSupplyKeeper() + err := sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("usdx", 1000))) + suite.NoError(err) + err = sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("debt", 10000))) + suite.NoError(err) + suite.keeper.RunSurplusAndDebtAuctions(suite.ctx) + acc := sk.GetModuleAccount(suite.ctx, auction.ModuleName) + suite.Equal(cs(c("debt", 9000)), acc.GetCoins()) +} + +func TestAuctionTestSuite(t *testing.T) { + suite.Run(t, new(AuctionTestSuite)) +} diff --git a/x/cdp/keeper/cdp.go b/x/cdp/keeper/cdp.go index af4f4808..335cae6a 100644 --- a/x/cdp/keeper/cdp.go +++ b/x/cdp/keeper/cdp.go @@ -303,6 +303,14 @@ func (k Keeper) GetDebtDenom(ctx sdk.Context) (denom string) { return } +// GetGovDenom returns the denom of debt in the system +func (k Keeper) GetGovDenom(ctx sdk.Context) (denom string) { + store := prefix.NewStore(ctx.KVStore(k.key), types.DebtDenomKey) + bz := store.Get([]byte{}) + k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &denom) + return +} + // SetDebtDenom set the denom of debt in the system func (k Keeper) SetDebtDenom(ctx sdk.Context, denom string) { if denom == "" { @@ -313,6 +321,16 @@ func (k Keeper) SetDebtDenom(ctx sdk.Context, denom string) { return } +// SetGovDenom set the denom of the governance token in the system +func (k Keeper) SetGovDenom(ctx sdk.Context, denom string) { + if denom == "" { + panic("gov denom not set in genesis") + } + store := prefix.NewStore(ctx.KVStore(k.key), types.GovDenomKey) + store.Set([]byte{}, k.cdc.MustMarshalBinaryLengthPrefixed(denom)) + return +} + // ValidateCollateral validates that a collateral is valid for use in cdps func (k Keeper) ValidateCollateral(ctx sdk.Context, collateral sdk.Coins) sdk.Error { if len(collateral) != 1 { diff --git a/x/cdp/keeper/deposit.go b/x/cdp/keeper/deposit.go index 7bfa2293..35258c9b 100644 --- a/x/cdp/keeper/deposit.go +++ b/x/cdp/keeper/deposit.go @@ -18,13 +18,8 @@ func (k Keeper) DepositCollateral(ctx sdk.Context, owner sdk.AccAddress, deposit if !found { return types.ErrCdpNotFound(k.codespace, owner, collateral[0].Denom) } - // deposits blocked if cdp is in liquidation, have to check all deposits - err = k.ValidateAvailableCDP(ctx, cdp.ID) - if err != nil { - return err - } - deposit, found := k.GetDeposit(ctx, types.StatusNil, cdp.ID, depositor) + deposit, found := k.GetDeposit(ctx, cdp.ID, depositor) if found { deposit.Amount = deposit.Amount.Add(collateral) } else { @@ -67,12 +62,7 @@ func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner sdk.AccAddress, deposi if !found { return types.ErrCdpNotFound(k.codespace, owner, collateral[0].Denom) } - // withdrawals blocked if cdp is in liquidation - err = k.ValidateAvailableCDP(ctx, cdp.ID) - if err != nil { - return err - } - deposit, found := k.GetDeposit(ctx, types.StatusNil, cdp.ID, depositor) + deposit, found := k.GetDeposit(ctx, cdp.ID, depositor) if !found { return types.ErrDepositNotFound(k.codespace, depositor, cdp.ID) } @@ -113,28 +103,17 @@ func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner sdk.AccAddress, deposi deposit.Amount = deposit.Amount.Sub(collateral) if deposit.Amount.IsZero() { - k.DeleteDeposit(ctx, types.StatusNil, deposit.CdpID, deposit.Depositor) + k.DeleteDeposit(ctx, deposit.CdpID, deposit.Depositor) } else { k.SetDeposit(ctx, deposit) } return nil } -// ValidateAvailableCDP validates that the deposits of a cdp are not in liquidation -func (k Keeper) ValidateAvailableCDP(ctx sdk.Context, cdpID uint64) sdk.Error { - deposits := k.GetDeposits(ctx, cdpID) - for _, d := range deposits { - if d.InLiquidation { - return types.ErrCdpNotAvailable(k.codespace, cdpID) - } - } - return nil -} - // GetDeposit returns the deposit of a depositor on a particular cdp from the store -func (k Keeper) GetDeposit(ctx sdk.Context, status types.DepositStatus, cdpID uint64, depositor sdk.AccAddress) (deposit types.Deposit, found bool) { +func (k Keeper) GetDeposit(ctx sdk.Context, cdpID uint64, depositor sdk.AccAddress) (deposit types.Deposit, found bool) { store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix) - bz := store.Get(types.DepositKey(status, cdpID, depositor)) + bz := store.Get(types.DepositKey(cdpID, depositor)) if bz == nil { return deposit, false } @@ -147,35 +126,20 @@ func (k Keeper) GetDeposit(ctx sdk.Context, status types.DepositStatus, cdpID ui func (k Keeper) SetDeposit(ctx sdk.Context, deposit types.Deposit) { store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix) bz := k.cdc.MustMarshalBinaryLengthPrefixed(deposit) - if deposit.InLiquidation { - store.Set(types.DepositKey(types.StatusLiquidated, deposit.CdpID, deposit.Depositor), bz) - return - } - store.Set(types.DepositKey(types.StatusNil, deposit.CdpID, deposit.Depositor), bz) + store.Set(types.DepositKey(deposit.CdpID, deposit.Depositor), bz) } // DeleteDeposit deletes a deposit from the store -func (k Keeper) DeleteDeposit(ctx sdk.Context, status types.DepositStatus, cdpID uint64, depositor sdk.AccAddress) { +func (k Keeper) DeleteDeposit(ctx sdk.Context, cdpID uint64, depositor sdk.AccAddress) { store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix) - store.Delete(types.DepositKey(status, cdpID, depositor)) + store.Delete(types.DepositKey(cdpID, depositor)) } // IterateDeposits iterates over the all the deposits of a cdp and performs a callback function func (k Keeper) IterateDeposits(ctx sdk.Context, cdpID uint64, cb func(deposit types.Deposit) (stop bool)) { store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix) - iterator := sdk.KVStorePrefixIterator(store, types.DepositIterKey(types.StatusNil, cdpID)) - - defer iterator.Close() - for ; iterator.Valid(); iterator.Next() { - var deposit types.Deposit - k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &deposit) - - if cb(deposit) { - break - } - } - iterator = sdk.KVStorePrefixIterator(store, types.DepositIterKey(types.StatusLiquidated, cdpID)) + iterator := sdk.KVStorePrefixIterator(store, types.GetCdpIDBytes(cdpID)) defer iterator.Close() for ; iterator.Valid(); iterator.Next() { @@ -196,28 +160,3 @@ func (k Keeper) GetDeposits(ctx sdk.Context, cdpID uint64) (deposits types.Depos }) return } - -// IterateLiquidatedDeposits iterates over the all liquidated deposits performs a callback function -func (k Keeper) IterateLiquidatedDeposits(ctx sdk.Context, cb func(deposit types.Deposit) (stop bool)) { - store := prefix.NewStore(ctx.KVStore(k.key), types.DepositKeyPrefix) - iterator := sdk.KVStorePrefixIterator(store, []byte{types.StatusLiquidated.AsByte()}) - - defer iterator.Close() - for ; iterator.Valid(); iterator.Next() { - var deposit types.Deposit - k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &deposit) - - if cb(deposit) { - break - } - } -} - -// GetAllLiquidatedDeposits returns all deposits with status liquidated -func (k Keeper) GetAllLiquidatedDeposits(ctx sdk.Context) (deposits types.Deposits) { - k.IterateLiquidatedDeposits(ctx, func(deposit types.Deposit) bool { - deposits = append(deposits, deposit) - return false - }) - return -} diff --git a/x/cdp/keeper/deposit_test.go b/x/cdp/keeper/deposit_test.go index 8f7cb316..fab85c8a 100644 --- a/x/cdp/keeper/deposit_test.go +++ b/x/cdp/keeper/deposit_test.go @@ -45,15 +45,15 @@ func (suite *DepositTestSuite) SetupTest() { } func (suite *DepositTestSuite) TestGetSetDeposit() { - d, found := suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + d, found := suite.keeper.GetDeposit(suite.ctx, uint64(1), suite.addrs[0]) suite.True(found) td := types.NewDeposit(uint64(1), suite.addrs[0], cs(c("xrp", 400000000))) suite.True(d.Equals(td)) ds := suite.keeper.GetDeposits(suite.ctx, uint64(1)) suite.Equal(1, len(ds)) suite.True(ds[0].Equals(td)) - suite.keeper.DeleteDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) - _, found = suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + suite.keeper.DeleteDeposit(suite.ctx, uint64(1), suite.addrs[0]) + _, found = suite.keeper.GetDeposit(suite.ctx, uint64(1), suite.addrs[0]) suite.False(found) ds = suite.keeper.GetDeposits(suite.ctx, uint64(1)) suite.Equal(0, len(ds)) @@ -62,7 +62,7 @@ func (suite *DepositTestSuite) TestGetSetDeposit() { func (suite *DepositTestSuite) TestDepositCollateral() { err := suite.keeper.DepositCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 10000000))) suite.NoError(err) - d, found := suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + d, found := suite.keeper.GetDeposit(suite.ctx, uint64(1), suite.addrs[0]) suite.True(found) td := types.NewDeposit(uint64(1), suite.addrs[0], cs(c("xrp", 410000000))) suite.True(d.Equals(td)) @@ -83,7 +83,7 @@ func (suite *DepositTestSuite) TestDepositCollateral() { err = suite.keeper.DepositCollateral(suite.ctx, suite.addrs[0], suite.addrs[1], cs(c("xrp", 10000000))) suite.NoError(err) - d, found = suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[1]) + d, found = suite.keeper.GetDeposit(suite.ctx, uint64(1), suite.addrs[1]) suite.True(found) td = types.NewDeposit(uint64(1), suite.addrs[1], cs(c("xrp", 10000000))) suite.True(d.Equals(td)) @@ -98,24 +98,6 @@ func (suite *DepositTestSuite) TestWithdrawCollateral() { err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], cs(c("xrp", 10000000))) suite.Equal(types.CodeCdpNotFound, err.Result().Code) - d, _ := suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) - d.InLiquidation = true - suite.keeper.DeleteDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) - suite.keeper.SetDeposit(suite.ctx, d) - _, f := suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) - suite.False(f) - - err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 10000000))) - suite.Equal(types.CodeCdpNotAvailable, err.Result().Code) - - d, f = suite.keeper.GetDeposit(suite.ctx, types.StatusLiquidated, uint64(1), suite.addrs[0]) - suite.True(f) - suite.keeper.DeleteDeposit(suite.ctx, types.StatusLiquidated, uint64(1), suite.addrs[0]) - d.InLiquidation = false - suite.keeper.SetDeposit(suite.ctx, d) - _, f = suite.keeper.GetDeposit(suite.ctx, types.StatusLiquidated, uint64(1), suite.addrs[0]) - suite.False(f) - cd, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(1)) cd.AccumulatedFees = cs(c("usdx", 1)) suite.keeper.SetCDP(suite.ctx, cd) @@ -124,9 +106,9 @@ func (suite *DepositTestSuite) TestWithdrawCollateral() { err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 10000000))) suite.NoError(err) - d, _ = suite.keeper.GetDeposit(suite.ctx, types.StatusNil, uint64(1), suite.addrs[0]) + dep, _ := suite.keeper.GetDeposit(suite.ctx, uint64(1), suite.addrs[0]) td := types.NewDeposit(uint64(1), suite.addrs[0], cs(c("xrp", 390000000))) - suite.True(d.Equals(td)) + suite.True(dep.Equals(td)) ak := suite.app.GetAccountKeeper() acc := ak.GetAccount(suite.ctx, suite.addrs[0]) suite.Equal(i(110000000), acc.GetCoins().AmountOf("xrp")) @@ -135,20 +117,6 @@ func (suite *DepositTestSuite) TestWithdrawCollateral() { suite.Equal(types.CodeDepositNotFound, err.Result().Code) } -func (suite *DepositTestSuite) TestIterateLiquidatedDeposits() { - for j := 0; j < 10; j++ { - d := types.NewDeposit(uint64(j+2), suite.addrs[j], cs(c("xrp", 1000000))) - if j%2 == 0 { - d.InLiquidation = true - } - suite.keeper.SetDeposit(suite.ctx, d) - } - ds := suite.keeper.GetAllLiquidatedDeposits(suite.ctx) - for _, d := range ds { - suite.True(d.InLiquidation) - } - suite.Equal(5, len(ds)) -} func TestDepositTestSuite(t *testing.T) { suite.Run(t, new(DepositTestSuite)) } diff --git a/x/cdp/keeper/draw.go b/x/cdp/keeper/draw.go index 4571edf4..1bc1ac66 100644 --- a/x/cdp/keeper/draw.go +++ b/x/cdp/keeper/draw.go @@ -14,11 +14,7 @@ func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string if !found { return types.ErrCdpNotFound(k.codespace, owner, denom) } - err := k.ValidateAvailableCDP(ctx, cdp.ID) - if err != nil { - return err - } - err = k.ValidatePrincipalDraw(ctx, principal) + err := k.ValidatePrincipalDraw(ctx, principal) if err != nil { return err } @@ -84,11 +80,7 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri if !found { return types.ErrCdpNotFound(k.codespace, owner, denom) } - err := k.ValidateAvailableCDP(ctx, cdp.ID) - if err != nil { - return err - } - err = k.ValidatePaymentCoins(ctx, cdp, payment) + err := k.ValidatePaymentCoins(ctx, cdp, payment) if err != nil { return err } @@ -189,7 +181,7 @@ func (k Keeper) ReturnCollateral(ctx sdk.Context, cdp types.CDP) { if err != nil { panic(err) } - k.DeleteDeposit(ctx, types.StatusNil, cdp.ID, deposit.Depositor) + k.DeleteDeposit(ctx, cdp.ID, deposit.Depositor) } } diff --git a/x/cdp/keeper/integration_test.go b/x/cdp/keeper/integration_test.go index fd8997f2..78e1c980 100644 --- a/x/cdp/keeper/integration_test.go +++ b/x/cdp/keeper/integration_test.go @@ -39,16 +39,20 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { cdpGenesis := cdp.GenesisState{ Params: cdp.Params{ - GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, CollateralParams: cdp.CollateralParams{ { - Denom: asset, - LiquidationRatio: liquidationRatio, - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), - StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr - Prefix: 0x20, - ConversionFactor: i(6), - MarketID: asset + ":usd", + Denom: asset, + LiquidationRatio: liquidationRatio, + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr + LiquidationPenalty: d("0.05"), + AuctionSize: i(100), + Prefix: 0x20, + ConversionFactor: i(6), + MarketID: asset + ":usd", }, }, DebtParams: cdp.DebtParams{ @@ -63,6 +67,7 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { }, StartingCdpID: cdp.DefaultCdpStartingID, DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, CDPs: cdp.CDPs{}, PreviousBlockTime: cdp.DefaultPreviousBlockTime, } @@ -97,25 +102,31 @@ func NewPricefeedGenStateMulti() app.GenesisState { func NewCDPGenStateMulti() app.GenesisState { cdpGenesis := cdp.GenesisState{ Params: cdp.Params{ - GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + GlobalDebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000), sdk.NewInt64Coin("susd", 1000000000000)), + SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, + DebtAuctionThreshold: cdp.DefaultDebtThreshold, CollateralParams: cdp.CollateralParams{ { - Denom: "xrp", - LiquidationRatio: sdk.MustNewDecFromStr("2.0"), - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), - StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr - Prefix: 0x20, - MarketID: "xrp:usd", - ConversionFactor: i(6), + Denom: "xrp", + LiquidationRatio: sdk.MustNewDecFromStr("2.0"), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr + LiquidationPenalty: d("0.05"), + AuctionSize: i(7000000000), + Prefix: 0x20, + MarketID: "xrp:usd", + ConversionFactor: i(6), }, { - Denom: "btc", - LiquidationRatio: sdk.MustNewDecFromStr("1.5"), - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), - StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr - Prefix: 0x21, - MarketID: "btc:usd", - ConversionFactor: i(8), + Denom: "btc", + LiquidationRatio: sdk.MustNewDecFromStr("1.5"), + DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 500000000000), sdk.NewInt64Coin("susd", 500000000000)), + StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr + LiquidationPenalty: d("0.025"), + AuctionSize: i(10000000), + Prefix: 0x21, + MarketID: "btc:usd", + ConversionFactor: i(8), }, }, DebtParams: cdp.DebtParams{ @@ -137,6 +148,7 @@ func NewCDPGenStateMulti() app.GenesisState { }, StartingCdpID: cdp.DefaultCdpStartingID, DebtDenom: cdp.DefaultDebtDenom, + GovDenom: cdp.DefaultGovDenom, CDPs: cdp.CDPs{}, PreviousBlockTime: cdp.DefaultPreviousBlockTime, } diff --git a/x/cdp/keeper/keeper.go b/x/cdp/keeper/keeper.go index 90c3410e..94570d2b 100644 --- a/x/cdp/keeper/keeper.go +++ b/x/cdp/keeper/keeper.go @@ -17,22 +17,29 @@ type Keeper struct { paramSubspace subspace.Subspace pricefeedKeeper types.PricefeedKeeper supplyKeeper types.SupplyKeeper + auctionKeeper types.AuctionKeeper codespace sdk.CodespaceType } // NewKeeper creates a new keeper -func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, sk types.SupplyKeeper, codespace sdk.CodespaceType) Keeper { +func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, pfk types.PricefeedKeeper, ak types.AuctionKeeper, sk types.SupplyKeeper, codespace sdk.CodespaceType) Keeper { - // ensure module account is set + // ensure cdp module account is set if addr := sk.GetModuleAddress(types.ModuleName); addr == nil { panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) } + // ensure liquidator module account is set + if addr := sk.GetModuleAddress(types.LiquidatorMacc); addr == nil { + panic(fmt.Sprintf("%s module account has not been set", types.LiquidatorMacc)) + } + return Keeper{ key: key, cdc: cdc, paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()), pricefeedKeeper: pfk, + auctionKeeper: ak, supplyKeeper: sk, codespace: codespace, } diff --git a/x/cdp/keeper/params.go b/x/cdp/keeper/params.go index d7d34156..337af5a3 100644 --- a/x/cdp/keeper/params.go +++ b/x/cdp/keeper/params.go @@ -52,8 +52,7 @@ func (k Keeper) GetDenomPrefix(ctx sdk.Context, denom string) (byte, bool) { return 0x00, false } -// private methods panic if the input is invalid - +// private methods assume collateral has been validated, panic if the input is invalid func (k Keeper) getDenomFromByte(ctx sdk.Context, db byte) string { params := k.GetParams(ctx) for _, cp := range params.CollateralParams { @@ -79,3 +78,19 @@ func (k Keeper) getLiquidationRatio(ctx sdk.Context, denom string) sdk.Dec { } return cp.LiquidationRatio } + +func (k Keeper) getLiquidationPenalty(ctx sdk.Context, denom string) sdk.Dec { + cp, found := k.GetCollateral(ctx, denom) + if !found { + panic(fmt.Sprintf("collateral not found: %s", denom)) + } + return cp.LiquidationPenalty +} + +func (k Keeper) getAuctionSize(ctx sdk.Context, denom string) sdk.Int { + cp, found := k.GetCollateral(ctx, denom) + if !found { + panic(fmt.Sprintf("collateral not found: %s", denom)) + } + return cp.AuctionSize +} diff --git a/x/cdp/keeper/seize.go b/x/cdp/keeper/seize.go index 09598ecd..fcb15756 100644 --- a/x/cdp/keeper/seize.go +++ b/x/cdp/keeper/seize.go @@ -15,50 +15,51 @@ import ( // 4. decrements the total amount of principal outstanding for that collateral type // (this is the equivalent of saying that fees are no longer accumulated by a cdp once it // gets liquidated) -func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) { +func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) sdk.Error { + // Calculate the previous collateral ratio + oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees)) // Update fees periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix())) fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom) cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees) cdp.FeesUpdated = ctx.BlockTime() - // Liquidate deposits + // TODO implement liquidation penalty + + // Move debt coins from cdp to liquidator account deposits := k.GetDeposits(ctx, cdp.ID) - for _, dep := range deposits { - if !dep.InLiquidation { - dep.InLiquidation = true - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeCdpLiquidation, - sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), - sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)), - sdk.NewAttribute(types.AttributeKeyDepositor, fmt.Sprintf("%s", dep.Depositor)), - ), - ) - k.DeleteDeposit(ctx, types.StatusNil, cdp.ID, dep.Depositor) - k.SetDeposit(ctx, dep) - err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, dep.Amount) - if err != nil { - panic(err) - } - } else { - return - } - } - - // Transfer debt coins from cdp module account to liquidator module account - debtAmt := sdk.ZeroInt() - for _, dc := range cdp.Principal { - debtAmt = debtAmt.Add(dc.Amount) + debt := sdk.ZeroInt() + for _, pc := range cdp.Principal { + debt = debt.Add(pc.Amount) } for _, dc := range cdp.AccumulatedFees { - debtAmt = debtAmt.Add(dc.Amount) + debt = debt.Add(dc.Amount) } - debtCoins := sdk.NewCoins(sdk.NewCoin(k.GetDebtDenom(ctx), debtAmt)) - err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, debtCoins) + debtCoin := sdk.NewCoin(k.GetDebtDenom(ctx), debt) + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, sdk.NewCoins(debtCoin)) if err != nil { - panic(err) + return err + } + + // liquidate deposits and send collateral from cdp to liquidator + for _, dep := range deposits { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeCdpLiquidation, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)), + sdk.NewAttribute(types.AttributeKeyDepositor, fmt.Sprintf("%s", dep.Depositor)), + ), + ) + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, dep.Amount) + if err != nil { + return err + } + k.DeleteDeposit(ctx, dep.CdpID, dep.Depositor) + } + err = k.AuctionCollateral(ctx, deposits, debt, cdp.Principal[0].Denom) + if err != nil { + return err } // Decrement total principal for this collateral type @@ -71,6 +72,10 @@ func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) { } k.DecrementTotalPrincipal(ctx, cdp.Collateral[0].Denom, coinsToDecrement) } + k.RemoveCdpOwnerIndex(ctx, cdp) + k.RemoveCdpCollateralRatioIndex(ctx, cdp.Collateral[0].Denom, cdp.ID, oldCollateralToDebtRatio) + k.DeleteCDP(ctx, cdp) + return nil } // HandleNewDebt compounds the accumulated fees for the input collateral and principal coins. @@ -88,15 +93,18 @@ func (k Keeper) HandleNewDebt(ctx sdk.Context, collateralDenom string, principal } // LiquidateCdps seizes collateral from all CDPs below the input liquidation ratio -func (k Keeper) LiquidateCdps(ctx sdk.Context, marketID string, denom string, liquidationRatio sdk.Dec) { +func (k Keeper) LiquidateCdps(ctx sdk.Context, marketID string, denom string, liquidationRatio sdk.Dec) sdk.Error { price, err := k.pricefeedKeeper.GetCurrentPrice(ctx, marketID) if err != nil { - return + return err } normalizedRatio := sdk.OneDec().Quo(price.Price.Quo(liquidationRatio)) cdpsToLiquidate := k.GetAllCdpsByDenomAndRatio(ctx, denom, normalizedRatio) for _, c := range cdpsToLiquidate { - k.SeizeCollateral(ctx, c) + err := k.SeizeCollateral(ctx, c) + if err != nil { + return err + } } - return + return nil } diff --git a/x/cdp/keeper/seize_test.go b/x/cdp/keeper/seize_test.go index e40dc786..0c89db6b 100644 --- a/x/cdp/keeper/seize_test.go +++ b/x/cdp/keeper/seize_test.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/simulation" "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/cdp/keeper" "github.com/kava-labs/kava/x/cdp/types" "github.com/stretchr/testify/suite" @@ -55,15 +56,15 @@ func (suite *SeizeTestSuite) SetupTest() { suite.ctx = ctx suite.app = tApp suite.keeper = tApp.GetCDPKeeper() - + randSource := rand.New(rand.NewSource(int64(777))) for j := 0; j < 100; j++ { collateral := "xrp" amount := 10000000000 - debt := simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 750000000, 1249000000) + debt := simulation.RandIntBetween(randSource, 750000000, 1249000000) if j%2 == 0 { collateral = "btc" amount = 100000000 - debt = simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 2700000000, 5332000000) + debt = simulation.RandIntBetween(randSource, 2700000000, 5332000000) if debt >= 4000000000 { tracker.btc = append(tracker.btc, uint64(j+1)) tracker.debt += int64(debt) @@ -103,16 +104,41 @@ func (suite *SeizeTestSuite) TestSeizeCollateral() { p := cdp.Principal[0].Amount cl := cdp.Collateral[0].Amount tpb := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") - suite.keeper.SeizeCollateral(suite.ctx, cdp) + err := suite.keeper.SeizeCollateral(suite.ctx, cdp) + suite.NoError(err) tpa := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") suite.Equal(tpb.Sub(tpa), p) - liqModAcc := sk.GetModuleAccount(suite.ctx, types.LiquidatorMacc) - suite.Equal(cs(c("debt", p.Int64()), c("xrp", cl.Int64())), liqModAcc.GetCoins()) + auctionMacc := sk.GetModuleAccount(suite.ctx, auction.ModuleName) + suite.Equal(cs(c("debt", p.Int64()), c("xrp", cl.Int64())), auctionMacc.GetCoins()) ak := suite.app.GetAccountKeeper() acc := ak.GetAccount(suite.ctx, suite.addrs[1]) suite.Equal(p.Int64(), acc.GetCoins().AmountOf("usdx").Int64()) - err := suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[1], cs(c("xrp", 10))) - suite.Equal(types.CodeCdpNotAvailable, err.Result().Code) + err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[1], cs(c("xrp", 10))) + suite.Equal(types.CodeCdpNotFound, err.Result().Code) +} + +func (suite *SeizeTestSuite) TestSeizeCollateralMultiDeposit() { + sk := suite.app.GetSupplyKeeper() + cdp, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2)) + err := suite.keeper.DepositCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], cs(c("xrp", 6999000000))) + suite.NoError(err) + cdp, _ = suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2)) + deposits := suite.keeper.GetDeposits(suite.ctx, cdp.ID) + suite.Equal(2, len(deposits)) + p := cdp.Principal[0].Amount + cl := cdp.Collateral[0].Amount + tpb := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") + err = suite.keeper.SeizeCollateral(suite.ctx, cdp) + suite.NoError(err) + tpa := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") + suite.Equal(tpb.Sub(tpa), p) + auctionMacc := sk.GetModuleAccount(suite.ctx, auction.ModuleName) + suite.Equal(cs(c("debt", p.Int64()), c("xrp", cl.Int64())), auctionMacc.GetCoins()) + ak := suite.app.GetAccountKeeper() + acc := ak.GetAccount(suite.ctx, suite.addrs[1]) + suite.Equal(p.Int64(), acc.GetCoins().AmountOf("usdx").Int64()) + err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[1], cs(c("xrp", 10))) + suite.Equal(types.CodeCdpNotFound, err.Result().Code) } func (suite *SeizeTestSuite) TestLiquidateCdps() { diff --git a/x/cdp/types/deposit.go b/x/cdp/types/deposit.go index 5e6b0fae..1cafcb59 100644 --- a/x/cdp/types/deposit.go +++ b/x/cdp/types/deposit.go @@ -8,50 +8,22 @@ import ( // Deposit defines an amount of coins deposited by an account to a cdp type Deposit struct { - CdpID uint64 `json:"cdp_id" yaml:"cdp_id"` // cdpID of the cdp - Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"` // Address of the depositor - Amount sdk.Coins `json:"amount" yaml:"amount"` // Deposit amount - InLiquidation bool `json:"in_liquidation" yaml:"in_liquidation"` -} - -// DepositStatus is a type alias that represents a deposit status as a byte -type DepositStatus byte - -// Valid Deposit statuses -const ( - StatusNil DepositStatus = 0x00 - StatusLiquidated DepositStatus = 0x01 -) - -// AsByte returns the status as byte -func (ds DepositStatus) AsByte() byte { - return byte(ds) -} - -// StatusFromByte returns the status from its byte representation -func StatusFromByte(b byte) DepositStatus { - switch b { - case 0x00: - return StatusNil - case 0x01: - return StatusLiquidated - default: - panic(fmt.Sprintf("unrecognized deposit status, %v", b)) - } + CdpID uint64 `json:"cdp_id" yaml:"cdp_id"` // cdpID of the cdp + Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"` // Address of the depositor + Amount sdk.Coins `json:"amount" yaml:"amount"` // Deposit amount } // NewDeposit creates a new Deposit object func NewDeposit(cdpID uint64, depositor sdk.AccAddress, amount sdk.Coins) Deposit { - return Deposit{cdpID, depositor, amount, false} + return Deposit{cdpID, depositor, amount} } // String implements fmt.Stringer func (d Deposit) String() string { return fmt.Sprintf(`Deposit for CDP %d: Depositor: %s - Amount: %s - In Liquidation: %t`, - d.CdpID, d.Depositor, d.Amount, d.InLiquidation) + Amount: %s`, + d.CdpID, d.Depositor, d.Amount) } // Deposits a collection of Deposit objects @@ -65,9 +37,6 @@ func (ds Deposits) String() string { out := fmt.Sprintf("Deposits for CDP %d:", ds[0].CdpID) for _, dep := range ds { out += fmt.Sprintf("\n %s: %s", dep.Depositor, dep.Amount) - if dep.InLiquidation { - out += fmt.Sprintf("(in liquidation)") - } } return out } @@ -81,3 +50,13 @@ func (d Deposit) Equals(comp Deposit) bool { func (d Deposit) Empty() bool { return d.Equals(Deposit{}) } + +func (ds Deposits) SumCollateral() (sum sdk.Int) { + sum = sdk.ZeroInt() + for _, d := range ds { + if !d.Amount.IsZero() { + sum = sum.Add(d.Amount[0].Amount) + } + } + return +} diff --git a/x/cdp/types/events.go b/x/cdp/types/events.go index ba58c9d6..1884084b 100644 --- a/x/cdp/types/events.go +++ b/x/cdp/types/events.go @@ -2,15 +2,17 @@ package types // Event types for cdp module const ( - EventTypeCreateCdp = "create_cdp" - EventTypeCdpDeposit = "cdp_deposit" - EventTypeCdpDraw = "cdp_draw" - EventTypeCdpRepay = "cdp_repayment" - EventTypeCdpClose = "cdp_close" - EventTypeCdpWithdrawal = "cdp_withdrawal" - EventTypeCdpLiquidation = "cdp_liquidation" + EventTypeCreateCdp = "create_cdp" + EventTypeCdpDeposit = "cdp_deposit" + EventTypeCdpDraw = "cdp_draw" + EventTypeCdpRepay = "cdp_repayment" + EventTypeCdpClose = "cdp_close" + EventTypeCdpWithdrawal = "cdp_withdrawal" + EventTypeCdpLiquidation = "cdp_liquidation" + EventTypeBeginBlockerFatal = "cdp_begin_block_error" AttributeKeyCdpID = "cdp_id" AttributeKeyDepositor = "depositor" AttributeValueCategory = "cdp" + AttributeKeyError = "error_message" ) diff --git a/x/cdp/types/expected_keepers.go b/x/cdp/types/expected_keepers.go index c57ad94f..402458dc 100644 --- a/x/cdp/types/expected_keepers.go +++ b/x/cdp/types/expected_keepers.go @@ -32,3 +32,10 @@ type PricefeedKeeper interface { SetPrice(sdk.Context, sdk.AccAddress, string, sdk.Dec, time.Time) (pftypes.PostedPrice, sdk.Error) SetCurrentPrices(sdk.Context, string) sdk.Error } + +// AuctionKeeper expected interface for the auction keeper (noalias) +type AuctionKeeper interface { + StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (uint64, sdk.Error) + StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin, debt sdk.Coin) (uint64, sdk.Error) + StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int, debt sdk.Coin) (uint64, sdk.Error) +} diff --git a/x/cdp/types/genesis.go b/x/cdp/types/genesis.go index 5c4ebf61..8737bce5 100644 --- a/x/cdp/types/genesis.go +++ b/x/cdp/types/genesis.go @@ -13,6 +13,7 @@ type GenesisState struct { Deposits Deposits `json:"deposits" yaml:"deposits"` StartingCdpID uint64 `json:"starting_cdp_id" yaml:"starting_cdp_id"` DebtDenom string `json:"debt_denom" yaml:"debt_denom"` + GovDenom string `json:"gov_denom" yaml:"gov_denom"` PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"` } @@ -24,6 +25,7 @@ func DefaultGenesisState() GenesisState { Deposits: Deposits{}, StartingCdpID: DefaultCdpStartingID, DebtDenom: DefaultDebtDenom, + GovDenom: DefaultGovDenom, PreviousBlockTime: DefaultPreviousBlockTime, } } diff --git a/x/cdp/types/keys.go b/x/cdp/types/keys.go index a128e387..180ad38e 100644 --- a/x/cdp/types/keys.go +++ b/x/cdp/types/keys.go @@ -51,10 +51,11 @@ var ( CollateralRatioIndexPrefix = []byte{0x02} CdpIDKey = []byte{0x03} DebtDenomKey = []byte{0x04} - DepositKeyPrefix = []byte{0x05} - PrincipalKeyPrefix = []byte{0x06} - AccumulatorKeyPrefix = []byte{0x07} - PreviousBlockTimeKey = []byte{0x08} + GovDenomKey = []byte{0x05} + DepositKeyPrefix = []byte{0x06} + PrincipalKeyPrefix = []byte{0x07} + AccumulatorKeyPrefix = []byte{0x08} + PreviousBlockTimeKey = []byte{0x09} ) var lenPositiveDec = len(SortableDecBytes(sdk.OneDec())) @@ -95,28 +96,25 @@ func SplitDenomIterKey(key []byte) byte { } // DepositKey key of a specific deposit in the store -func DepositKey(status DepositStatus, cdpID uint64, depositor sdk.AccAddress) []byte { - return createKey([]byte{status.AsByte()}, sep, GetCdpIDBytes(cdpID), sep, depositor) +func DepositKey(cdpID uint64, depositor sdk.AccAddress) []byte { + return createKey(GetCdpIDBytes(cdpID), sep, depositor) } // SplitDepositKey returns the component parts of a deposit key -func SplitDepositKey(key []byte) (DepositStatus, uint64, sdk.AccAddress) { - status := StatusFromByte(key[0]) - cdpID := GetCdpIDFromBytes(key[2:10]) - addr := key[11:] - return status, cdpID, addr +func SplitDepositKey(key []byte) (uint64, sdk.AccAddress) { + cdpID := GetCdpIDFromBytes(key[0:8]) + addr := key[9:] + return cdpID, addr } // DepositIterKey returns the prefix key for iterating over deposits to a cdp -func DepositIterKey(status DepositStatus, cdpID uint64) []byte { - return createKey([]byte{status.AsByte()}, sep, GetCdpIDBytes(cdpID)) +func DepositIterKey(cdpID uint64) []byte { + return GetCdpIDBytes(cdpID) } // SplitDepositIterKey returns the component parts of a key for iterating over deposits on a cdp -func SplitDepositIterKey(key []byte) (status DepositStatus, cdpID uint64) { - status = StatusFromByte(key[0]) - cdpID = GetCdpIDFromBytes(key[2:]) - return status, cdpID +func SplitDepositIterKey(key []byte) (cdpID uint64) { + return GetCdpIDFromBytes(key) } // CollateralRatioBytes returns the liquidation ratio as sortable bytes diff --git a/x/cdp/types/keys_test.go b/x/cdp/types/keys_test.go index 51d4de26..935f6415 100644 --- a/x/cdp/types/keys_test.go +++ b/x/cdp/types/keys_test.go @@ -20,18 +20,16 @@ func TestKeys(t *testing.T) { db = SplitDenomIterKey(denomKey) require.Equal(t, byte(0x01), db) - depositKey := DepositKey(StatusNil, 2, addr) - status, id, a := SplitDepositKey(depositKey) + depositKey := DepositKey(2, addr) + id, a := SplitDepositKey(depositKey) require.Equal(t, 2, int(id)) require.Equal(t, a, addr) - require.Equal(t, StatusNil, status) - depositIterKey := DepositIterKey(StatusLiquidated, 2) - status, id = SplitDepositIterKey(depositIterKey) + depositIterKey := DepositIterKey(2) + id = SplitDepositIterKey(depositIterKey) require.Equal(t, 2, int(id)) - require.Equal(t, StatusLiquidated, status) - require.Panics(t, func() { SplitDepositIterKey(append([]byte{0x03}, GetCdpIDBytes(2)...)) }) + require.Panics(t, func() { SplitDepositIterKey([]byte{0x03}) }) collateralKey := CollateralRatioKey(0x01, 2, sdk.MustNewDecFromStr("1.50")) db, id, ratio := SplitCollateralRatioKey(collateralKey) diff --git a/x/cdp/types/params.go b/x/cdp/types/params.go index c0a43271..4dee6ef7 100644 --- a/x/cdp/types/params.go +++ b/x/cdp/types/params.go @@ -15,12 +15,17 @@ var ( KeyCollateralParams = []byte("CollateralParams") KeyDebtParams = []byte("DebtParams") KeyCircuitBreaker = []byte("CircuitBreaker") + KeyDebtThreshold = []byte("DebtThreshold") + KeySurplusThreshold = []byte("SurplusThreshold") DefaultGlobalDebt = sdk.Coins{} DefaultCircuitBreaker = false DefaultCollateralParams = CollateralParams{} DefaultDebtParams = DebtParams{} DefaultCdpStartingID = uint64(1) DefaultDebtDenom = "debt" + DefaultGovDenom = "ukava" + DefaultSurplusThreshold = sdk.NewInt(1000) + DefaultDebtThreshold = sdk.NewInt(1000) DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0)) minCollateralPrefix = 0 maxCollateralPrefix = 255 @@ -28,10 +33,12 @@ var ( // Params governance parameters for cdp module type Params struct { - CollateralParams CollateralParams `json:"collateral_params" yaml:"collateral_params"` - DebtParams DebtParams `json:"debt_params" yaml:"debt_params"` - GlobalDebtLimit sdk.Coins `json:"global_debt_limit" yaml:"global_debt_limit"` - CircuitBreaker bool `json:"circuit_breaker" yaml:"circuit_breaker"` + CollateralParams CollateralParams `json:"collateral_params" yaml:"collateral_params"` + DebtParams DebtParams `json:"debt_params" yaml:"debt_params"` + GlobalDebtLimit sdk.Coins `json:"global_debt_limit" yaml:"global_debt_limit"` + SurplusAuctionThreshold sdk.Int `json:"surplus_auction_threshold" yaml:"surplus_auction_threshold"` + DebtAuctionThreshold sdk.Int `json:"debt_auction_threshold" yaml:"debt_auction_threshold"` + CircuitBreaker bool `json:"circuit_breaker" yaml:"circuit_breaker"` } // String implements fmt.Stringer @@ -40,35 +47,41 @@ func (p Params) String() string { Global Debt Limit: %s Collateral Params: %s Debt Params: %s + Surplus Auction Threshold: %s + Debt Auction Threshold: %s Circuit Breaker: %t`, - p.GlobalDebtLimit, p.CollateralParams, p.DebtParams, p.CircuitBreaker, + p.GlobalDebtLimit, p.CollateralParams, p.DebtParams, p.SurplusAuctionThreshold, p.DebtAuctionThreshold, p.CircuitBreaker, ) } // NewParams returns a new params object -func NewParams(debtLimit sdk.Coins, collateralParams CollateralParams, debtParams DebtParams, breaker bool) Params { +func NewParams(debtLimit sdk.Coins, collateralParams CollateralParams, debtParams DebtParams, surplusThreshold sdk.Int, debtThreshold sdk.Int, breaker bool) Params { return Params{ - GlobalDebtLimit: debtLimit, - CollateralParams: collateralParams, - DebtParams: debtParams, - CircuitBreaker: breaker, + GlobalDebtLimit: debtLimit, + CollateralParams: collateralParams, + DebtParams: debtParams, + DebtAuctionThreshold: debtThreshold, + SurplusAuctionThreshold: surplusThreshold, + CircuitBreaker: breaker, } } // DefaultParams returns default params for cdp module func DefaultParams() Params { - return NewParams(DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParams, DefaultCircuitBreaker) + return NewParams(DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParams, DefaultSurplusThreshold, DefaultDebtThreshold, DefaultCircuitBreaker) } // CollateralParam governance parameters for each collateral type within the cdp module type CollateralParam struct { - Denom string `json:"denom" yaml:"denom"` // Coin name of collateral type - LiquidationRatio sdk.Dec `json:"liquidation_ratio" yaml:"liquidation_ratio"` // The ratio (Collateral (priced in stable coin) / Debt) under which a CDP will be liquidated - DebtLimit sdk.Coins `json:"debt_limit" yaml:"debt_limit"` // Maximum amount of debt allowed to be drawn from this collateral type - StabilityFee sdk.Dec `json:"stability_fee" yaml:"stability_fee"` // per second stability fee for loans opened using this collateral - Prefix byte `json:"prefix" yaml:"prefix"` - MarketID string `json:"market_id" yaml:"market_id"` // marketID for fetching price of the asset from the pricefeed - ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` // factor for converting internal units to one base unit of collateral + Denom string `json:"denom" yaml:"denom"` // Coin name of collateral type + LiquidationRatio sdk.Dec `json:"liquidation_ratio" yaml:"liquidation_ratio"` // The ratio (Collateral (priced in stable coin) / Debt) under which a CDP will be liquidated + DebtLimit sdk.Coins `json:"debt_limit" yaml:"debt_limit"` // Maximum amount of debt allowed to be drawn from this collateral type + StabilityFee sdk.Dec `json:"stability_fee" yaml:"stability_fee"` // per second stability fee for loans opened using this collateral + AuctionSize sdk.Int // Max amount of collateral to sell off in any one auction. + LiquidationPenalty sdk.Dec // percentage penalty (between [0, 1]) applied to a cdp if it is liquidated + Prefix byte `json:"prefix" yaml:"prefix"` + MarketID string `json:"market_id" yaml:"market_id"` // marketID for fetching price of the asset from the pricefeed + ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` // factor for converting internal units to one base unit of collateral } // String implements fmt.Stringer @@ -77,11 +90,13 @@ func (cp CollateralParam) String() string { Denom: %s Liquidation Ratio: %s Stability Fee: %s + Liquidation Penalty: %s Debt Limit: %s + Auction Size: %s Prefix: %b Market ID: %s Conversion Factor: %s`, - cp.Denom, cp.LiquidationRatio, cp.StabilityFee, cp.DebtLimit, cp.Prefix, cp.MarketID, cp.ConversionFactor) + cp.Denom, cp.LiquidationRatio, cp.StabilityFee, cp.LiquidationPenalty, cp.DebtLimit, cp.AuctionSize, cp.Prefix, cp.MarketID, cp.ConversionFactor) } // CollateralParams array of CollateralParam @@ -140,6 +155,8 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs { {Key: KeyCollateralParams, Value: &p.CollateralParams}, {Key: KeyDebtParams, Value: &p.DebtParams}, {Key: KeyCircuitBreaker, Value: &p.CircuitBreaker}, + {Key: KeySurplusThreshold, Value: &p.SurplusAuctionThreshold}, + {Key: KeyDebtThreshold, Value: &p.DebtAuctionThreshold}, } } @@ -203,6 +220,12 @@ func (p Params) Validate() error { return fmt.Errorf("collateral debt limit for %s exceeds global debt limit: \n\tglobal debt limit: %s\n\tcollateral debt limits: %s", cp.Denom, p.GlobalDebtLimit, cp.DebtLimit) } + if cp.LiquidationPenalty.LT(sdk.ZeroDec()) || cp.LiquidationPenalty.GT(sdk.OneDec()) { + return fmt.Errorf("liquidation penalty should be between 0 and 1, is %s for %s", cp.LiquidationPenalty, cp.Denom) + } + if !cp.AuctionSize.IsPositive() { + return fmt.Errorf("auction size should be positive, is %s for %s", cp.AuctionSize, cp.Denom) + } } if collateralParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) { return fmt.Errorf("collateral debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tcollateral debt limits: %s", @@ -212,5 +235,12 @@ func (p Params) Validate() error { if p.GlobalDebtLimit.IsAnyNegative() { return fmt.Errorf("global debt limit should be positive for all debt tokens, is %s", p.GlobalDebtLimit) } + + if !p.SurplusAuctionThreshold.IsPositive() { + return fmt.Errorf("surplus auction threshold should be positive, is %s", p.SurplusAuctionThreshold) + } + if !p.DebtAuctionThreshold.IsPositive() { + return fmt.Errorf("debt auction threshold should be positive, is %s", p.DebtAuctionThreshold) + } return nil } From 6bf1a4ce5b055613698e2a31826abbe7039ef7fe Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Thu, 16 Jan 2020 12:15:18 +0100 Subject: [PATCH 56/99] fix: genesis param json tags --- x/auction/types/genesis.go | 4 ++-- x/pricefeed/types/genesis.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x/auction/types/genesis.go b/x/auction/types/genesis.go index ccf75c8e..12eb5322 100644 --- a/x/auction/types/genesis.go +++ b/x/auction/types/genesis.go @@ -20,7 +20,7 @@ type GenesisAuctions []GenesisAuction // GenesisState is auction state that must be provided at chain genesis. type GenesisState struct { NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` - Params Params `json:"auction_params" yaml:"auction_params"` + Params Params `json:"params" yaml:"params"` Auctions GenesisAuctions `json:"genesis_auctions" yaml:"genesis_auctions"` } @@ -50,7 +50,7 @@ func (gs GenesisState) IsEmpty() bool { return gs.Equal(GenesisState{}) } -// ValidateGenesis validates genesis inputs. It returns error if validation of any input fails. +// Validate validates genesis inputs. It returns error if validation of any input fails. func (gs GenesisState) Validate() error { if err := gs.Params.Validate(); err != nil { return err diff --git a/x/pricefeed/types/genesis.go b/x/pricefeed/types/genesis.go index 775143c1..4bcba82c 100644 --- a/x/pricefeed/types/genesis.go +++ b/x/pricefeed/types/genesis.go @@ -6,7 +6,7 @@ import ( // GenesisState - pricefeed state that must be provided at genesis type GenesisState struct { - Params Params `json:"asset_params" yaml:"asset_params"` + Params Params `json:"params" yaml:"params"` PostedPrices []PostedPrice `json:"posted_prices" yaml:"posted_prices"` } From 407361313e08cd0edd9812b45c4c95b9191e3390 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Thu, 16 Jan 2020 12:15:27 +0100 Subject: [PATCH 57/99] chore: linting --- x/auction/types/auctions.go | 5 +++-- x/auction/types/events.go | 1 + x/auction/types/keys.go | 4 ++++ x/auction/types/params.go | 5 ++--- x/auction/types/querier.go | 4 ++-- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 203d9fec..3ef09f47 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -8,10 +8,10 @@ import ( "github.com/cosmos/cosmos-sdk/x/supply" ) -// distantFuture is a very large time value to use as initial the ending time for auctions. +// DistantFuture is a very large time value to use as initial the ending time for auctions. // It is not set to the max time supported. This can cause problems with time comparisons, see https://stackoverflow.com/a/32620397. // Also amino panics when encoding times ≥ the start of year 10000. -var DistantFuture time.Time = time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC) +var DistantFuture = time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC) // Auction is an interface for handling common actions on auctions. type Auction interface { @@ -57,6 +57,7 @@ func (a BaseAuction) GetBid() sdk.Coin { return a.Bid } // GetEndTime is a getter for auction end time. func (a BaseAuction) GetEndTime() time.Time { return a.EndTime } +// Validate verifies that the auction end time is before max end time func (a BaseAuction) Validate() error { if a.EndTime.After(a.MaxEndTime) { return fmt.Errorf("MaxEndTime < EndTime (%s < %s)", a.MaxEndTime, a.EndTime) diff --git a/x/auction/types/events.go b/x/auction/types/events.go index 032882c7..9d85743a 100644 --- a/x/auction/types/events.go +++ b/x/auction/types/events.go @@ -1,5 +1,6 @@ package types +// Events for auction module const ( EventTypeAuctionStart = "auction_start" EventTypeAuctionBid = "auction_bid" diff --git a/x/auction/types/keys.go b/x/auction/types/keys.go index 03c8cc29..b32188d0 100644 --- a/x/auction/types/keys.go +++ b/x/auction/types/keys.go @@ -20,9 +20,11 @@ const ( // DefaultParamspace default name for parameter store DefaultParamspace = ModuleName + // QuerierRoute route used for abci queries QuerierRoute = ModuleName ) +// Key prefixes var ( AuctionKeyPrefix = []byte{0x00} // prefix for keys that store auctions AuctionByTimeKeyPrefix = []byte{0x01} // prefix for keys that are part of the auctionsByTime index @@ -30,10 +32,12 @@ var ( NextAuctionIDKey = []byte{0x02} // key for the next auction id ) +// GetAuctionKey returns the bytes of an auction key func GetAuctionKey(auctionID uint64) []byte { return Uint64ToBytes(auctionID) } +// GetAuctionByTimeKey returns the key for iterating auctions by time func GetAuctionByTimeKey(endTime time.Time, auctionID uint64) []byte { return append(sdk.FormatTimeBytes(endTime), Uint64ToBytes(auctionID)...) } diff --git a/x/auction/types/params.go b/x/auction/types/params.go index 5f52f258..cbea5b11 100644 --- a/x/auction/types/params.go +++ b/x/auction/types/params.go @@ -54,11 +54,10 @@ func ParamKeyTable() subspace.KeyTable { } // ParamSetPairs implements the ParamSet interface and returns all the key/value pairs. -// nolint func (p *Params) ParamSetPairs() subspace.ParamSetPairs { return subspace.ParamSetPairs{ - {KeyAuctionBidDuration, &p.BidDuration}, - {KeyAuctionDuration, &p.MaxAuctionDuration}, + {Key: KeyAuctionBidDuration, Value: &p.BidDuration}, + {Key: KeyAuctionDuration, Value: &p.MaxAuctionDuration}, } } diff --git a/x/auction/types/querier.go b/x/auction/types/querier.go index 3b373317..34474b9a 100644 --- a/x/auction/types/querier.go +++ b/x/auction/types/querier.go @@ -19,8 +19,8 @@ func (n QueryResAuctions) String() string { // QueryAllAuctionParams is the params for an auctions query type QueryAllAuctionParams struct { - Page int `json"page:" yaml:"page"` - Limit int `json"limit:" yaml:"limit"` + Page int `json:"page" yaml:"page"` + Limit int `json:"limit" yaml:"limit"` } // NewQueryAllAuctionParams creates a new QueryAllAuctionParams From 69436f447fabc2c589047ca447c8ac115c5eec12 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Thu, 16 Jan 2020 14:30:59 +0100 Subject: [PATCH 58/99] fix: missing tags in collateral --- x/cdp/types/params.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/x/cdp/types/params.go b/x/cdp/types/params.go index 4dee6ef7..58b9016a 100644 --- a/x/cdp/types/params.go +++ b/x/cdp/types/params.go @@ -73,12 +73,12 @@ func DefaultParams() Params { // CollateralParam governance parameters for each collateral type within the cdp module type CollateralParam struct { - Denom string `json:"denom" yaml:"denom"` // Coin name of collateral type - LiquidationRatio sdk.Dec `json:"liquidation_ratio" yaml:"liquidation_ratio"` // The ratio (Collateral (priced in stable coin) / Debt) under which a CDP will be liquidated - DebtLimit sdk.Coins `json:"debt_limit" yaml:"debt_limit"` // Maximum amount of debt allowed to be drawn from this collateral type - StabilityFee sdk.Dec `json:"stability_fee" yaml:"stability_fee"` // per second stability fee for loans opened using this collateral - AuctionSize sdk.Int // Max amount of collateral to sell off in any one auction. - LiquidationPenalty sdk.Dec // percentage penalty (between [0, 1]) applied to a cdp if it is liquidated + Denom string `json:"denom" yaml:"denom"` // Coin name of collateral type + LiquidationRatio sdk.Dec `json:"liquidation_ratio" yaml:"liquidation_ratio"` // The ratio (Collateral (priced in stable coin) / Debt) under which a CDP will be liquidated + DebtLimit sdk.Coins `json:"debt_limit" yaml:"debt_limit"` // Maximum amount of debt allowed to be drawn from this collateral type + StabilityFee sdk.Dec `json:"stability_fee" yaml:"stability_fee"` // per second stability fee for loans opened using this collateral + AuctionSize sdk.Int `json:"auction_size" yaml:"auction_size"` // Max amount of collateral to sell off in any one auction. + LiquidationPenalty sdk.Dec `json:"liquidation_penalty" yaml:"liquidation_penalty"` // percentage penalty (between [0, 1]) applied to a cdp if it is liquidated Prefix byte `json:"prefix" yaml:"prefix"` MarketID string `json:"market_id" yaml:"market_id"` // marketID for fetching price of the asset from the pricefeed ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` // factor for converting internal units to one base unit of collateral From 2193b19c1dfcb949856bc806b3932a874dc6779e Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Thu, 16 Jan 2020 16:17:16 +0100 Subject: [PATCH 59/99] fix: genesis auctions tag --- x/auction/types/genesis.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/auction/types/genesis.go b/x/auction/types/genesis.go index 12eb5322..6180f21e 100644 --- a/x/auction/types/genesis.go +++ b/x/auction/types/genesis.go @@ -21,7 +21,7 @@ type GenesisAuctions []GenesisAuction type GenesisState struct { NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` Params Params `json:"params" yaml:"params"` - Auctions GenesisAuctions `json:"genesis_auctions" yaml:"genesis_auctions"` + Auctions GenesisAuctions `json:"auctions" yaml:"auctions"` } // NewGenesisState returns a new genesis state object for auctions module. From 2d9820b3d1073a91d696eede0c7f8d17eaebd2a8 Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Thu, 16 Jan 2020 16:20:12 +0100 Subject: [PATCH 60/99] cdp cli clean up (#305) * clean up querier cmds #304 #298 #296 * #299 address cli ux issues * fix typo * edit help message --- x/cdp/client/cli/query.go | 51 ++++++++++++------- x/cdp/client/cli/tx.go | 101 +++++++++++++++++++++++--------------- x/cdp/keeper/querier.go | 8 ++- x/cdp/types/params.go | 2 +- x/cdp/types/querier.go | 12 ++--- 5 files changed, 104 insertions(+), 70 deletions(-) diff --git a/x/cdp/client/cli/query.go b/x/cdp/client/cli/query.go index 45b768ec..01247bd7 100644 --- a/x/cdp/client/cli/query.go +++ b/x/cdp/client/cli/query.go @@ -10,6 +10,7 @@ import ( "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/version" "github.com/kava-labs/kava/x/cdp/types" ) @@ -35,9 +36,15 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { // QueryCdpCmd returns the command handler for querying a particular cdp func QueryCdpCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "cdp [ownerAddress] [collateralType]", + Use: "cdp [owner-addr] [collateral-name]", Short: "get info about a cdp", - Args: cobra.ExactArgs(2), + Long: strings.TrimSpace( + fmt.Sprintf(`Get a CDP by the owner address and the collateral name. + +Example: +$ %s query %s cdp kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw uatom +`, version.ClientName, types.ModuleName)), + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -46,9 +53,8 @@ func QueryCdpCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { if err != nil { return err } - collateralType := args[1] // TODO validation? bz, err := cdc.MarshalJSON(types.QueryCdpParams{ - CollateralDenom: collateralType, + CollateralDenom: args[1], Owner: ownerAddress, }) if err != nil { @@ -73,13 +79,15 @@ func QueryCdpCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { // QueryCdpsByDenomCmd returns the command handler for querying cdps for a collateral type func QueryCdpsByDenomCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "cdps [collateralType]", - Short: "Query cdps by collateral type", - Long: strings.TrimSpace(`Query cdps by a specific collateral type, or query all cdps if none is specifed: + Use: "cdps [collateral-name]", + Short: "query CDPs by collateral", + Long: strings.TrimSpace( + fmt.Sprintf(`List all CDPs collateralized with the specified asset. -$ query cdp cdps uatom - `), - Args: cobra.MaximumNArgs(1), +Example: +$ %s query %s cdps uatom +`, version.ClientName, types.ModuleName)), + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -105,32 +113,37 @@ $ query cdp cdps uatom } // QueryCdpsByDenomAndRatioCmd returns the command handler for querying cdps -// by specified collateral type and collateralization ratio +// that are under the specified collateral ratio func QueryCdpsByDenomAndRatioCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "cdps [collateralType] [ratio]", - Short: "get cdps with matching collateral type and below the specified ratio", - Long: strings.TrimSpace(`Get all CDPS of a particular collateral type with collateralization - ratio below the specified input.`), + Use: "cdps-by-ratio [collateral-name] [collateralization-ratio]", + Short: "get cdps under a collateralization ratio", + Long: strings.TrimSpace( + fmt.Sprintf(`List all CDPs under a collateralization ratios. +Collateralization ratio is: collateral * price / debt. + +Example: +$ %s query %s cdps-by-ratio uatom 1.5 +`, version.ClientName, types.ModuleName)), 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]) + ratio, errSdk := sdk.NewDecFromStr(args[1]) if errSdk != nil { return fmt.Errorf(errSdk.Error()) } bz, err := cdc.MarshalJSON(types.QueryCdpsByRatioParams{ CollateralDenom: args[0], - Ratio: price, + Ratio: ratio, }) if err != nil { return err } // Query - route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdps) + route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdpsByCollateralization) res, _, err := cliCtx.QueryWithData(route, bz) if err != nil { return err @@ -162,7 +175,7 @@ func QueryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { } // Decode and print results - var out types.QueryCdpParams + var out types.Params cdc.MustUnmarshalJSON(res, &out) return cliCtx.PrintOutput(out) }, diff --git a/x/cdp/client/cli/tx.go b/x/cdp/client/cli/tx.go index 83d9ca94..9214aa1b 100644 --- a/x/cdp/client/cli/tx.go +++ b/x/cdp/client/cli/tx.go @@ -1,12 +1,16 @@ package cli import ( + "fmt" + "strings" + "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/version" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth/client/utils" @@ -34,17 +38,24 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { // GetCmdCreateCdp returns the command handler for creating a cdp func GetCmdCreateCdp(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "create [ownerAddress] [collateralChange] [debtChange]", + Use: "create [collateral] [debt]", Short: "create a new cdp", - Args: cobra.ExactArgs(3), + Long: strings.TrimSpace( + fmt.Sprintf(`Create a new cdp, depositing some collateral and drawing some debt. + +Example: +$ %s tx %s create 10000000uatom 1000usdx --from myKeyName +`, version.ClientName, types.ModuleName)), + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) - collateral, err := sdk.ParseCoins(args[1]) + + collateral, err := sdk.ParseCoins(args[0]) if err != nil { return err } - debt, err := sdk.ParseCoins(args[2]) + debt, err := sdk.ParseCoins(args[1]) if err != nil { return err } @@ -61,13 +72,20 @@ func GetCmdCreateCdp(cdc *codec.Codec) *cobra.Command { // GetCmdDeposit cli command for depositing to a cdp. func GetCmdDeposit(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "deposit [ownerAddress] [depositorAddress] [collateralChange]", - Short: "deposit to an existing cdp", - Args: cobra.ExactArgs(3), + Use: "deposit [owner-addr] [collateral]", + Short: "deposit collateral to an existing cdp", + Long: strings.TrimSpace( + fmt.Sprintf(`Add collateral to an existing cdp. + +Example: +$ %s tx %s deposit kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw 10000000uatom --from myKeyName +`, version.ClientName, types.ModuleName)), + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) - collateral, err := sdk.ParseCoins(args[2]) + + collateral, err := sdk.ParseCoins(args[1]) if err != nil { return err } @@ -75,11 +93,7 @@ func GetCmdDeposit(cdc *codec.Codec) *cobra.Command { if err != nil { return err } - depositor, err := sdk.AccAddressFromBech32(args[1]) - if err != nil { - return err - } - msg := types.NewMsgDeposit(owner, depositor, collateral) + msg := types.NewMsgDeposit(owner, cliCtx.GetFromAddress(), collateral) err = msg.ValidateBasic() if err != nil { return err @@ -92,13 +106,20 @@ func GetCmdDeposit(cdc *codec.Codec) *cobra.Command { // GetCmdWithdraw cli command for withdrawing from a cdp. func GetCmdWithdraw(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "withdraw [ownerAddress] [depositorAddress] [collateralChange]", - Short: "withdraw from an existing cdp", - Args: cobra.ExactArgs(3), + Use: "withdraw [owner-addr] [collateral]", + Short: "withdraw collateral from an existing cdp", + Long: strings.TrimSpace( + fmt.Sprintf(`Remove collateral from an existing cdp. + +Example: +$ %s tx %s withdraw kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw 10000000uatom --from myKeyName +`, version.ClientName, types.ModuleName)), + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) - collateral, err := sdk.ParseCoins(args[2]) + + collateral, err := sdk.ParseCoins(args[1]) if err != nil { return err } @@ -106,11 +127,7 @@ func GetCmdWithdraw(cdc *codec.Codec) *cobra.Command { if err != nil { return err } - depositor, err := sdk.AccAddressFromBech32(args[1]) - if err != nil { - return err - } - msg := types.NewMsgWithdraw(owner, depositor, collateral) + msg := types.NewMsgWithdraw(owner, cliCtx.GetFromAddress(), collateral) err = msg.ValidateBasic() if err != nil { return err @@ -123,21 +140,24 @@ func GetCmdWithdraw(cdc *codec.Codec) *cobra.Command { // GetCmdDraw cli command for depositing to a cdp. func GetCmdDraw(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "draw [ownerAddress] [collateralDenom] [debtChange]", + Use: "draw [collateral-name] [debt]", Short: "draw debt off an existing cdp", - Args: cobra.ExactArgs(3), + Long: strings.TrimSpace( + fmt.Sprintf(`Create debt in an existing cdp and send the newly minted asset to your account. + +Example: +$ %s tx %s draw uatom 1000usdx --from myKeyName +`, version.ClientName, types.ModuleName)), + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) - debt, err := sdk.ParseCoins(args[2]) + + debt, err := sdk.ParseCoins(args[1]) if err != nil { return err } - owner, err := sdk.AccAddressFromBech32(args[0]) - if err != nil { - return err - } - msg := types.NewMsgDrawDebt(owner, args[1], debt) + msg := types.NewMsgDrawDebt(cliCtx.GetFromAddress(), args[0], debt) err = msg.ValidateBasic() if err != nil { return err @@ -150,21 +170,24 @@ func GetCmdDraw(cdc *codec.Codec) *cobra.Command { // GetCmdRepay cli command for depositing to a cdp. func GetCmdRepay(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "repay [ownerAddress] [collateralDenom] [payment]", - Short: "repay debt from an existing cdp", - Args: cobra.ExactArgs(3), + Use: "repay [collateral-name] [debt]", + Short: "repay debt to an existing cdp", + Long: strings.TrimSpace( + fmt.Sprintf(`Cancel out debt in an existing cdp. + +Example: +$ %s tx %s repay uatom 1000usdx --from myKeyName +`, version.ClientName, types.ModuleName)), + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) - payment, err := sdk.ParseCoins(args[2]) + + payment, err := sdk.ParseCoins(args[1]) if err != nil { return err } - owner, err := sdk.AccAddressFromBech32(args[0]) - if err != nil { - return err - } - msg := types.NewMsgRepayDebt(owner, args[1], payment) + msg := types.NewMsgRepayDebt(cliCtx.GetFromAddress(), args[0], payment) err = msg.ValidateBasic() if err != nil { return err diff --git a/x/cdp/keeper/querier.go b/x/cdp/keeper/querier.go index ec83986a..dada19ca 100644 --- a/x/cdp/keeper/querier.go +++ b/x/cdp/keeper/querier.go @@ -1,8 +1,6 @@ package keeper import ( - "fmt" - abci "github.com/tendermint/tendermint/abci/types" "github.com/cosmos/cosmos-sdk/codec" @@ -33,7 +31,7 @@ func queryGetCdp(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, var requestParams types.QueryCdpParams err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams) if err != nil { - return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err)) + return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) } _, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom) @@ -59,7 +57,7 @@ func queryGetCdpsByRatio(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) var requestParams types.QueryCdpsByRatioParams err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams) if err != nil { - return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err)) + return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) } _, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom) if !valid { @@ -79,7 +77,7 @@ func queryGetCdpsByDenom(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) 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)) + return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) } _, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom) if !valid { diff --git a/x/cdp/types/params.go b/x/cdp/types/params.go index 58b9016a..ec0f94f9 100644 --- a/x/cdp/types/params.go +++ b/x/cdp/types/params.go @@ -126,7 +126,7 @@ func (dp DebtParam) String() string { Reference Asset: %s Debt Limit: %s Conversion Factor: %s - Debt Floot %s`, dp.Denom, dp.ReferenceAsset, dp.DebtLimit, dp.ConversionFactor, dp.DebtFloor) + Debt Floor %s`, dp.Denom, dp.ReferenceAsset, dp.DebtLimit, dp.ConversionFactor, dp.DebtFloor) } // DebtParams array of DebtParam diff --git a/x/cdp/types/querier.go b/x/cdp/types/querier.go index 368621e2..fa3c8ed9 100644 --- a/x/cdp/types/querier.go +++ b/x/cdp/types/querier.go @@ -27,6 +27,12 @@ func NewQueryCdpsParams(denom string) QueryCdpsParams { } } +// QueryCdpParams params for query /cdp/cdp +type QueryCdpParams struct { + CollateralDenom string // get CDPs with this collateral denom + Owner sdk.AccAddress // get CDPs belonging to this owner +} + // NewQueryCdpParams returns QueryCdpParams func NewQueryCdpParams(owner sdk.AccAddress, denom string) QueryCdpParams { return QueryCdpParams{ @@ -35,12 +41,6 @@ func NewQueryCdpParams(owner sdk.AccAddress, denom string) QueryCdpParams { } } -// QueryCdpParams params for query /cdp/cdp -type QueryCdpParams struct { - CollateralDenom string // get CDPs with this collateral denom - Owner sdk.AccAddress // get CDPs belonging to this owner -} - // QueryCdpsByRatioParams params for query /cdp/cdps/ratio type QueryCdpsByRatioParams struct { CollateralDenom string // get CDPs with this collateral denom From c5043ffabe366504bdd28b20b088664230a0710f Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Thu, 16 Jan 2020 16:21:38 +0100 Subject: [PATCH 61/99] add stability fee validation (#307) --- x/cdp/types/params.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x/cdp/types/params.go b/x/cdp/types/params.go index ec0f94f9..190fb97b 100644 --- a/x/cdp/types/params.go +++ b/x/cdp/types/params.go @@ -162,6 +162,7 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs { // Validate checks that the parameters have valid values. func (p Params) Validate() error { + // validate debt params debtDenoms := make(map[string]int) debtParamsDebtLimit := sdk.Coins{} for _, dp := range p.DebtParams { @@ -184,6 +185,7 @@ func (p Params) Validate() error { p.GlobalDebtLimit, debtParamsDebtLimit) } + // validate collateral params collateralDupMap := make(map[string]int) prefixDupMap := make(map[int]int) collateralParamsDebtLimit := sdk.Coins{} @@ -226,16 +228,19 @@ func (p Params) Validate() error { if !cp.AuctionSize.IsPositive() { return fmt.Errorf("auction size should be positive, is %s for %s", cp.AuctionSize, cp.Denom) } + if cp.StabilityFee.LT(sdk.OneDec()) { + return fmt.Errorf("stability fee must be ≥ 1.0, is %s for %s", cp.StabilityFee, cp.Denom) + } } if collateralParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) { return fmt.Errorf("collateral debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tcollateral debt limits: %s", p.GlobalDebtLimit, collateralParamsDebtLimit) } + // validate global params if p.GlobalDebtLimit.IsAnyNegative() { return fmt.Errorf("global debt limit should be positive for all debt tokens, is %s", p.GlobalDebtLimit) } - if !p.SurplusAuctionThreshold.IsPositive() { return fmt.Errorf("surplus auction threshold should be positive, is %s", p.SurplusAuctionThreshold) } From d286f530531a867e58cb4ed626f49946166bc55f Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Thu, 16 Jan 2020 18:52:29 +0100 Subject: [PATCH 62/99] R4R: cli auction clean up (#310) * kvcli q auction params * reset go.sum * Updated QueryGetAuctionsCmd naming to match conventions * clean up: tx auctions placebid * requested changes --- x/auction/client/cli/query.go | 36 +++++++++++++++++++++++++++++----- x/auction/client/cli/tx.go | 4 ++-- x/auction/client/rest/query.go | 20 ++++++++++++++++--- x/auction/keeper/querier.go | 15 ++++++++++++++ x/auction/types/querier.go | 3 ++- 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/x/auction/client/cli/query.go b/x/auction/client/cli/query.go index 3c5127fa..bb3d47a1 100644 --- a/x/auction/client/cli/query.go +++ b/x/auction/client/cli/query.go @@ -20,20 +20,21 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { } auctionQueryCmd.AddCommand(client.GetCommands( - GetCmdGetAuctions(queryRoute, cdc), + QueryGetAuctionsCmd(queryRoute, cdc), + QueryParamsCmd(queryRoute, cdc), )...) return auctionQueryCmd } -// GetCmdGetAuctions queries the auctions in the store -func GetCmdGetAuctions(queryRoute string, cdc *codec.Codec) *cobra.Command { +// QueryGetAuctionsCmd queries the auctions in the store +func QueryGetAuctionsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "getauctions", + Use: "auctions", 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) + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/auctions", queryRoute), nil) if err != nil { fmt.Printf("error when getting auctions - %s", err) return nil @@ -47,3 +48,28 @@ func GetCmdGetAuctions(queryRoute string, cdc *codec.Codec) *cobra.Command { }, } } + +// QueryParamsCmd queries the auction module parameters +func QueryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "params", + Short: "get the auction module parameters", + Long: "Get the current global auction 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) + if err != nil { + return err + } + + // Decode and print results + var out types.Params + cdc.MustUnmarshalJSON(res, &out) + return cliCtx.PrintOutput(out) + }, + } +} diff --git a/x/auction/client/cli/tx.go b/x/auction/client/cli/tx.go index e705eb74..b0adc7f0 100644 --- a/x/auction/client/cli/tx.go +++ b/x/auction/client/cli/tx.go @@ -30,10 +30,10 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { return auctionTxCmd } -// GetCmdPlaceBid cli command for creating and modifying cdps. +// GetCmdPlaceBid cli command for placing bids on auctions func GetCmdPlaceBid(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "placebid [auctionID] [amount]", + Use: "placebid [auction-id] [amount]", Short: "place a bid on an auction", Args: cobra.MinimumNArgs(2), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/x/auction/client/rest/query.go b/x/auction/client/rest/query.go index 87a21915..f64a80bb 100644 --- a/x/auction/client/rest/query.go +++ b/x/auction/client/rest/query.go @@ -8,13 +8,13 @@ import ( "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") + "github.com/kava-labs/kava/x/auction/types" +) func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { r.HandleFunc(fmt.Sprintf("/auction/getauctions"), queryGetAuctionsHandlerFn(cliCtx)).Methods("GET") - + r.HandleFunc("/auction/params", getParamsHandlerFn(cliCtx)).Methods("GET") } func queryGetAuctionsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { @@ -28,3 +28,17 @@ func queryGetAuctionsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { 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/auction/%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) + } +} diff --git a/x/auction/keeper/querier.go b/x/auction/keeper/querier.go index 9fe384c8..29bfebc9 100644 --- a/x/auction/keeper/querier.go +++ b/x/auction/keeper/querier.go @@ -13,6 +13,8 @@ func NewQuerier(keeper Keeper) sdk.Querier { switch path[0] { case types.QueryGetAuction: return queryAuctions(ctx, req, keeper) + case types.QueryGetParams: + return queryGetParams(ctx, req, keeper) default: return nil, sdk.ErrUnknownRequest("unknown auction query endpoint") } @@ -34,3 +36,16 @@ func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res [ return bz, nil } + +// query params in the auction store +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 +} diff --git a/x/auction/types/querier.go b/x/auction/types/querier.go index 34474b9a..038e9bb0 100644 --- a/x/auction/types/querier.go +++ b/x/auction/types/querier.go @@ -6,7 +6,8 @@ import ( const ( // QueryGetAuction command for getting the information about a particular auction - QueryGetAuction = "getauctions" + QueryGetAuction = "auctions" + QueryGetParams = "params" ) // QueryResAuctions Result Payload for an auctions query From dca59447aa9b25477abf9b3bdb3087fecff5845c Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Fri, 17 Jan 2020 13:24:43 +0100 Subject: [PATCH 63/99] fix: remove redundant debt limit param (#312) --- x/cdp/integration_test.go | 10 ---------- x/cdp/keeper/cdp.go | 18 ++++++++++++------ x/cdp/keeper/cdp_test.go | 5 ++++- x/cdp/keeper/draw.go | 5 +++++ x/cdp/keeper/integration_test.go | 3 --- x/cdp/types/deposit.go | 1 + x/cdp/types/params.go | 26 ++++++-------------------- 7 files changed, 28 insertions(+), 40 deletions(-) diff --git a/x/cdp/integration_test.go b/x/cdp/integration_test.go index 7cd40fcf..ef648956 100644 --- a/x/cdp/integration_test.go +++ b/x/cdp/integration_test.go @@ -59,7 +59,6 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { { Denom: "usdx", ReferenceAsset: "usd", - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), ConversionFactor: i(6), DebtFloor: i(10000000), }, @@ -133,14 +132,12 @@ func NewCDPGenStateMulti() app.GenesisState { { Denom: "usdx", ReferenceAsset: "usd", - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), ConversionFactor: i(6), DebtFloor: i(10000000), }, { Denom: "susd", ReferenceAsset: "usd", - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("susd", 1000000000000)), ConversionFactor: i(6), DebtFloor: i(10000000), }, @@ -189,14 +186,10 @@ func badGenStates() []badGenState { g6 := baseGenState() g6.Params.DebtParams[0].Denom = "susd" - g7 := baseGenState() - g7.Params.DebtParams[0].DebtLimit = sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000001)) - g8 := baseGenState() g8.Params.DebtParams = append(g8.Params.DebtParams, cdp.DebtParam{ Denom: "lol", ReferenceAsset: "usd", - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("lol", 1000000000000)), }) g9 := baseGenState() @@ -221,7 +214,6 @@ func badGenStates() []badGenState { badGenState{Genesis: g4, Reason: "single collateral exceeds debt limit"}, badGenState{Genesis: g5, Reason: "combined collateral exceeds debt limit"}, badGenState{Genesis: g6, Reason: "duplicate debt denom"}, - badGenState{Genesis: g7, Reason: "debt limit exceeds global debt limit"}, badGenState{Genesis: g8, Reason: "debt param not found in global debt limit"}, badGenState{Genesis: g9, Reason: "debt denom not set"}, badGenState{Genesis: g10, Reason: "previous block time not set"}, @@ -261,14 +253,12 @@ func baseGenState() cdp.GenesisState { { Denom: "usdx", ReferenceAsset: "usd", - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), ConversionFactor: i(6), DebtFloor: i(10000000), }, { Denom: "susd", ReferenceAsset: "usd", - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("susd", 1000000000000)), ConversionFactor: i(6), DebtFloor: i(10000000), }, diff --git a/x/cdp/keeper/cdp.go b/x/cdp/keeper/cdp.go index 335cae6a..7a342990 100644 --- a/x/cdp/keeper/cdp.go +++ b/x/cdp/keeper/cdp.go @@ -350,9 +350,6 @@ func (k Keeper) ValidatePrincipalAdd(ctx sdk.Context, principal sdk.Coins) sdk.E if !found { return types.ErrDebtNotSupported(k.codespace, dc.Denom) } - if sdk.NewCoins(dc).IsAnyGT(dp.DebtLimit) { - return types.ErrExceedsDebtLimit(k.codespace, sdk.NewCoins(dc), dp.DebtLimit) - } if dc.Amount.LT(dp.DebtFloor) { return types.ErrBelowDebtFloor(k.codespace, sdk.NewCoins(dc), dp.DebtFloor) } @@ -363,12 +360,21 @@ func (k Keeper) ValidatePrincipalAdd(ctx sdk.Context, principal sdk.Coins) sdk.E // ValidatePrincipalDraw validates that an asset is valid for use as debt when drawing debt off an existing cdp func (k Keeper) ValidatePrincipalDraw(ctx sdk.Context, principal sdk.Coins) sdk.Error { for _, dc := range principal { - dp, found := k.GetDebt(ctx, dc.Denom) + _, found := k.GetDebt(ctx, dc.Denom) if !found { return types.ErrDebtNotSupported(k.codespace, dc.Denom) } - if sdk.NewCoins(dc).IsAnyGT(dp.DebtLimit) { - return types.ErrExceedsDebtLimit(k.codespace, sdk.NewCoins(dc), dp.DebtLimit) + } + return nil +} + +// ValidateDebtLimit validates that the input debt amount does not exceed the global debt limit +func (k Keeper) ValidateDebtLimit(ctx sdk.Context, collateralDenom string, principal sdk.Coins) sdk.Error { + for _, dc := range principal { + totalPrincipal := k.GetTotalPrincipal(ctx, collateralDenom, dc.Denom).Add(dc.Amount) + globalLimit := k.GetParams(ctx).GlobalDebtLimit.AmountOf(dc.Denom) + if totalPrincipal.GT(globalLimit) { + return types.ErrExceedsDebtLimit(k.codespace, sdk.NewCoins(sdk.NewCoin(dc.Denom, totalPrincipal)), sdk.NewCoins(sdk.NewCoin(dc.Denom, globalLimit))) } } return nil diff --git a/x/cdp/keeper/cdp_test.go b/x/cdp/keeper/cdp_test.go index 26d93857..89e35136 100644 --- a/x/cdp/keeper/cdp_test.go +++ b/x/cdp/keeper/cdp_test.go @@ -256,8 +256,11 @@ func (suite *CdpTestSuite) TestValidatePrincipal() { err = suite.keeper.ValidatePrincipalAdd(suite.ctx, d) suite.Equal(types.CodeDebtNotSupported, err.Result().Code) d = sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(1000000000001))) - err = suite.keeper.ValidatePrincipalAdd(suite.ctx, d) + err = suite.keeper.ValidateDebtLimit(suite.ctx, "xrp", d) suite.Equal(types.CodeExceedsDebtLimit, err.Result().Code) + d = sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(100000000))) + err = suite.keeper.ValidateDebtLimit(suite.ctx, "xrp", d) + suite.NoError(err) } func (suite *CdpTestSuite) TestCalculateCollateralizationRatio() { diff --git a/x/cdp/keeper/draw.go b/x/cdp/keeper/draw.go index 1bc1ac66..f286e45a 100644 --- a/x/cdp/keeper/draw.go +++ b/x/cdp/keeper/draw.go @@ -19,6 +19,11 @@ func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom string return err } + err = k.ValidateDebtLimit(ctx, cdp.Collateral[0].Denom, principal) + if err != nil { + return err + } + // fee calculation periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix())) fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom) diff --git a/x/cdp/keeper/integration_test.go b/x/cdp/keeper/integration_test.go index 78e1c980..7b149f35 100644 --- a/x/cdp/keeper/integration_test.go +++ b/x/cdp/keeper/integration_test.go @@ -59,7 +59,6 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { { Denom: "usdx", ReferenceAsset: "usd", - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), ConversionFactor: i(6), DebtFloor: i(10000000), }, @@ -133,14 +132,12 @@ func NewCDPGenStateMulti() app.GenesisState { { Denom: "usdx", ReferenceAsset: "usd", - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("usdx", 1000000000000)), ConversionFactor: i(6), DebtFloor: i(10000000), }, { Denom: "susd", ReferenceAsset: "usd", - DebtLimit: sdk.NewCoins(sdk.NewInt64Coin("susd", 1000000000000)), ConversionFactor: i(6), DebtFloor: i(10000000), }, diff --git a/x/cdp/types/deposit.go b/x/cdp/types/deposit.go index 1cafcb59..4edd5fc8 100644 --- a/x/cdp/types/deposit.go +++ b/x/cdp/types/deposit.go @@ -51,6 +51,7 @@ func (d Deposit) Empty() bool { return d.Equals(Deposit{}) } +// SumCollateral returns the total amount of collateral in the input deposits func (ds Deposits) SumCollateral() (sum sdk.Int) { sum = sdk.ZeroInt() for _, d := range ds { diff --git a/x/cdp/types/params.go b/x/cdp/types/params.go index 190fb97b..5fb33325 100644 --- a/x/cdp/types/params.go +++ b/x/cdp/types/params.go @@ -113,20 +113,18 @@ func (cps CollateralParams) String() string { // DebtParam governance params for debt assets type DebtParam struct { - Denom string `json:"denom" yaml:"denom"` - ReferenceAsset string `json:"reference_asset" yaml:"reference_asset"` - DebtLimit sdk.Coins `json:"debt_limit" yaml:"debt_limit"` - ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` - DebtFloor sdk.Int `json:"debt_floor" yaml:"debt_floor"` // minimum active loan size, used to prevent dust + Denom string `json:"denom" yaml:"denom"` + ReferenceAsset string `json:"reference_asset" yaml:"reference_asset"` + ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` + DebtFloor sdk.Int `json:"debt_floor" yaml:"debt_floor"` // minimum active loan size, used to prevent dust } func (dp DebtParam) String() string { return fmt.Sprintf(`Debt: Denom: %s Reference Asset: %s - Debt Limit: %s Conversion Factor: %s - Debt Floor %s`, dp.Denom, dp.ReferenceAsset, dp.DebtLimit, dp.ConversionFactor, dp.DebtFloor) + Debt Floor %s`, dp.Denom, dp.ReferenceAsset, dp.ConversionFactor, dp.DebtFloor) } // DebtParams array of DebtParam @@ -164,25 +162,13 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs { func (p Params) Validate() error { // validate debt params debtDenoms := make(map[string]int) - debtParamsDebtLimit := sdk.Coins{} for _, dp := range p.DebtParams { _, found := debtDenoms[dp.Denom] if found { return fmt.Errorf("duplicate debt denom: %s", dp.Denom) } debtDenoms[dp.Denom] = 1 - if dp.DebtLimit.IsAnyNegative() { - return fmt.Errorf("debt limit for all debt tokens should be positive, is %s for %s", dp.DebtLimit, dp.Denom) - } - debtParamsDebtLimit = debtParamsDebtLimit.Add(dp.DebtLimit) - } - if !debtParamsDebtLimit.DenomsSubsetOf(p.GlobalDebtLimit) { - return fmt.Errorf("debt denom not found in global debt limit:\n\tglobal debt limit: %s\n\tdebt limits: %s", - p.GlobalDebtLimit, debtParamsDebtLimit) - } - if debtParamsDebtLimit.IsAnyGT(p.GlobalDebtLimit) { - return fmt.Errorf("debt limit exceeds global debt limit:\n\tglobal debt limit: %s\n\tdebt limits: %s", - p.GlobalDebtLimit, debtParamsDebtLimit) + } // validate collateral params From d04aad5cc9a8f889cef7ff71dc5d72ec14fd5710 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Fri, 17 Jan 2020 13:29:19 +0100 Subject: [PATCH 64/99] Pricefeed fixes (#308) * fix: remove redundant debt limit param * wip: test pricefeed genesis * fix: pricefeed querier * fix: comments, naming * fix: query path * fix: store methods * fix: query methods * fix: standardize genesis validation --- x/cdp/integration_test.go | 6 +- x/cdp/keeper/integration_test.go | 6 +- x/pricefeed/abci.go | 2 +- x/pricefeed/alias.go | 38 ++++---- x/pricefeed/client/cli/query.go | 82 +++++++++------- x/pricefeed/client/cli/tx.go | 7 +- x/pricefeed/client/rest/query.go | 57 ++++++++++++ x/pricefeed/client/rest/rest.go | 108 ++-------------------- x/pricefeed/client/rest/tx.go | 66 +++++++++++++ x/pricefeed/doc.go | 2 +- x/pricefeed/genesis.go | 26 +++--- x/pricefeed/genesis_test.go | 39 ++++++++ x/pricefeed/handler.go | 10 +- x/pricefeed/integration_test.go | 62 +++++++++++++ x/pricefeed/keeper/integration_test.go | 36 ++++++++ x/pricefeed/keeper/keeper.go | 36 ++++---- x/pricefeed/keeper/keeper_test.go | 14 +-- x/pricefeed/keeper/params.go | 40 ++++---- x/pricefeed/keeper/params_test.go | 49 ++++++++++ x/pricefeed/keeper/querier.go | 76 +++++++-------- x/pricefeed/module.go | 9 +- x/pricefeed/types/errors.go | 10 +- x/pricefeed/types/genesis.go | 14 +-- x/pricefeed/types/key.go | 4 +- x/pricefeed/types/{asset.go => market.go} | 38 ++------ x/pricefeed/types/msgs.go | 22 ++--- x/pricefeed/types/params.go | 42 ++++----- x/pricefeed/types/querier.go | 26 +----- 28 files changed, 559 insertions(+), 368 deletions(-) create mode 100644 x/pricefeed/client/rest/query.go create mode 100644 x/pricefeed/client/rest/tx.go create mode 100644 x/pricefeed/genesis_test.go create mode 100644 x/pricefeed/integration_test.go create mode 100644 x/pricefeed/keeper/integration_test.go create mode 100644 x/pricefeed/keeper/params_test.go rename x/pricefeed/types/{asset.go => market.go} (63%) diff --git a/x/cdp/integration_test.go b/x/cdp/integration_test.go index ef648956..bb429006 100644 --- a/x/cdp/integration_test.go +++ b/x/cdp/integration_test.go @@ -21,7 +21,7 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { pfGenesis := pricefeed.GenesisState{ Params: pricefeed.Params{ Markets: []pricefeed.Market{ - pricefeed.Market{MarketID: asset + ":usd", BaseAsset: asset, QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, + pricefeed.Market{MarketID: asset + ":usd", BaseAsset: asset, QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, }, }, PostedPrices: []pricefeed.PostedPrice{ @@ -77,8 +77,8 @@ func NewPricefeedGenStateMulti() app.GenesisState { pfGenesis := pricefeed.GenesisState{ Params: pricefeed.Params{ Markets: []pricefeed.Market{ - pricefeed.Market{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, - pricefeed.Market{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, + pricefeed.Market{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + pricefeed.Market{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, }, }, PostedPrices: []pricefeed.PostedPrice{ diff --git a/x/cdp/keeper/integration_test.go b/x/cdp/keeper/integration_test.go index 7b149f35..6905882a 100644 --- a/x/cdp/keeper/integration_test.go +++ b/x/cdp/keeper/integration_test.go @@ -21,7 +21,7 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState { pfGenesis := pricefeed.GenesisState{ Params: pricefeed.Params{ Markets: []pricefeed.Market{ - pricefeed.Market{MarketID: asset + ":usd", BaseAsset: asset, QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, + pricefeed.Market{MarketID: asset + ":usd", BaseAsset: asset, QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, }, }, PostedPrices: []pricefeed.PostedPrice{ @@ -77,8 +77,8 @@ func NewPricefeedGenStateMulti() app.GenesisState { pfGenesis := pricefeed.GenesisState{ Params: pricefeed.Params{ Markets: []pricefeed.Market{ - pricefeed.Market{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, - pricefeed.Market{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: pricefeed.Oracles{}, Active: true}, + pricefeed.Market{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + pricefeed.Market{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, }, }, PostedPrices: []pricefeed.PostedPrice{ diff --git a/x/pricefeed/abci.go b/x/pricefeed/abci.go index ac0ca068..1e56f918 100644 --- a/x/pricefeed/abci.go +++ b/x/pricefeed/abci.go @@ -9,7 +9,7 @@ import ( // EndBlocker updates the current pricefeed func EndBlocker(ctx sdk.Context, k Keeper) { // Update the current price of each asset. - for _, a := range k.GetMarketParams(ctx) { + for _, a := range k.GetMarkets(ctx) { if a.Active { err := k.SetCurrentPrices(ctx, a.MarketID) if err != nil { diff --git a/x/pricefeed/alias.go b/x/pricefeed/alias.go index 01a51ea7..e3fa62ac 100644 --- a/x/pricefeed/alias.go +++ b/x/pricefeed/alias.go @@ -26,12 +26,12 @@ const ( DefaultParamspace = types.DefaultParamspace RawPriceFeedPrefix = types.RawPriceFeedPrefix CurrentPricePrefix = types.CurrentPricePrefix - AssetPrefix = types.AssetPrefix + MarketPrefix = types.MarketPrefix OraclePrefix = types.OraclePrefix TypeMsgPostPrice = types.TypeMsgPostPrice QueryCurrentPrice = types.QueryCurrentPrice QueryRawPrices = types.QueryRawPrices - QueryAssets = types.QueryAssets + QueryMarkets = types.QueryMarkets ) var ( @@ -40,36 +40,32 @@ var ( ErrEmptyInput = types.ErrEmptyInput ErrExpired = types.ErrExpired ErrNoValidPrice = types.ErrNoValidPrice - ErrInvalidAsset = types.ErrInvalidAsset + ErrInvalidMarket = types.ErrInvalidMarket ErrInvalidOracle = types.ErrInvalidOracle NewGenesisState = types.NewGenesisState DefaultGenesisState = types.DefaultGenesisState - ValidateGenesis = types.ValidateGenesis NewMsgPostPrice = types.NewMsgPostPrice - ParamKeyTable = types.ParamKeyTable NewParams = types.NewParams DefaultParams = types.DefaultParams + ParamKeyTable = types.ParamKeyTable NewKeeper = keeper.NewKeeper NewQuerier = keeper.NewQuerier // variable aliases - ModuleCdc = types.ModuleCdc - KeyMarkets = types.KeyMarkets + ModuleCdc = types.ModuleCdc + KeyMarkets = types.KeyMarkets + DefaultMarkets = types.DefaultMarkets ) type ( - Market = types.Market - Markets = types.Markets - Oracle = types.Oracle - Oracles = types.Oracles - CurrentPrice = types.CurrentPrice - PostedPrice = types.PostedPrice - SortDecs = types.SortDecs - GenesisState = types.GenesisState - MsgPostPrice = types.MsgPostPrice - Params = types.Params - ParamSubspace = types.ParamSubspace - QueryRawPricesResp = types.QueryRawPricesResp - QueryAssetsResp = types.QueryAssetsResp - Keeper = keeper.Keeper + GenesisState = types.GenesisState + Market = types.Market + Markets = types.Markets + CurrentPrice = types.CurrentPrice + PostedPrice = types.PostedPrice + SortDecs = types.SortDecs + MsgPostPrice = types.MsgPostPrice + Params = types.Params + QueryPricesParams = types.QueryPricesParams + Keeper = keeper.Keeper ) diff --git a/x/pricefeed/client/cli/query.go b/x/pricefeed/client/cli/query.go index 3de15af3..0100a373 100644 --- a/x/pricefeed/client/cli/query.go +++ b/x/pricefeed/client/cli/query.go @@ -11,7 +11,7 @@ import ( ) // GetQueryCmd returns the cli query commands for this module -func GetQueryCmd(storeKey string, cdc *codec.Codec) *cobra.Command { +func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { // Group nameservice queries under a subcommand pricefeedQueryCmd := &cobra.Command{ Use: types.ModuleName, @@ -22,9 +22,9 @@ func GetQueryCmd(storeKey string, cdc *codec.Codec) *cobra.Command { } pricefeedQueryCmd.AddCommand(client.GetCommands( - GetCmdCurrentPrice(storeKey, cdc), - GetCmdRawPrices(storeKey, cdc), - GetCmdAssets(storeKey, cdc), + GetCmdCurrentPrice(queryRoute, cdc), + GetCmdRawPrices(queryRoute, cdc), + GetCmdMarkets(queryRoute, cdc), )...) return pricefeedQueryCmd @@ -33,21 +33,28 @@ func GetQueryCmd(storeKey string, cdc *codec.Codec) *cobra.Command { // 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", + Use: "price [marketID]", + Short: "get the current price for the input market", 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) + marketID := args[0] + + bz, err := cdc.MarshalJSON(types.QueryPricesParams{ + MarketID: marketID, + }) 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 + return err } - var out types.CurrentPrice - cdc.MustUnmarshalJSON(res, &out) - return cliCtx.PrintOutput(out) + route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryCurrentPrice) + + res, _, err := cliCtx.QueryWithData(route, bz) + if err != nil { + return err + } + var price types.CurrentPrice + cdc.MustUnmarshalJSON(res, &price) + return cliCtx.PrintOutput(price) }, } } @@ -55,39 +62,48 @@ func GetCmdCurrentPrice(queryRoute string, cdc *codec.Codec) *cobra.Command { // 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", + Use: "rawprices [marketID]", + Short: "get the raw oracle prices for the input market", 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) + marketID := args[0] + + bz, err := cdc.MarshalJSON(types.QueryPricesParams{ + MarketID: marketID, + }) if err != nil { - fmt.Printf("could not get raw prices for - %s \n", assetCode) - return nil + return err } - var out types.QueryRawPricesResp - cdc.MustUnmarshalJSON(res, &out) - return cliCtx.PrintOutput(out) + route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryRawPrices) + + res, _, err := cliCtx.QueryWithData(route, bz) + + if err != nil { + return err + } + var prices []types.PostedPrice + cdc.MustUnmarshalJSON(res, &prices) + return cliCtx.PrintOutput(prices) }, } } -// GetCmdAssets queries list of assets in the pricefeed -func GetCmdAssets(queryRoute string, cdc *codec.Codec) *cobra.Command { +// GetCmdMarkets queries list of markets in the pricefeed +func GetCmdMarkets(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "assets", - Short: "get the assets in the pricefeed", + Use: "markets", + Short: "get the markets 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) + route := fmt.Sprintf("custom/%s/markets", queryRoute) + res, _, err := cliCtx.QueryWithData(route, nil) if err != nil { - fmt.Printf("could not get assets") - return nil + return err } - var out types.QueryAssetsResp - cdc.MustUnmarshalJSON(res, &out) - return cliCtx.PrintOutput(out) + var markets types.Markets + cdc.MustUnmarshalJSON(res, &markets) + return cliCtx.PrintOutput(markets) }, } } diff --git a/x/pricefeed/client/cli/tx.go b/x/pricefeed/client/cli/tx.go index 199ad2a0..1793e59c 100644 --- a/x/pricefeed/client/cli/tx.go +++ b/x/pricefeed/client/cli/tx.go @@ -36,8 +36,8 @@ func GetTxCmd(storeKey string, cdc *codec.Codec) *cobra.Command { // 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", + Use: "postprice [marketID] [price] [expiry]", + Short: "post the latest price for a particular market", Args: cobra.ExactArgs(3), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) @@ -51,8 +51,7 @@ func GetCmdPostPrice(cdc *codec.Codec) *cobra.Command { } expiryInt, ok := sdk.NewIntFromString(args[2]) if !ok { - fmt.Printf("invalid expiry - %s \n", args[2]) - return nil + return fmt.Errorf("invalid expiry - %s", args[2]) } expiry := tmtime.Canonical(time.Unix(expiryInt.Int64(), 0)) diff --git a/x/pricefeed/client/rest/query.go b/x/pricefeed/client/rest/query.go new file mode 100644 index 00000000..30b2af46 --- /dev/null +++ b/x/pricefeed/client/rest/query.go @@ -0,0 +1,57 @@ +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/pricefeed/types" +) + +// define routes that get registered by the main application +func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { + r.HandleFunc(fmt.Sprintf("/%s/rawprices/{%s}", types.ModuleName, restName), queryRawPricesHandler(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/currentprice/{%s}", types.ModuleName, restName), queryCurrentPriceHandler(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/markets", types.ModuleName), queryMarketsHandler(cliCtx)).Methods("GET") +} + +func queryRawPricesHandler(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + paramType := vars[restName] + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/rawprices/%s", paramType), nil) + if err != nil { + rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) + return + } + rest.PostProcessResponse(w, cliCtx, res) + } +} + +func queryCurrentPriceHandler(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + paramType := vars[restName] + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/price/%s", paramType), nil) + if err != nil { + rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) + return + } + rest.PostProcessResponse(w, cliCtx, res) + } +} + +func queryMarketsHandler(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/markets/"), nil) + if err != nil { + rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) + return + } + rest.PostProcessResponse(w, cliCtx, res) + } +} diff --git a/x/pricefeed/client/rest/rest.go b/x/pricefeed/client/rest/rest.go index 988d8ea5..4e24a5fd 100644 --- a/x/pricefeed/client/rest/rest.go +++ b/x/pricefeed/client/rest/rest.go @@ -1,116 +1,24 @@ package rest import ( - "fmt" - "net/http" - "time" - "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" - tmtime "github.com/tendermint/tendermint/types/time" ) const ( - restName = "assetCode" + restName = "marketID" ) type postPriceReq struct { - BaseReq rest.BaseReq `json:"base_req"` - AssetCode string `json:"asset_code"` - Price string `json:"price"` - Expiry string `json:"expiry"` + BaseReq rest.BaseReq `json:"base_req"` + MarketID string `json:"market_id"` + 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 - } - - expiryInt, ok := sdk.NewIntFromString(req.Expiry) - if !ok { - rest.WriteErrorResponse(w, http.StatusBadRequest, "invalid expiry") - return - } - expiry := tmtime.Canonical(time.Unix(expiryInt.Int64(), 0)) - - // 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) - } +func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { + registerQueryRoutes(cliCtx, r) + registerTxRoutes(cliCtx, r) } diff --git a/x/pricefeed/client/rest/tx.go b/x/pricefeed/client/rest/tx.go new file mode 100644 index 00000000..6d72a972 --- /dev/null +++ b/x/pricefeed/client/rest/tx.go @@ -0,0 +1,66 @@ +package rest + +import ( + "fmt" + "net/http" + "time" + + "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/pricefeed/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) { + r.HandleFunc(fmt.Sprintf("/%s/rawprices", types.ModuleName), postPriceHandler(cliCtx)).Methods("PUT") +} + +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 + } + + expiryInt, ok := sdk.NewIntFromString(req.Expiry) + if !ok { + rest.WriteErrorResponse(w, http.StatusBadRequest, "invalid expiry") + return + } + expiry := tmtime.Canonical(time.Unix(expiryInt.Int64(), 0)) + + // create the message + msg := types.NewMsgPostPrice(addr, req.MarketID, price, expiry) + err = msg.ValidateBasic() + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg}) + } + +} \ No newline at end of file diff --git a/x/pricefeed/doc.go b/x/pricefeed/doc.go index 0d403145..e25ab6b0 100644 --- a/x/pricefeed/doc.go +++ b/x/pricefeed/doc.go @@ -1,6 +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 allows a group of white-listed oracles to post price information of specific markets that are tracked by the system. For each market, the module computes the median of all posted prices by white-listed oracles and takes that as the current price value. */ package pricefeed diff --git a/x/pricefeed/genesis.go b/x/pricefeed/genesis.go index b80fe9a5..158d84e6 100644 --- a/x/pricefeed/genesis.go +++ b/x/pricefeed/genesis.go @@ -5,23 +5,27 @@ import ( ) // InitGenesis sets distribution information for genesis. -func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { - - // Set the assets and oracles from params - keeper.SetParams(ctx, data.Params) +func InitGenesis(ctx sdk.Context, keeper Keeper, gs GenesisState) { + err := gs.Validate() + if err != nil { + panic(err) + } + // Set the markets and oracles from params + keeper.SetParams(ctx, gs.Params) // Iterate through the posted prices and set them in the store - for _, pp := range data.PostedPrices { + for _, pp := range gs.PostedPrices { _, err := keeper.SetPrice(ctx, pp.OracleAddress, pp.MarketID, pp.Price, pp.Expiry) if err != nil { panic(err) } } + params := keeper.GetParams(ctx) // Set the current price (if any) based on what's now in the store - for _, a := range data.Params.Markets { - if a.Active { - err := keeper.SetCurrentPrices(ctx, a.MarketID) + for _, market := range params.Markets { + if market.Active { + err := keeper.SetCurrentPrices(ctx, market.MarketID) if err != nil { panic(err) } @@ -32,12 +36,12 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { // 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 + // Get the params for markets and oracles params := keeper.GetParams(ctx) var postedPrices []PostedPrice - for _, asset := range keeper.GetMarketParams(ctx) { - pp := keeper.GetRawPrices(ctx, asset.MarketID) + for _, market := range keeper.GetMarkets(ctx) { + pp := keeper.GetRawPrices(ctx, market.MarketID) postedPrices = append(postedPrices, pp...) } diff --git a/x/pricefeed/genesis_test.go b/x/pricefeed/genesis_test.go new file mode 100644 index 00000000..968729bc --- /dev/null +++ b/x/pricefeed/genesis_test.go @@ -0,0 +1,39 @@ +package pricefeed_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/pricefeed" + + "github.com/stretchr/testify/suite" +) + +type GenesisTestSuite struct { + suite.Suite + + ctx sdk.Context + keeper pricefeed.Keeper +} + +func (suite *GenesisTestSuite) TestValidGenState() { + tApp := app.NewTestApp() + + suite.NotPanics(func() { + tApp.InitializeFromGenesisStates( + NewPricefeedGenStateMulti(), + ) + }) + _, addrs := app.GeneratePrivKeyAddressPairs(10) + + suite.NotPanics(func() { + tApp.InitializeFromGenesisStates( + NewPricefeedGenStateWithOracles(addrs), + ) + }) +} + +func TestGenesisTestSuite(t *testing.T) { + suite.Run(t, new(GenesisTestSuite)) +} diff --git a/x/pricefeed/handler.go b/x/pricefeed/handler.go index 704839f9..0c738a65 100644 --- a/x/pricefeed/handler.go +++ b/x/pricefeed/handler.go @@ -28,11 +28,13 @@ func HandleMsgPostPrice( k Keeper, msg MsgPostPrice) sdk.Result { - // TODO cleanup message validation and errors - _, err := k.GetOracle(ctx, msg.AssetCode, msg.From) + _, err := k.GetOracle(ctx, msg.MarketID, msg.From) if err != nil { - return ErrInvalidOracle(k.Codespace()).Result() + return err.Result() + } + _, err = k.SetPrice(ctx, msg.From, msg.MarketID, msg.Price, msg.Expiry) + if err != nil { + err.Result() } - k.SetPrice(ctx, msg.From, msg.AssetCode, msg.Price, msg.Expiry) return sdk.Result{} } diff --git a/x/pricefeed/integration_test.go b/x/pricefeed/integration_test.go new file mode 100644 index 00000000..4d5f2c76 --- /dev/null +++ b/x/pricefeed/integration_test.go @@ -0,0 +1,62 @@ +package pricefeed_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/pricefeed" +) + +func NewPricefeedGenStateMulti() app.GenesisState { + pfGenesis := pricefeed.GenesisState{ + Params: pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + pricefeed.Market{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + }, + }, + PostedPrices: []pricefeed.PostedPrice{ + pricefeed.PostedPrice{ + MarketID: "btc:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("8000.00"), + Expiry: time.Now().Add(1 * time.Hour), + }, + pricefeed.PostedPrice{ + MarketID: "xrp:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("0.25"), + Expiry: time.Now().Add(1 * time.Hour), + }, + }, + } + return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)} +} + +func NewPricefeedGenStateWithOracles(addrs []sdk.AccAddress) app.GenesisState { + pfGenesis := pricefeed.GenesisState{ + Params: pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: addrs, Active: true}, + pricefeed.Market{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: addrs, Active: true}, + }, + }, + PostedPrices: []pricefeed.PostedPrice{ + pricefeed.PostedPrice{ + MarketID: "btc:usd", + OracleAddress: addrs[0], + Price: sdk.MustNewDecFromStr("8000.00"), + Expiry: time.Now().Add(1 * time.Hour), + }, + pricefeed.PostedPrice{ + MarketID: "xrp:usd", + OracleAddress: addrs[0], + Price: sdk.MustNewDecFromStr("0.25"), + Expiry: time.Now().Add(1 * time.Hour), + }, + }, + } + return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)} +} diff --git a/x/pricefeed/keeper/integration_test.go b/x/pricefeed/keeper/integration_test.go new file mode 100644 index 00000000..5594cda8 --- /dev/null +++ b/x/pricefeed/keeper/integration_test.go @@ -0,0 +1,36 @@ +package keeper_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/pricefeed" +) + +func NewPricefeedGenStateMulti() app.GenesisState { + pfGenesis := pricefeed.GenesisState{ + Params: pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + pricefeed.Market{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + }, + }, + PostedPrices: []pricefeed.PostedPrice{ + pricefeed.PostedPrice{ + MarketID: "btc:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("8000.00"), + Expiry: time.Now().Add(1 * time.Hour), + }, + pricefeed.PostedPrice{ + MarketID: "xrp:usd", + OracleAddress: sdk.AccAddress{}, + Price: sdk.MustNewDecFromStr("0.25"), + Expiry: time.Now().Add(1 * time.Hour), + }, + }, + } + return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)} +} diff --git a/x/pricefeed/keeper/keeper.go b/x/pricefeed/keeper/keeper.go index ca76e5ae..10c8b410 100644 --- a/x/pricefeed/keeper/keeper.go +++ b/x/pricefeed/keeper/keeper.go @@ -6,34 +6,32 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/params" + "github.com/cosmos/cosmos-sdk/x/params/subspace" "github.com/kava-labs/kava/x/pricefeed/types" ) // Keeper struct for pricefeed module type Keeper struct { - // The keys used to access the stores from Context - storeKey sdk.StoreKey + // key used to access the stores from Context + key sdk.StoreKey // Codec for binary encoding/decoding cdc *codec.Codec // The reference to the Paramstore to get and set pricefeed specific params - paramstore params.Subspace + paramSubspace subspace.Subspace // Reserved codespace codespace sdk.CodespaceType } -// NewKeeper returns a new keeper for the pricefeed module. It handles: -// - adding oracles -// - adding/removing assets from the pricefeed +// NewKeeper returns a new keeper for the pricefeed module. func NewKeeper( - cdc *codec.Codec, storeKey sdk.StoreKey, paramstore params.Subspace, codespace sdk.CodespaceType, + cdc *codec.Codec, key sdk.StoreKey, paramSubspace subspace.Subspace, codespace sdk.CodespaceType, ) Keeper { return Keeper{ - paramstore: paramstore.WithKeyTable(types.ParamKeyTable()), - storeKey: storeKey, - cdc: cdc, - codespace: codespace, + paramSubspace: paramSubspace.WithKeyTable(types.ParamKeyTable()), + key: key, + cdc: cdc, + codespace: codespace, } } @@ -46,7 +44,7 @@ func (k Keeper) SetPrice( expiry time.Time) (types.PostedPrice, sdk.Error) { // If the expiry is less than or equal to the current blockheight, we consider the price valid if expiry.After(ctx.BlockTime()) { - store := ctx.KVStore(k.storeKey) + store := ctx.KVStore(k.key) prices := k.GetRawPrices(ctx, marketID) var index int found := false @@ -82,7 +80,7 @@ func (k Keeper) SetPrice( func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) sdk.Error { _, ok := k.GetMarket(ctx, marketID) if !ok { - return types.ErrInvalidAsset(k.codespace) + return types.ErrInvalidMarket(k.codespace, marketID) } prices := k.GetRawPrices(ctx, marketID) var notExpiredPrices []types.CurrentPrice @@ -96,7 +94,7 @@ func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) sdk.Error { } } if len(notExpiredPrices) == 0 { - store := ctx.KVStore(k.storeKey) + store := ctx.KVStore(k.key) store.Set( []byte(types.CurrentPricePrefix+marketID), k.cdc.MustMarshalBinaryBare(types.CurrentPrice{}), ) @@ -104,7 +102,7 @@ func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) sdk.Error { } medianPrice := k.CalculateMedianPrice(ctx, notExpiredPrices) - store := ctx.KVStore(k.storeKey) + store := ctx.KVStore(k.key) currentPrice := types.CurrentPrice{ MarketID: marketID, Price: medianPrice, @@ -144,9 +142,9 @@ func (k Keeper) calculateMeanPrice(ctx sdk.Context, prices []types.CurrentPrice) return mean } -// GetCurrentPrice fetches the current median price of all oracles for a specific asset +// GetCurrentPrice fetches the current median price of all oracles for a specific market func (k Keeper) GetCurrentPrice(ctx sdk.Context, marketID string) (types.CurrentPrice, sdk.Error) { - store := ctx.KVStore(k.storeKey) + store := ctx.KVStore(k.key) bz := store.Get([]byte(types.CurrentPricePrefix + marketID)) if bz == nil { @@ -162,7 +160,7 @@ func (k Keeper) GetCurrentPrice(ctx sdk.Context, marketID string) (types.Current // GetRawPrices fetches the set of all prices posted by oracles for an asset func (k Keeper) GetRawPrices(ctx sdk.Context, marketID string) []types.PostedPrice { - store := ctx.KVStore(k.storeKey) + store := ctx.KVStore(k.key) bz := store.Get([]byte(types.RawPriceFeedPrefix + marketID)) var prices []types.PostedPrice k.cdc.MustUnmarshalBinaryBare(bz, &prices) diff --git a/x/pricefeed/keeper/keeper_test.go b/x/pricefeed/keeper/keeper_test.go index d29298a3..0f79b026 100644 --- a/x/pricefeed/keeper/keeper_test.go +++ b/x/pricefeed/keeper/keeper_test.go @@ -20,11 +20,11 @@ func TestKeeper_SetGetMarket(t *testing.T) { mp := types.Params{ Markets: types.Markets{ - types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, }, } keeper.SetParams(ctx, mp) - markets := keeper.GetMarketParams(ctx) + markets := keeper.GetMarkets(ctx) require.Equal(t, len(markets), 1) require.Equal(t, markets[0].MarketID, "tstusd") @@ -33,12 +33,12 @@ func TestKeeper_SetGetMarket(t *testing.T) { mp = types.Params{ Markets: types.Markets{ - types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, - types.Market{MarketID: "tst2usd", BaseAsset: "tst2", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + types.Market{MarketID: "tst2usd", BaseAsset: "tst2", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, }, } keeper.SetParams(ctx, mp) - markets = keeper.GetMarketParams(ctx) + markets = keeper.GetMarkets(ctx) require.Equal(t, len(markets), 2) require.Equal(t, markets[0].MarketID, "tstusd") require.Equal(t, markets[1].MarketID, "tst2usd") @@ -56,7 +56,7 @@ func TestKeeper_GetSetPrice(t *testing.T) { mp := types.Params{ Markets: types.Markets{ - types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, }, } keeper.SetParams(ctx, mp) @@ -100,7 +100,7 @@ func TestKeeper_GetSetCurrentPrice(t *testing.T) { mp := types.Params{ Markets: types.Markets{ - types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: types.Oracles{}, Active: true}, + types.Market{MarketID: "tstusd", BaseAsset: "tst", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, }, } keeper.SetParams(ctx, mp) diff --git a/x/pricefeed/keeper/params.go b/x/pricefeed/keeper/params.go index aa64cca1..8b297f57 100644 --- a/x/pricefeed/keeper/params.go +++ b/x/pricefeed/keeper/params.go @@ -1,58 +1,56 @@ package keeper import ( - "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/x/pricefeed/types" ) -// GetParams gets params from the store +// GetParams returns the params from the store func (k Keeper) GetParams(ctx sdk.Context) types.Params { - return types.NewParams(k.GetMarketParams(ctx)) + var p types.Params + k.paramSubspace.GetParamSet(ctx, &p) + return p } -// SetParams updates params in the store +// SetParams sets params on the store func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { - k.paramstore.SetParamSet(ctx, ¶ms) + k.paramSubspace.SetParamSet(ctx, ¶ms) } -// GetMarketParams get asset params from store -func (k Keeper) GetMarketParams(ctx sdk.Context) types.Markets { - var markets types.Markets - k.paramstore.Get(ctx, types.KeyMarkets, &markets) - return markets +// GetMarkets returns the markets from params +func (k Keeper) GetMarkets(ctx sdk.Context) types.Markets { + return k.GetParams(ctx).Markets } // GetOracles returns the oracles in the pricefeed store -func (k Keeper) GetOracles(ctx sdk.Context, marketID string) (types.Oracles, error) { +func (k Keeper) GetOracles(ctx sdk.Context, marketID string) ([]sdk.AccAddress, sdk.Error) { - for _, m := range k.GetMarketParams(ctx) { + for _, m := range k.GetMarkets(ctx) { if marketID == m.MarketID { return m.Oracles, nil } } - return types.Oracles{}, fmt.Errorf("asset %s not found", marketID) + return []sdk.AccAddress{}, types.ErrInvalidMarket(k.Codespace(), marketID) } // GetOracle returns the oracle from the store or an error if not found -func (k Keeper) GetOracle(ctx sdk.Context, marketID string, address sdk.AccAddress) (types.Oracle, error) { +func (k Keeper) GetOracle(ctx sdk.Context, marketID string, address sdk.AccAddress) (sdk.AccAddress, sdk.Error) { oracles, err := k.GetOracles(ctx, marketID) if err != nil { - return types.Oracle{}, fmt.Errorf("asset %s not found", marketID) + return sdk.AccAddress{}, types.ErrInvalidMarket(k.Codespace(), marketID) } - for _, o := range oracles { - if address.Equals(o.Address) { - return o, nil + for _, addr := range oracles { + if address.Equals(addr) { + return addr, nil } } - return types.Oracle{}, fmt.Errorf("oracle %s not found for asset %s", address, marketID) + return sdk.AccAddress{}, types.ErrInvalidOracle(k.codespace, address) } // GetMarket returns the market if it is in the pricefeed system func (k Keeper) GetMarket(ctx sdk.Context, marketID string) (types.Market, bool) { - markets := k.GetMarketParams(ctx) + markets := k.GetMarkets(ctx) for i := range markets { if markets[i].MarketID == marketID { diff --git a/x/pricefeed/keeper/params_test.go b/x/pricefeed/keeper/params_test.go new file mode 100644 index 00000000..d4ccc483 --- /dev/null +++ b/x/pricefeed/keeper/params_test.go @@ -0,0 +1,49 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/pricefeed/keeper" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type KeeperTestSuite struct { + suite.Suite + + keeper keeper.Keeper + addrs []sdk.AccAddress + app app.TestApp + ctx sdk.Context +} + +func (suite *KeeperTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + _, addrs := app.GeneratePrivKeyAddressPairs(10) + tApp.InitializeFromGenesisStates( + NewPricefeedGenStateMulti(), + ) + suite.keeper = tApp.GetPriceFeedKeeper() + suite.ctx = ctx + suite.addrs = addrs +} + +func (suite *KeeperTestSuite) TestGetSetOracles() { + params := suite.keeper.GetParams(suite.ctx) + suite.Equal([]sdk.AccAddress(nil), params.Markets[0].Oracles) + params.Markets[0].Oracles = suite.addrs + suite.NotPanics(func() { suite.keeper.SetParams(suite.ctx, params) }) + params = suite.keeper.GetParams(suite.ctx) + suite.Equal(suite.addrs, params.Markets[0].Oracles) + addr, err := suite.keeper.GetOracle(suite.ctx, params.Markets[0].MarketID, suite.addrs[0]) + suite.NoError(err) + suite.Equal(suite.addrs[0], addr) +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} diff --git a/x/pricefeed/keeper/querier.go b/x/pricefeed/keeper/querier.go index 022011af..7952e688 100644 --- a/x/pricefeed/keeper/querier.go +++ b/x/pricefeed/keeper/querier.go @@ -1,6 +1,8 @@ package keeper import ( + "fmt" + "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -14,11 +16,11 @@ 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.QueryCurrentPrice: - return queryCurrentPrice(ctx, path[1:], req, keeper) + return queryCurrentPrice(ctx, req, keeper) case types.QueryRawPrices: - return queryRawPrices(ctx, path[1:], req, keeper) - case types.QueryAssets: - return queryAssets(ctx, req, keeper) + return queryRawPrices(ctx, req, keeper) + case types.QueryMarkets: + return queryMarkets(ctx, req, keeper) default: return nil, sdk.ErrUnknownRequest("unknown pricefeed query endpoint") } @@ -26,51 +28,53 @@ func NewQuerier(keeper Keeper) sdk.Querier { } -func queryCurrentPrice(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { - marketID := path[0] - _, found := keeper.GetMarket(ctx, marketID) - if !found { - return []byte{}, sdk.ErrUnknownRequest("asset not found") - } - currentPrice, err := keeper.GetCurrentPrice(ctx, marketID) +func queryCurrentPrice(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, sdkErr sdk.Error) { + var requestParams types.QueryPricesParams + err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams) if err != nil { - return nil, err + return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err)) } - 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 types.QueryRawPricesResp - marketID := path[0] - _, found := keeper.GetMarket(ctx, marketID) + _, found := keeper.GetMarket(ctx, requestParams.MarketID) if !found { return []byte{}, sdk.ErrUnknownRequest("asset not found") } - rawPrices := keeper.GetRawPrices(ctx, marketID) - for _, price := range rawPrices { - priceList = append(priceList, price.String()) + currentPrice, sdkErr := keeper.GetCurrentPrice(ctx, requestParams.MarketID) + if sdkErr != nil { + return nil, sdkErr } - bz, err2 := codec.MarshalJSONIndent(keeper.cdc, priceList) - if err2 != nil { + bz, err := codec.MarshalJSONIndent(keeper.cdc, currentPrice) + if err != 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 types.QueryAssetsResp - assets := keeper.GetMarketParams(ctx) - for _, asset := range assets { - assetList = append(assetList, asset.String()) +func queryRawPrices(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, sdkErr sdk.Error) { + var requestParams types.QueryPricesParams + err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams) + if err != nil { + return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err)) } - bz, err2 := codec.MarshalJSONIndent(keeper.cdc, assetList) - if err2 != nil { + _, found := keeper.GetMarket(ctx, requestParams.MarketID) + if !found { + return []byte{}, sdk.ErrUnknownRequest("asset not found") + } + rawPrices := keeper.GetRawPrices(ctx, requestParams.MarketID) + + bz, err := codec.MarshalJSONIndent(keeper.cdc, rawPrices) + if err != nil { + panic("could not marshal result to JSON") + } + + return bz, nil +} + +func queryMarkets(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, sdkErr sdk.Error) { + markets := keeper.GetMarkets(ctx) + + bz, err := codec.MarshalJSONIndent(keeper.cdc, markets) + if err != nil { panic("could not marshal result to JSON") } diff --git a/x/pricefeed/module.go b/x/pricefeed/module.go index 7b52be5a..e4136b14 100644 --- a/x/pricefeed/module.go +++ b/x/pricefeed/module.go @@ -20,6 +20,7 @@ var ( _ module.AppModule = AppModule{} _ module.AppModuleBasic = AppModuleBasic{} ) + // AppModuleBasic app module basics object type AppModuleBasic struct{} @@ -40,17 +41,17 @@ func (AppModuleBasic) DefaultGenesis() json.RawMessage { // ValidateGenesis module validate genesis func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { - var data GenesisState - err := ModuleCdc.UnmarshalJSON(bz, &data) + var gs GenesisState + err := ModuleCdc.UnmarshalJSON(bz, &gs) if err != nil { return err } - return ValidateGenesis(data) + return gs.Validate() } // RegisterRESTRoutes register rest routes func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { - rest.RegisterRoutes(ctx, rtr, StoreKey) + rest.RegisterRoutes(ctx, rtr) } // GetTxCmd get the root tx command of this module diff --git a/x/pricefeed/types/errors.go b/x/pricefeed/types/errors.go index 7a7c4548..325b0c1b 100644 --- a/x/pricefeed/types/errors.go +++ b/x/pricefeed/types/errors.go @@ -37,12 +37,12 @@ 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.")) +// ErrInvalidAsset Error constructor for posted price messages for invalid markets +func ErrInvalidMarket(codespace sdk.CodespaceType, marketId string) sdk.Error { + return sdk.NewError(codespace, CodeInvalidAsset, fmt.Sprintf("market %s does not exist", marketId)) } // 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.")) +func ErrInvalidOracle(codespace sdk.CodespaceType, addr sdk.AccAddress) sdk.Error { + return sdk.NewError(codespace, CodeInvalidOracle, fmt.Sprintf("oracle %s does not exist or not authorized", addr)) } diff --git a/x/pricefeed/types/genesis.go b/x/pricefeed/types/genesis.go index 4bcba82c..eb8ba380 100644 --- a/x/pricefeed/types/genesis.go +++ b/x/pricefeed/types/genesis.go @@ -27,22 +27,22 @@ func DefaultGenesisState() GenesisState { } // Equal checks whether two gov GenesisState structs are equivalent -func (data GenesisState) Equal(data2 GenesisState) bool { - b1 := ModuleCdc.MustMarshalBinaryBare(data) - b2 := ModuleCdc.MustMarshalBinaryBare(data2) +func (gs GenesisState) Equal(gs2 GenesisState) bool { + b1 := ModuleCdc.MustMarshalBinaryBare(gs) + b2 := ModuleCdc.MustMarshalBinaryBare(gs2) return bytes.Equal(b1, b2) } // IsEmpty returns true if a GenesisState is empty -func (data GenesisState) IsEmpty() bool { - return data.Equal(GenesisState{}) +func (gs GenesisState) IsEmpty() bool { + return gs.Equal(GenesisState{}) } // ValidateGenesis performs basic validation of genesis data returning an // error for any failed validation criteria. -func ValidateGenesis(data GenesisState) error { +func (gs GenesisState) Validate() error { - if err := data.Params.Validate(); err != nil { + if err := gs.Params.Validate(); err != nil { return err } return nil diff --git a/x/pricefeed/types/key.go b/x/pricefeed/types/key.go index 4a20c25a..a1107954 100644 --- a/x/pricefeed/types/key.go +++ b/x/pricefeed/types/key.go @@ -22,8 +22,8 @@ const ( // CurrentPricePrefix prefix for the current price of an asset CurrentPricePrefix = StoreKey + ":currentprice:" - // AssetPrefix Prefix for the assets in the pricefeed system - AssetPrefix = StoreKey + ":assets" + // MarketPrefix Prefix for the assets in the pricefeed system + MarketPrefix = StoreKey + ":markets" // OraclePrefix store prefix for the oracle accounts OraclePrefix = StoreKey + ":oracles" diff --git a/x/pricefeed/types/asset.go b/x/pricefeed/types/market.go similarity index 63% rename from x/pricefeed/types/asset.go rename to x/pricefeed/types/market.go index bc19b532..a357db73 100644 --- a/x/pricefeed/types/asset.go +++ b/x/pricefeed/types/market.go @@ -8,13 +8,13 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// Market struct that represents an asset in the pricefeed +// Market an asset in the pricefeed type Market struct { - MarketID string `json:"market_id" yaml:"market_id"` - BaseAsset string `json:"base_asset" yaml:"base_asset"` - QuoteAsset string `json:"quote_asset" yaml:"quote_asset"` - Oracles Oracles `json:"oracles" yaml:"oracles"` - Active bool `json:"active" yaml:"active"` + MarketID string `json:"market_id" yaml:"market_id"` + BaseAsset string `json:"base_asset" yaml:"base_asset"` + QuoteAsset string `json:"quote_asset" yaml:"quote_asset"` + Oracles []sdk.AccAddress `json:"oracles" yaml:"oracles"` + Active bool `json:"active" yaml:"active"` } // String implement fmt.Stringer @@ -40,35 +40,13 @@ func (ms Markets) String() string { return strings.TrimSpace(out) } -// Oracle struct that documents which address an oracle is using -type Oracle struct { - Address sdk.AccAddress `json:"address" yaml:"address"` -} - -// String implements fmt.Stringer -func (o Oracle) String() string { - return fmt.Sprintf(`Address: %s`, o.Address) -} - -// Oracles array type for oracle -type Oracles []Oracle - -// String implements fmt.Stringer -func (os Oracles) String() string { - out := "Oracles:\n" - for _, o := range os { - out += fmt.Sprintf("%s\n", o.String()) - } - return strings.TrimSpace(out) -} - -// CurrentPrice struct that contains the metadata of a current price for a particular asset in the pricefeed module. +// CurrentPrice struct that contains the metadata of a current price for a particular market in the pricefeed module. type CurrentPrice struct { MarketID string `json:"market_id" yaml:"market_id"` Price sdk.Dec `json:"price" yaml:"price"` } -// PostedPrice struct represented a price for an asset posted by a specific oracle +// PostedPrice price for market posted by a specific oracle type PostedPrice struct { MarketID string `json:"market_id" yaml:"market_id"` OracleAddress sdk.AccAddress `json:"oracle_address" yaml:"oracle_address"` diff --git a/x/pricefeed/types/msgs.go b/x/pricefeed/types/msgs.go index eed227bc..e73e32dd 100644 --- a/x/pricefeed/types/msgs.go +++ b/x/pricefeed/types/msgs.go @@ -14,10 +14,10 @@ const ( // 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 time.Time // expiry time + From sdk.AccAddress // client that sent in this address + MarketID string // asset code used by exchanges/api + Price sdk.Dec // price in decimal (max precision 18) + Expiry time.Time // expiry time } // NewMsgPostPrice creates a new post price msg @@ -27,10 +27,10 @@ func NewMsgPostPrice( price sdk.Dec, expiry time.Time) MsgPostPrice { return MsgPostPrice{ - From: from, - AssetCode: assetCode, - Price: price, - Expiry: expiry, + From: from, + MarketID: assetCode, + Price: price, + Expiry: expiry, } } @@ -54,10 +54,10 @@ func (msg MsgPostPrice) GetSigners() []sdk.AccAddress { // 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") + return sdk.ErrInternal("invalid (empty) from address") } - if len(msg.AssetCode) == 0 { - return sdk.ErrInternal("invalid (empty) asset code") + if len(msg.MarketID) == 0 { + return sdk.ErrInternal("invalid (empty) market id") } if msg.Price.LT(sdk.ZeroDec()) { return sdk.ErrInternal("invalid (negative) price") diff --git a/x/pricefeed/types/params.go b/x/pricefeed/types/params.go index fdef14d3..6236a8eb 100644 --- a/x/pricefeed/types/params.go +++ b/x/pricefeed/types/params.go @@ -4,33 +4,20 @@ import ( "fmt" "strings" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/params" ) +// Parameter keys var ( - // KeyMarkets store key for markets - KeyMarkets = []byte("Markets") + KeyMarkets = []byte("Markets") + DefaultMarkets = Markets{} ) -// ParamKeyTable Key declaration for parameters -func ParamKeyTable() params.KeyTable { - return params.NewKeyTable().RegisterParamSet(&Params{}) -} - // Params params for pricefeed. Can be altered via governance type Params struct { Markets Markets `json:"markets" yaml:"markets"` // Array containing the markets supported by the pricefeed } -// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs -// pairs of pricefeed module's parameters. -func (p Params) ParamSetPairs() params.ParamSetPairs { - return params.ParamSetPairs{ - {Key: KeyMarkets, Value: &p.Markets}, - } -} - // NewParams creates a new AssetParams object func NewParams(markets Markets) Params { return Params{ @@ -40,24 +27,31 @@ func NewParams(markets Markets) Params { // DefaultParams default params for pricefeed func DefaultParams() Params { - return NewParams(Markets{}) + return NewParams(DefaultMarkets) +} + +// ParamKeyTable Key declaration for parameters +func ParamKeyTable() params.KeyTable { + return params.NewKeyTable().RegisterParamSet(&Params{}) +} + +// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs +// pairs of pricefeed module's parameters. +func (p *Params) ParamSetPairs() params.ParamSetPairs { + return params.ParamSetPairs{ + {Key: KeyMarkets, Value: &p.Markets}, + } } // String implements fmt.stringer func (p Params) String() string { out := "Params:\n" for _, a := range p.Markets { - out += a.String() + out += fmt.Sprintf("%s\n", a.String()) } return strings.TrimSpace(out) } -// 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{}) -} - // Validate ensure that params have valid values func (p Params) Validate() error { // iterate over assets and verify them diff --git a/x/pricefeed/types/querier.go b/x/pricefeed/types/querier.go index 7439c362..074a1ee4 100644 --- a/x/pricefeed/types/querier.go +++ b/x/pricefeed/types/querier.go @@ -1,9 +1,5 @@ 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 @@ -13,23 +9,11 @@ const ( QueryCurrentPrice = "price" // QueryRawPrices command for raw price queries QueryRawPrices = "rawprices" - // QueryAssets command for assets query - QueryAssets = "assets" + // QueryMarkets command for assets query + QueryMarkets = "markets" ) -// QueryRawPricesResp response to a rawprice query -type QueryRawPricesResp []string - -// implement fmt.Stringer -func (n QueryRawPricesResp) String() string { - return strings.Join(n[:], "\n") +// QueryPricesParams fields for querying prices +type QueryPricesParams struct { + MarketID string } - -// QueryAssetsResp response to a assets query -type QueryAssetsResp []string - -// implement fmt.Stringer -func (n QueryAssetsResp) String() string { - return strings.Join(n[:], "\n") -} - From 710a29980f03bb82aff9d6d92a368b8ef3ef79fb Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Fri, 17 Jan 2020 13:55:17 +0100 Subject: [PATCH 65/99] R4R: add params query to pricefeed module (#311) * fix: remove redundant debt limit param * wip: test pricefeed genesis * fix: pricefeed querier * fix: comments, naming * fix: query path * fix: store methods * fix: query methods * add params query to pricefeed module * minor formatting updates, removed commented out legacy code, etc. Co-authored-by: Kevin Davis --- x/pricefeed/client/cli/query.go | 26 ++++++++++++++++++++++++++ x/pricefeed/client/cli/tx.go | 4 +--- x/pricefeed/client/rest/query.go | 15 +++++++++++++++ x/pricefeed/keeper/querier.go | 15 +++++++++++++++ x/pricefeed/types/codec.go | 2 +- x/pricefeed/types/errors.go | 2 +- x/pricefeed/types/msgs_test.go | 3 --- x/pricefeed/types/querier.go | 2 ++ 8 files changed, 61 insertions(+), 8 deletions(-) diff --git a/x/pricefeed/client/cli/query.go b/x/pricefeed/client/cli/query.go index 0100a373..5ad41c41 100644 --- a/x/pricefeed/client/cli/query.go +++ b/x/pricefeed/client/cli/query.go @@ -25,6 +25,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { GetCmdCurrentPrice(queryRoute, cdc), GetCmdRawPrices(queryRoute, cdc), GetCmdMarkets(queryRoute, cdc), + GetCmdQueryParams(queryRoute, cdc), )...) return pricefeedQueryCmd @@ -107,3 +108,28 @@ func GetCmdMarkets(queryRoute string, cdc *codec.Codec) *cobra.Command { }, } } + +// GetCmdQueryParams queries the pricefeed module parameters +func GetCmdQueryParams(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "params", + Short: "get the pricefeed module parameters", + Long: "Get the current global pricefeed 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) + if err != nil { + return err + } + + // Decode and print results + var out types.Params + cdc.MustUnmarshalJSON(res, &out) + return cliCtx.PrintOutput(out) + }, + } +} diff --git a/x/pricefeed/client/cli/tx.go b/x/pricefeed/client/cli/tx.go index 1793e59c..0b46a6af 100644 --- a/x/pricefeed/client/cli/tx.go +++ b/x/pricefeed/client/cli/tx.go @@ -42,9 +42,7 @@ func GetCmdPostPrice(cdc *codec.Codec) *cobra.Command { 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 diff --git a/x/pricefeed/client/rest/query.go b/x/pricefeed/client/rest/query.go index 30b2af46..9ff29431 100644 --- a/x/pricefeed/client/rest/query.go +++ b/x/pricefeed/client/rest/query.go @@ -17,6 +17,7 @@ func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { r.HandleFunc(fmt.Sprintf("/%s/rawprices/{%s}", types.ModuleName, restName), queryRawPricesHandler(cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/%s/currentprice/{%s}", types.ModuleName, restName), queryCurrentPriceHandler(cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/%s/markets", types.ModuleName), queryMarketsHandler(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/params", types.ModuleName), queryParamsHandlerFn(cliCtx)).Methods("GET") } func queryRawPricesHandler(cliCtx context.CLIContext) http.HandlerFunc { @@ -55,3 +56,17 @@ func queryMarketsHandler(cliCtx context.CLIContext) http.HandlerFunc { rest.PostProcessResponse(w, cliCtx, res) } } + +func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Get the params + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/pricefeed/%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) + } +} diff --git a/x/pricefeed/keeper/querier.go b/x/pricefeed/keeper/querier.go index 7952e688..6ea20a63 100644 --- a/x/pricefeed/keeper/querier.go +++ b/x/pricefeed/keeper/querier.go @@ -21,6 +21,8 @@ func NewQuerier(keeper Keeper) sdk.Querier { return queryRawPrices(ctx, req, keeper) case types.QueryMarkets: return queryMarkets(ctx, req, keeper) + case types.QueryGetParams: + return queryGetParams(ctx, req, keeper) default: return nil, sdk.ErrUnknownRequest("unknown pricefeed query endpoint") } @@ -80,3 +82,16 @@ func queryMarkets(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res [] return bz, nil } + +// query params in the auction store +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 +} diff --git a/x/pricefeed/types/codec.go b/x/pricefeed/types/codec.go index cd0437e3..e651c17c 100644 --- a/x/pricefeed/types/codec.go +++ b/x/pricefeed/types/codec.go @@ -10,7 +10,7 @@ func init() { RegisterCodec(ModuleCdc) } -// RegisterCode registers concrete types on the Amino code +// RegisterCodec registers concrete types on the Amino code func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(MsgPostPrice{}, "pricefeed/MsgPostPrice", nil) } diff --git a/x/pricefeed/types/errors.go b/x/pricefeed/types/errors.go index 325b0c1b..6d545f02 100644 --- a/x/pricefeed/types/errors.go +++ b/x/pricefeed/types/errors.go @@ -37,7 +37,7 @@ 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 markets +// ErrInvalidMarket Error constructor for posted price messages for invalid markets func ErrInvalidMarket(codespace sdk.CodespaceType, marketId string) sdk.Error { return sdk.NewError(codespace, CodeInvalidAsset, fmt.Sprintf("market %s does not exist", marketId)) } diff --git a/x/pricefeed/types/msgs_test.go b/x/pricefeed/types/msgs_test.go index fe139969..74192420 100644 --- a/x/pricefeed/types/msgs_test.go +++ b/x/pricefeed/types/msgs_test.go @@ -10,9 +10,6 @@ import ( func TestMsgPlaceBid_ValidateBasic(t *testing.T) { addr := sdk.AccAddress([]byte("someName")) - // oracles := []Oracle{Oracle{ - // OracleAddress: addr.String(), - // }} price, _ := sdk.NewDecFromStr("0.3005") expiry := tmtime.Now() negativePrice, _ := sdk.NewDecFromStr("-3.05") diff --git a/x/pricefeed/types/querier.go b/x/pricefeed/types/querier.go index 074a1ee4..8e5e1912 100644 --- a/x/pricefeed/types/querier.go +++ b/x/pricefeed/types/querier.go @@ -11,6 +11,8 @@ const ( QueryRawPrices = "rawprices" // QueryMarkets command for assets query QueryMarkets = "markets" + // QueryGetParams command for params query + QueryGetParams = "params" ) // QueryPricesParams fields for querying prices From b87968905266dcd4ed8b716f139f0675252196d3 Mon Sep 17 00:00:00 2001 From: rhuairahrighairigh Date: Fri, 17 Jan 2020 16:17:48 +0100 Subject: [PATCH 66/99] add missing return --- x/pricefeed/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/pricefeed/handler.go b/x/pricefeed/handler.go index 0c738a65..cfcc34e0 100644 --- a/x/pricefeed/handler.go +++ b/x/pricefeed/handler.go @@ -34,7 +34,7 @@ func HandleMsgPostPrice( } _, err = k.SetPrice(ctx, msg.From, msg.MarketID, msg.Price, msg.Expiry) if err != nil { - err.Result() + return err.Result() } return sdk.Result{} } From d887266c249ecf694641164603bb89df4a6a3599 Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Mon, 20 Jan 2020 19:28:30 +0000 Subject: [PATCH 67/99] Run begin blocker (#321) * add cdp begin blocker * fix: use reasonable defaults for surplus/debt auctions Co-authored-by: Kevin Davis --- app/app.go | 4 ++-- x/cdp/keeper/auctions_test.go | 12 ++++++------ x/cdp/types/params.go | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/app.go b/app/app.go index 70ea5031..839391ac 100644 --- a/app/app.go +++ b/app/app.go @@ -270,9 +270,9 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, // During begin block slashing happens after distr.BeginBlocker so that // there is nothing left over in the validator fee pool, so as to keep the // CanWithdrawInvariant invariant. - app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName) + app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, cdp.ModuleName) - app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName, auction.ModuleName) // TODO is this correct order? + app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName, auction.ModuleName) // Note: genutils must occur after staking so that pools are properly // initialized with tokens from genesis accounts. diff --git a/x/cdp/keeper/auctions_test.go b/x/cdp/keeper/auctions_test.go index bfa6b94a..59cb45fa 100644 --- a/x/cdp/keeper/auctions_test.go +++ b/x/cdp/keeper/auctions_test.go @@ -51,24 +51,24 @@ func (suite *AuctionTestSuite) TestNetDebtSurplus() { func (suite *AuctionTestSuite) TestSurplusAuction() { sk := suite.app.GetSupplyKeeper() - err := sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("usdx", 10000))) + err := sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("usdx", 10000000000))) suite.NoError(err) - err = sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("debt", 1000))) + err = sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("debt", 1000000000))) suite.NoError(err) suite.keeper.RunSurplusAndDebtAuctions(suite.ctx) acc := sk.GetModuleAccount(suite.ctx, auction.ModuleName) - suite.Equal(cs(c("usdx", 9000)), acc.GetCoins()) + suite.Equal(cs(c("usdx", 9000000000)), acc.GetCoins()) } func (suite *AuctionTestSuite) TestDebtAuction() { sk := suite.app.GetSupplyKeeper() - err := sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("usdx", 1000))) + err := sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("usdx", 1000000000))) suite.NoError(err) - err = sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("debt", 10000))) + err = sk.MintCoins(suite.ctx, types.LiquidatorMacc, cs(c("debt", 10000000000))) suite.NoError(err) suite.keeper.RunSurplusAndDebtAuctions(suite.ctx) acc := sk.GetModuleAccount(suite.ctx, auction.ModuleName) - suite.Equal(cs(c("debt", 9000)), acc.GetCoins()) + suite.Equal(cs(c("debt", 9000000000)), acc.GetCoins()) } func TestAuctionTestSuite(t *testing.T) { diff --git a/x/cdp/types/params.go b/x/cdp/types/params.go index 5fb33325..2d8c78d4 100644 --- a/x/cdp/types/params.go +++ b/x/cdp/types/params.go @@ -24,8 +24,8 @@ var ( DefaultCdpStartingID = uint64(1) DefaultDebtDenom = "debt" DefaultGovDenom = "ukava" - DefaultSurplusThreshold = sdk.NewInt(1000) - DefaultDebtThreshold = sdk.NewInt(1000) + DefaultSurplusThreshold = sdk.NewInt(1000000000) + DefaultDebtThreshold = sdk.NewInt(1000000000) DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0)) minCollateralPrefix = 0 maxCollateralPrefix = 255 From 2be1a3196edcd491545aaefb6559621aa3bd923b Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Tue, 21 Jan 2020 10:06:07 +0000 Subject: [PATCH 68/99] make bidding tests independent (#322) --- x/auction/keeper/bidding_test.go | 39 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/x/auction/keeper/bidding_test.go b/x/auction/keeper/bidding_test.go index 56cea3f2..b4f3ed1f 100644 --- a/x/auction/keeper/bidding_test.go +++ b/x/auction/keeper/bidding_test.go @@ -36,26 +36,6 @@ func TestAuctionBidding(t *testing.T) { collateralAddrs := addrs[2:] collateralWeights := is(30, 20, 10) - tApp := app.NewTestApp() - - // Set up seller account - sellerAcc := supply.NewEmptyModuleAccount(modName, supply.Minter, supply.Burner) - require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 1000), c("token2", 1000), c("debt", 1000)))) - - // Initialize genesis accounts - tApp.InitializeFromGenesisStates( - NewAuthGenStateFromAccs(authexported.GenesisAccounts{ - auth.NewBaseAccount(buyer, cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), - auth.NewBaseAccount(secondBuyer, cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), - auth.NewBaseAccount(collateralAddrs[0], cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), - auth.NewBaseAccount(collateralAddrs[1], cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), - auth.NewBaseAccount(collateralAddrs[2], cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), - sellerAcc, - }), - ) - ctx := tApp.NewContext(false, abci.Header{}) - keeper := tApp.GetAuctionKeeper() - type auctionArgs struct { auctionType AuctionType seller string @@ -285,6 +265,25 @@ func TestAuctionBidding(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { + // Setup test + tApp := app.NewTestApp() + // Set up seller account + sellerAcc := supply.NewEmptyModuleAccount(modName, supply.Minter, supply.Burner) + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 1000), c("token2", 1000), c("debt", 1000)))) + // Initialize genesis accounts + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + auth.NewBaseAccount(secondBuyer, cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + auth.NewBaseAccount(collateralAddrs[0], cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + auth.NewBaseAccount(collateralAddrs[1], cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + auth.NewBaseAccount(collateralAddrs[2], cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + sellerAcc, + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + // Start Auction var id uint64 var err sdk.Error From f01a3f46ee801e5b6ac4bc4aa751510b6182814d Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Tue, 21 Jan 2020 17:41:37 +0000 Subject: [PATCH 69/99] Auction cli/rest fixes (#319) * cleanup auction tx cli * add querier for getting one auction * missed querier fixes * update querier tests * add msg struct tags * add auction rest endpoint and tidy * add struct tags to auctions * minor UX tweaks --- x/auction/alias.go | 1 - x/auction/client/cli/query.go | 57 ++++++++++++++++++++++++----- x/auction/client/cli/tx.go | 22 +++++++---- x/auction/client/rest/query.go | 63 +++++++++++++++++++++++++++++--- x/auction/keeper/auctions.go | 4 +- x/auction/keeper/querier.go | 40 +++++++++++++++++--- x/auction/keeper/querier_test.go | 40 +++++++++++++++++--- x/auction/types/auctions.go | 30 +++++++-------- x/auction/types/errors.go | 2 +- x/auction/types/msg.go | 6 +-- x/auction/types/querier.go | 22 +++++------ 11 files changed, 219 insertions(+), 68 deletions(-) diff --git a/x/auction/alias.go b/x/auction/alias.go index dc79f6fa..c0b7a522 100644 --- a/x/auction/alias.go +++ b/x/auction/alias.go @@ -75,6 +75,5 @@ type ( GenesisState = types.GenesisState MsgPlaceBid = types.MsgPlaceBid Params = types.Params - QueryResAuctions = types.QueryResAuctions Keeper = keeper.Keeper ) diff --git a/x/auction/client/cli/query.go b/x/auction/client/cli/query.go index bb3d47a1..96bc15b7 100644 --- a/x/auction/client/cli/query.go +++ b/x/auction/client/cli/query.go @@ -2,12 +2,14 @@ package cli import ( "fmt" + "strconv" "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" ) @@ -20,6 +22,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { } auctionQueryCmd.AddCommand(client.GetCommands( + QueryGetAuctionCmd(queryRoute, cdc), QueryGetAuctionsCmd(queryRoute, cdc), QueryParamsCmd(queryRoute, cdc), )...) @@ -27,24 +30,60 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { return auctionQueryCmd } +// QueryGetAuctionCmd queries one auction in the store +func QueryGetAuctionCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "auction [auction-id]", + Short: "get a info about an auction", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Prepare params for querier + id, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("auction-id '%s' not a valid uint", args[0]) + } + bz, err := cdc.MarshalJSON(types.QueryAuctionParams{ + AuctionID: id, + }) + if err != nil { + return err + } + + // Query + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAuction), bz) + if err != nil { + return err + } + + // Decode and print results + var auction types.Auction + cdc.MustUnmarshalJSON(res, &auction) + return cliCtx.PrintOutput(auction) + }, + } +} + // QueryGetAuctionsCmd queries the auctions in the store func QueryGetAuctionsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ Use: "auctions", Short: "get a list of active auctions", + Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) - res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/auctions", queryRoute), nil) + + // Query + res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetAuctions), nil) if err != nil { - fmt.Printf("error when getting auctions - %s", err) - return nil + return err } - var out types.QueryResAuctions - cdc.MustUnmarshalJSON(res, &out) - if len(out) == 0 { - out = append(out, "There are currently no auctions") - } - return cliCtx.PrintOutput(out) + + // Decode and print results + var auctions types.Auctions + cdc.MustUnmarshalJSON(res, &auctions) + return cliCtx.PrintOutput(auctions) }, } } diff --git a/x/auction/client/cli/tx.go b/x/auction/client/cli/tx.go index b0adc7f0..4f143d7a 100644 --- a/x/auction/client/cli/tx.go +++ b/x/auction/client/cli/tx.go @@ -3,20 +3,22 @@ package cli import ( "fmt" "strconv" + "strings" - "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/version" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + + "github.com/kava-labs/kava/x/auction/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 { auctionTxCmd := &cobra.Command{ Use: "auction", @@ -33,22 +35,26 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { // GetCmdPlaceBid cli command for placing bids on auctions func GetCmdPlaceBid(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ - Use: "placebid [auction-id] [amount]", + Use: "bid [auction-id] [amount]", Short: "place a bid on an auction", - Args: cobra.MinimumNArgs(2), + Long: strings.TrimSpace( + fmt.Sprintf(`Place a bid on any type of auction, updating the latest bid amount to [amount]. Collateral auctions must be bid up to their maxbid before entering reverse phase. + +Example: +$ %s tx %s bid 34 1000usdx --from myKeyName +`, version.ClientName, types.ModuleName)), + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { cliCtx := context.NewCLIContext().WithCodec(cdc) txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) id, err := strconv.ParseUint(args[0], 10, 64) if err != nil { - fmt.Printf("invalid auction id - %s \n", string(args[0])) - return err + return fmt.Errorf("auction-id '%s' not a valid uint", args[0]) } - amt, err := sdk.ParseCoin(args[2]) + amt, err := sdk.ParseCoin(args[1]) if err != nil { - fmt.Printf("invalid amount - %s \n", string(args[2])) return err } diff --git a/x/auction/client/rest/query.go b/x/auction/client/rest/query.go index f64a80bb..3a315445 100644 --- a/x/auction/client/rest/query.go +++ b/x/auction/client/rest/query.go @@ -12,18 +12,66 @@ import ( "github.com/kava-labs/kava/x/auction/types" ) +const RestAuctionID = "auction-id" + func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { - r.HandleFunc(fmt.Sprintf("/auction/getauctions"), queryGetAuctionsHandlerFn(cliCtx)).Methods("GET") - r.HandleFunc("/auction/params", getParamsHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/auction/auctions/{%s}", RestAuctionID), queryAuctionHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/auction/auctions", queryAuctionsHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/auction/parameters", getParamsHandlerFn(cliCtx)).Methods("GET") } -func queryGetAuctionsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { +func queryAuctionHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - res, height, err := cliCtx.QueryWithData("/custom/auction/getauctions", nil) + // Parse the query height + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + // Prepare params for querier + vars := mux.Vars(r) + if len(vars[RestAuctionID]) == 0 { + err := fmt.Errorf("%s required but not specified", RestAuctionID) + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + auctionID, ok := rest.ParseUint64OrReturnBadRequest(w, vars[RestAuctionID]) + if !ok { + return + } + bz, err := cliCtx.Codec.MarshalJSON(types.QueryAuctionParams{AuctionID: auctionID}) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // Query + res, height, err := cliCtx.QueryWithData("custom/gov/proposal", bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + // Decode and return results + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} + +func queryAuctionsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Parse the query height + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + // Get all auctions + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("/custom/auction/%s", types.QueryGetAuctions), nil) if err != nil { rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) return } + // Return auctions cliCtx = cliCtx.WithHeight(height) rest.PostProcessResponse(w, cliCtx, res) } @@ -31,14 +79,19 @@ func queryGetAuctionsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { func getParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + // Parse the query height + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } // Get the params res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/auction/%s", types.QueryGetParams), nil) - cliCtx = cliCtx.WithHeight(height) if err != nil { rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } // Return the params + cliCtx = cliCtx.WithHeight(height) rest.PostProcessResponse(w, cliCtx, res) } } diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index 0c69c231..b8079c20 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -250,7 +250,7 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc if err != nil { return a, err } - // Debt coins are sent to liquidator (until there is no CorrespondingDebt left). Amount sent is equal to bidIncrement. + // Debt coins are sent to liquidator (until there is no CorrespondingDebt left). Amount sent is equal to bidIncrement (or whatever is left if < bidIncrement). if a.CorrespondingDebt.IsPositive() { debtAmountToReturn := sdk.MinInt(bidIncrement.Amount, a.CorrespondingDebt.Amount) @@ -367,7 +367,7 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac return a, err } } - // Debt coins are sent to liquidator the first time a bid is placed. Amount sent is equal to Bid. + // Debt coins are sent to liquidator the first time a bid is placed. Amount sent is equal to min of Bid and amount of debt. if a.Bidder.Equals(supply.NewModuleAddress(a.Initiator)) { debtAmountToReturn := sdk.MinInt(a.Bid.Amount, a.CorrespondingDebt.Amount) diff --git a/x/auction/keeper/querier.go b/x/auction/keeper/querier.go index 29bfebc9..08d337b3 100644 --- a/x/auction/keeper/querier.go +++ b/x/auction/keeper/querier.go @@ -3,8 +3,9 @@ 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" + + "github.com/kava-labs/kava/x/auction/types" ) // NewQuerier is the module level router for state queries @@ -12,6 +13,8 @@ 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 queryAuction(ctx, req, keeper) + case types.QueryGetAuctions: return queryAuctions(ctx, req, keeper) case types.QueryGetParams: return queryGetParams(ctx, req, keeper) @@ -21,17 +24,41 @@ func NewQuerier(keeper Keeper) sdk.Querier { } } -func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { - var auctionsList types.Auctions +func queryAuction(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + // Decode request + var requestParams types.QueryAuctionParams + err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams) + if err != nil { + return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + } + // Lookup auction + auction, found := keeper.GetAuction(ctx, requestParams.AuctionID) + if !found { + return nil, types.ErrAuctionNotFound(types.DefaultCodespace, requestParams.AuctionID) + } + + // Encode results + bz, err := codec.MarshalJSONIndent(keeper.cdc, auction) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + + return bz, nil +} + +func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + // Get all auctions + auctionsList := types.Auctions{} keeper.IterateAuctions(ctx, func(a types.Auction) bool { auctionsList = append(auctionsList, a) return false }) - bz, err2 := codec.MarshalJSONIndent(keeper.cdc, auctionsList) - if err2 != nil { - return nil, sdk.ErrInternal("could not marshal result to JSON") + // Encode Results + bz, err := codec.MarshalJSONIndent(keeper.cdc, auctionsList) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) } return bz, nil @@ -47,5 +74,6 @@ func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]by if err != nil { return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) } + return bz, nil } diff --git a/x/auction/keeper/querier_test.go b/x/auction/keeper/querier_test.go index c1ec1f7f..8ecf15ec 100644 --- a/x/auction/keeper/querier_test.go +++ b/x/auction/keeper/querier_test.go @@ -59,10 +59,11 @@ func (suite *QuerierTestSuite) SetupTest() { suite.keeper = tApp.GetAuctionKeeper() // Populate with auctions + randSrc := rand.New(rand.NewSource(int64(1234))) for j := 0; j < TestAuctionCount; j++ { - lotAmount := simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 10, 100) + lotAmount := simulation.RandIntBetween(randSrc, 10, 100) id, err := suite.keeper.StartSurplusAuction(suite.ctx, modName, c("token1", int64(lotAmount)), "token2") - suite.Nil(err) + suite.NoError(err) auc, found := suite.keeper.GetAuction(suite.ctx, id) suite.True(found) @@ -72,12 +73,12 @@ func (suite *QuerierTestSuite) SetupTest() { suite.querier = keeper.NewQuerier(suite.keeper) } -func (suite *QuerierTestSuite) TestQueryAuctions() { +func (suite *QuerierTestSuite) TestQueryAuction() { ctx := suite.ctx.WithIsCheckTx(false) // Set up request query query := abci.RequestQuery{ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAuction}, "/"), - Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryAllAuctionParams(1, TestAuctionCount)), + Data: types.ModuleCdc.MustMarshalJSON(types.QueryAuctionParams{AuctionID: 0}), // get the first auction } // Execute query and check the []byte result @@ -85,11 +86,40 @@ func (suite *QuerierTestSuite) TestQueryAuctions() { suite.NoError(err) suite.NotNil(bz) + // Unmarshal the bytes into type Auction + var auction types.Auction + suite.NoError(types.ModuleCdc.UnmarshalJSON(bz, &auction)) + + // Check the returned auction + suite.Equal(suite.auctions[0].GetID(), auction.GetID()) + suite.Equal(suite.auctions[0].GetInitiator(), auction.GetInitiator()) + suite.Equal(suite.auctions[0].GetLot(), auction.GetLot()) + suite.Equal(suite.auctions[0].GetBid(), auction.GetBid()) + suite.Equal(suite.auctions[0].GetEndTime(), auction.GetEndTime()) + +} + +func (suite *QuerierTestSuite) TestQueryAuctions() { + ctx := suite.ctx.WithIsCheckTx(false) + // Set up request query + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAuctions}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryAllAuctionParams(1, TestAuctionCount)), + } + + // Execute query and check the []byte result + bz, err := suite.querier(ctx, []string{types.QueryGetAuctions}, query) + suite.NoError(err) + suite.NotNil(bz) + // Unmarshal the bytes into type Auctions var auctions types.Auctions - suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &auctions)) + suite.NoError(types.ModuleCdc.UnmarshalJSON(bz, &auctions)) // Check that each Auction has correct values + if len(auctions) == 0 && len(suite.auctions) != 0 { + suite.FailNow("no auctions returned") // skip the panic from indexing empty slice below + } for i := 0; i < TestAuctionCount; i++ { suite.Equal(suite.auctions[i].GetID(), auctions[i].GetID()) suite.Equal(suite.auctions[i].GetInitiator(), auctions[i].GetInitiator()) diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 3ef09f47..583d9765 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -29,14 +29,14 @@ type Auctions []Auction // BaseAuction is a common type shared by all Auctions. type BaseAuction struct { - ID uint64 - Initiator string // Module name that starts the auction. Pays out Lot. - Lot sdk.Coin // Coins that will paid out by Initiator to the winning bidder. - Bidder sdk.AccAddress // Latest bidder. Receiver of Lot. - Bid sdk.Coin // Coins paid into the auction the bidder. - HasReceivedBids bool // Whether the auction has received any bids or not. - EndTime time.Time // Current auction closing time. Triggers at the end of the block with time ≥ EndTime. - MaxEndTime time.Time // Maximum closing time. Auctions can close before this but never after. + ID uint64 `json:"id" yaml:"id"` + Initiator string `json:"initiator" yaml:"initiator"` // Module name that starts the auction. Pays out Lot. + Lot sdk.Coin `json:"lot" yaml:"lot"` // Coins that will paid out by Initiator to the winning bidder. + Bidder sdk.AccAddress `json:"bidder" yaml:"bidder"` // Latest bidder. Receiver of Lot. + Bid sdk.Coin `json:"bid" yaml:"bid"` // Coins paid into the auction the bidder. + HasReceivedBids bool `json:"has_received_bids" yaml:"has_received_bids"` // Whether the auction has received any bids or not. + EndTime time.Time `json:"end_time" yaml:"end_time"` // Current auction closing time. Triggers at the end of the block with time ≥ EndTime. + MaxEndTime time.Time `json:"max_end_time" yaml:"max_end_time"` // Maximum closing time. Auctions can close before this but never after. } // GetID is a getter for auction ID. @@ -82,7 +82,7 @@ func (a BaseAuction) String() string { // SurplusAuction is a forward auction that burns what it receives from bids. // It is normally used to sell off excess pegged asset acquired by the CDP system. type SurplusAuction struct { - BaseAuction + BaseAuction `json:"base_auction" yaml:"base_auction"` } // WithID returns an auction with the ID set. @@ -116,8 +116,8 @@ func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime tim // DebtAuction is a reverse auction that mints what it pays out. // It is normally used to acquire pegged asset to cover the CDP system's debts that were not covered by selling collateral. type DebtAuction struct { - BaseAuction - CorrespondingDebt sdk.Coin + BaseAuction `json:"base_auction" yaml:"base_auction"` + CorrespondingDebt sdk.Coin `json:"corresponding_debt" yaml:"corresponding_debt"` } // WithID returns an auction with the ID set. @@ -160,10 +160,10 @@ func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, e // Unsold Lot is sent to LotReturns, being divided among the addresses by weight. // Collateral auctions are normally used to sell off collateral seized from CDPs. type CollateralAuction struct { - BaseAuction - CorrespondingDebt sdk.Coin - MaxBid sdk.Coin - LotReturns WeightedAddresses + BaseAuction `json:"base_auction" yaml:"base_auction"` + CorrespondingDebt sdk.Coin `json:"corresponding_debt" yaml:"corresponding_debt"` + MaxBid sdk.Coin `json:"max_bid" yaml:"max_bid"` + LotReturns WeightedAddresses `json:"lot_returns" yaml:"lot_returns"` } // WithID returns an auction with the ID set. diff --git a/x/auction/types/errors.go b/x/auction/types/errors.go index f5b427aa..d281f3aa 100644 --- a/x/auction/types/errors.go +++ b/x/auction/types/errors.go @@ -68,7 +68,7 @@ func ErrInvalidLotDenom(codespace sdk.CodespaceType, lotDenom string, auctionLot // ErrBidTooSmall error for when bid is not greater than auction's last bid func ErrBidTooSmall(codespace sdk.CodespaceType, bid sdk.Coin, lastBid sdk.Coin) sdk.Error { - return sdk.NewError(codespace, CodeBidTooSmall, fmt.Sprintf("bid %s is smaller than auction's last bid %s", bid.String(), lastBid.String())) + return sdk.NewError(codespace, CodeBidTooSmall, fmt.Sprintf("bid %s is not greater than auction's last bid %s", bid.String(), lastBid.String())) } // ErrBidTooLarge error for when bid is larger than auction's maximum allowed bid diff --git a/x/auction/types/msg.go b/x/auction/types/msg.go index ed18d84b..b60fbbf4 100644 --- a/x/auction/types/msg.go +++ b/x/auction/types/msg.go @@ -7,9 +7,9 @@ var _ sdk.Msg = &MsgPlaceBid{} // MsgPlaceBid is the message type used to place a bid on any type of auction. type MsgPlaceBid struct { - AuctionID uint64 - Bidder sdk.AccAddress - Amount sdk.Coin // The new bid or lot to be set on the auction. + AuctionID uint64 `json:"auction_id" yaml:"auction_id"` + Bidder sdk.AccAddress `json:"bidder" yaml:"bidder"` + Amount sdk.Coin `json:"amount" yaml:"amount"` // The new bid or lot to be set on the auction. } // NewMsgPlaceBid returns a new MsgPlaceBid. diff --git a/x/auction/types/querier.go b/x/auction/types/querier.go index 038e9bb0..c8019f10 100644 --- a/x/auction/types/querier.go +++ b/x/auction/types/querier.go @@ -1,21 +1,17 @@ package types -import ( - "strings" -) - const ( - // QueryGetAuction command for getting the information about a particular auction - QueryGetAuction = "auctions" - QueryGetParams = "params" + // QueryGetAuction is the query path for querying one auction + QueryGetAuction = "auction" + // QueryGetAuction is the query path for querying all auctions + QueryGetAuctions = "auctions" + // QueryGetAuction is the query path for querying the global auction params + QueryGetParams = "params" ) -// QueryResAuctions Result Payload for an auctions query -type QueryResAuctions []string - -// implement fmt.Stringer -func (n QueryResAuctions) String() string { - return strings.Join(n[:], "\n") +// QueryAuctionParams params for query /auction/auction +type QueryAuctionParams struct { + AuctionID uint64 } // QueryAllAuctionParams is the params for an auctions query From a74f63a8f251673a7de9e55390c261a75d7c7d3b Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Tue, 21 Jan 2020 17:52:07 +0000 Subject: [PATCH 70/99] Apply Liquidation Penalty (#313) * fix: remove redundant debt limit param * wip: test pricefeed genesis * fix: pricefeed querier * fix: comments, naming * fix: query path * fix: store methods * fix: query methods * feat: Liquidation Penalty * fix: remove debt from liquidation penalty --- x/cdp/keeper/auctions.go | 5 +++-- x/cdp/keeper/seize.go | 18 ++++++++++++------ x/cdp/keeper/seize_test.go | 13 +++++++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/x/cdp/keeper/auctions.go b/x/cdp/keeper/auctions.go index 7c8397b9..5e6e422b 100644 --- a/x/cdp/keeper/auctions.go +++ b/x/cdp/keeper/auctions.go @@ -118,9 +118,10 @@ func (k Keeper) CreateAuctionsFromDeposit(ctx sdk.Context, dep types.Deposit, de for depositAmount.GTE(auctionSize) { // figure out how much debt is covered by one lots worth of collateral depositDebtAmount := (sdk.NewDecFromInt(auctionSize).Quo(sdk.NewDecFromInt(totalCollateral))).Mul(sdk.NewDecFromInt(debt)).RoundInt() - // start an auction for one lot, attempting to raise depositDebtAmount + penalty := k.ApplyLiquidationPenalty(ctx, depositDenom, depositDebtAmount) + // start an auction for one lot, attempting to raise depositDebtAmount plus the liquidation penalty _, err := k.auctionKeeper.StartCollateralAuction( - ctx, types.LiquidatorMacc, sdk.NewCoin(depositDenom, auctionSize), sdk.NewCoin(principalDenom, depositDebtAmount), []sdk.AccAddress{dep.Depositor}, + ctx, types.LiquidatorMacc, sdk.NewCoin(depositDenom, auctionSize), sdk.NewCoin(principalDenom, depositDebtAmount.Add(penalty)), []sdk.AccAddress{dep.Depositor}, []sdk.Int{auctionSize}, sdk.NewCoin(k.GetDebtDenom(ctx), depositDebtAmount)) if err != nil { return sdk.ZeroInt(), sdk.ZeroInt(), err diff --git a/x/cdp/keeper/seize.go b/x/cdp/keeper/seize.go index fcb15756..ca2efa52 100644 --- a/x/cdp/keeper/seize.go +++ b/x/cdp/keeper/seize.go @@ -10,11 +10,11 @@ import ( // SeizeCollateral liquidates the collateral in the input cdp. // the following operations are performed: // 1. updates the fees for the input cdp, -// 2. sends collateral for all deposits from the cdp module to the liquidator module, -// 3. moves debt coins from the cdp module to the liquidator module, +// 2. sends collateral for all deposits from the cdp module to the liquidator module account +// 3. Applies the liquidation penalty and mints the corresponding amount of debt coins in the cdp module +// 3. moves debt coins from the cdp module to the liquidator module account, // 4. decrements the total amount of principal outstanding for that collateral type -// (this is the equivalent of saying that fees are no longer accumulated by a cdp once it -// gets liquidated) +// (this is the equivalent of saying that fees are no longer accumulated by a cdp once it gets liquidated) func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) sdk.Error { // Calculate the previous collateral ratio oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees)) @@ -24,8 +24,6 @@ func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) sdk.Error { cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees) cdp.FeesUpdated = ctx.BlockTime() - // TODO implement liquidation penalty - // Move debt coins from cdp to liquidator account deposits := k.GetDeposits(ctx, cdp.ID) debt := sdk.ZeroInt() @@ -35,6 +33,7 @@ func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) sdk.Error { for _, dc := range cdp.AccumulatedFees { debt = debt.Add(dc.Amount) } + debtCoin := sdk.NewCoin(k.GetDebtDenom(ctx), debt) err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, sdk.NewCoins(debtCoin)) if err != nil { @@ -108,3 +107,10 @@ func (k Keeper) LiquidateCdps(ctx sdk.Context, marketID string, denom string, li } return nil } + +// ApplyLiquidationPenalty multiplies the input debt amount by the liquidation penalty and mints the debt coins in the cdp module account +func (k Keeper) ApplyLiquidationPenalty(ctx sdk.Context, denom string, debt sdk.Int) sdk.Int { + penalty := k.getLiquidationPenalty(ctx, denom) + penaltyAmount := sdk.NewDecFromInt(debt).Mul(penalty).RoundInt() + return penaltyAmount +} diff --git a/x/cdp/keeper/seize_test.go b/x/cdp/keeper/seize_test.go index 0c89db6b..e769826d 100644 --- a/x/cdp/keeper/seize_test.go +++ b/x/cdp/keeper/seize_test.go @@ -108,6 +108,9 @@ func (suite *SeizeTestSuite) TestSeizeCollateral() { suite.NoError(err) tpa := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") suite.Equal(tpb.Sub(tpa), p) + auctionKeeper := suite.app.GetAuctionKeeper() + _, found := auctionKeeper.GetAuction(suite.ctx, 0) + suite.True(found) auctionMacc := sk.GetModuleAccount(suite.ctx, auction.ModuleName) suite.Equal(cs(c("debt", p.Int64()), c("xrp", cl.Int64())), auctionMacc.GetCoins()) ak := suite.app.GetAccountKeeper() @@ -162,6 +165,16 @@ func (suite *SeizeTestSuite) TestHandleNewDebt() { suite.Equal(sdk.NewDec(tpb.Int64()).Mul(d("1.05")).TruncateInt().Int64(), tpa.Int64()) } +func (suite *SeizeTestSuite) TestApplyLiquidationPenalty() { + penalty := suite.keeper.ApplyLiquidationPenalty(suite.ctx, "xrp", i(1000)) + suite.Equal(i(50), penalty) + penalty = suite.keeper.ApplyLiquidationPenalty(suite.ctx, "btc", i(1000)) + suite.Equal(i(25), penalty) + penalty = suite.keeper.ApplyLiquidationPenalty(suite.ctx, "xrp", i(675760172)) + suite.Equal(i(33788009), penalty) + suite.Panics(func() { suite.keeper.ApplyLiquidationPenalty(suite.ctx, "lol", i(1000)) }) +} + func TestSeizeTestSuite(t *testing.T) { suite.Run(t, new(SeizeTestSuite)) } From f6a3567a666818b8315db4aec79750802b0c669e Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Tue, 21 Jan 2020 10:24:33 -0800 Subject: [PATCH 71/99] removed unimplemented MsgTransferCDP (#325) --- x/cdp/alias.go | 1 - x/cdp/types/codec.go | 1 - x/cdp/types/msg.go | 5 ----- 3 files changed, 7 deletions(-) diff --git a/x/cdp/alias.go b/x/cdp/alias.go index 951dff00..6d9d75e2 100644 --- a/x/cdp/alias.go +++ b/x/cdp/alias.go @@ -152,7 +152,6 @@ type ( MsgWithdraw = types.MsgWithdraw MsgDrawDebt = types.MsgDrawDebt MsgRepayDebt = types.MsgRepayDebt - MsgTransferCDP = types.MsgTransferCDP Params = types.Params CollateralParam = types.CollateralParam CollateralParams = types.CollateralParams diff --git a/x/cdp/types/codec.go b/x/cdp/types/codec.go index 6db0831d..48a5fb76 100644 --- a/x/cdp/types/codec.go +++ b/x/cdp/types/codec.go @@ -19,5 +19,4 @@ func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(MsgWithdraw{}, "cdp/MsgWithdraw", nil) cdc.RegisterConcrete(MsgDrawDebt{}, "cdp/MsgDrawDebt", nil) cdc.RegisterConcrete(MsgRepayDebt{}, "cdp/MsgRepayDebt", nil) - cdc.RegisterConcrete(MsgTransferCDP{}, "cdp/MsgTransferCDP", nil) } diff --git a/x/cdp/types/msg.go b/x/cdp/types/msg.go index 14d3a1e3..acfe1324 100644 --- a/x/cdp/types/msg.go +++ b/x/cdp/types/msg.go @@ -321,8 +321,3 @@ func (msg MsgRepayDebt) String() string { Payment: %s `, msg.Sender, msg.CdpDenom, msg.Payment) } - -// MsgTransferCDP changes the ownership of a cdp -type MsgTransferCDP struct { - // TODO -} From bab798f50d02a2895b8cba5579e6b0e67dc39329 Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Tue, 21 Jan 2020 20:28:25 +0000 Subject: [PATCH 72/99] fix: avoid panic if cdp debt > debt held by cdp account (#326) Co-authored-by: Kevin Davis --- x/cdp/abci_test.go | 54 ++++++++++++++++++++++++++++++++++++++ x/cdp/integration_test.go | 4 +-- x/cdp/keeper/seize.go | 10 ++++++- x/cdp/keeper/seize_test.go | 30 +++++++++++++++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) diff --git a/x/cdp/abci_test.go b/x/cdp/abci_test.go index e8c9573f..b16ab7e5 100644 --- a/x/cdp/abci_test.go +++ b/x/cdp/abci_test.go @@ -33,6 +33,32 @@ type liquidationTracker struct { } func (suite *ModuleTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + coins := []sdk.Coins{} + tracker := liquidationTracker{} + + for j := 0; j < 100; j++ { + coins = append(coins, cs(c("btc", 100000000), c("xrp", 10000000000))) + } + _, addrs := app.GeneratePrivKeyAddressPairs(100) + + authGS := app.NewAuthGenState( + addrs, coins) + tApp.InitializeFromGenesisStates( + authGS, + NewPricefeedGenStateMulti(), + NewCDPGenStateMulti(), + ) + suite.ctx = ctx + suite.app = tApp + suite.keeper = tApp.GetCDPKeeper() + suite.cdps = cdp.CDPs{} + suite.addrs = addrs + suite.liquidations = tracker +} + +func (suite *ModuleTestSuite) createCdps() { tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) cdps := make(cdp.CDPs, 100) @@ -96,6 +122,7 @@ func (suite *ModuleTestSuite) setPrice(price sdk.Dec, market string) { suite.Equal(price, pp.Price) } func (suite *ModuleTestSuite) TestBeginBlock() { + suite.createCdps() sk := suite.app.GetSupplyKeeper() acc := sk.GetModuleAccount(suite.ctx, cdp.ModuleName) originalXrpCollateral := acc.GetCoins().AmountOf("xrp") @@ -122,6 +149,33 @@ func (suite *ModuleTestSuite) TestBeginBlock() { } +func (suite *ModuleTestSuite) TestSeizeSingleCdpWithFees() { + err := suite.keeper.AddCdp(suite.ctx, suite.addrs[0], cs(c("xrp", 10000000000)), cs(c("usdx", 1000000000))) + suite.NoError(err) + suite.keeper.SetPreviousBlockTime(suite.ctx, suite.ctx.BlockTime()) + previousBlockTime, _ := suite.keeper.GetPreviousBlockTime(suite.ctx) + suite.Equal(i(1000000000), suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx")) + sk := suite.app.GetSupplyKeeper() + cdpMacc := sk.GetModuleAccount(suite.ctx, cdp.ModuleName) + suite.Equal(i(1000000000), cdpMacc.GetCoins().AmountOf("debt")) + for i := 0; i < 100; i++ { + suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 6)) + cdp.BeginBlocker(suite.ctx, abci.RequestBeginBlock{Header: suite.ctx.BlockHeader()}, suite.keeper) + } + + cdpMacc = sk.GetModuleAccount(suite.ctx, cdp.ModuleName) + suite.Equal(i(1000000900), (cdpMacc.GetCoins().AmountOf("debt"))) + cdp, _ := suite.keeper.GetCDP(suite.ctx, "xrp", 1) + + timeElapsed := sdk.NewInt(suite.ctx.BlockTime().Unix() - previousBlockTime.Unix()) + + fees := suite.keeper.CalculateFees(suite.ctx, cdp.Principal, timeElapsed, "xrp") + suite.Equal(i(928), fees.AmountOf("usdx")) + + err = suite.keeper.SeizeCollateral(suite.ctx, cdp) + suite.NoError(err) +} + func TestModuleTestSuite(t *testing.T) { suite.Run(t, new(ModuleTestSuite)) } diff --git a/x/cdp/integration_test.go b/x/cdp/integration_test.go index bb429006..2ae0ae9b 100644 --- a/x/cdp/integration_test.go +++ b/x/cdp/integration_test.go @@ -154,10 +154,10 @@ func NewCDPGenStateMulti() app.GenesisState { func cdps() (cdps cdp.CDPs) { _, addrs := app.GeneratePrivKeyAddressPairs(3) - c1 := cdp.NewCDP(uint64(1), addrs[0], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(10000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(8000000))), tmtime.Canonical(time.Now())) + c1 := cdp.NewCDP(uint64(1), addrs[0], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(100000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(8000000))), tmtime.Canonical(time.Now())) c2 := cdp.NewCDP(uint64(2), addrs[1], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(100000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(10000000))), tmtime.Canonical(time.Now())) c3 := cdp.NewCDP(uint64(3), addrs[1], sdk.NewCoins(sdk.NewCoin("btc", sdk.NewInt(1000000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(10000000))), tmtime.Canonical(time.Now())) - c4 := cdp.NewCDP(uint64(4), addrs[2], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(1000000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(500000000))), tmtime.Canonical(time.Now())) + c4 := cdp.NewCDP(uint64(4), addrs[2], sdk.NewCoins(sdk.NewCoin("xrp", sdk.NewInt(1000000000))), sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(50000000))), tmtime.Canonical(time.Now())) cdps = append(cdps, c1, c2, c3, c4) return } diff --git a/x/cdp/keeper/seize.go b/x/cdp/keeper/seize.go index ca2efa52..08645a2b 100644 --- a/x/cdp/keeper/seize.go +++ b/x/cdp/keeper/seize.go @@ -33,7 +33,10 @@ func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) sdk.Error { for _, dc := range cdp.AccumulatedFees { debt = debt.Add(dc.Amount) } - + modAccountDebt := k.getModAccountDebt(ctx, types.ModuleName) + if modAccountDebt.LT(debt) { + debt = modAccountDebt + } debtCoin := sdk.NewCoin(k.GetDebtDenom(ctx), debt) err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, sdk.NewCoins(debtCoin)) if err != nil { @@ -114,3 +117,8 @@ func (k Keeper) ApplyLiquidationPenalty(ctx sdk.Context, denom string, debt sdk. penaltyAmount := sdk.NewDecFromInt(debt).Mul(penalty).RoundInt() return penaltyAmount } + +func (k Keeper) getModAccountDebt(ctx sdk.Context, accountName string) sdk.Int { + macc := k.supplyKeeper.GetModuleAccount(ctx, accountName) + return macc.GetCoins().AmountOf(k.GetDebtDenom(ctx)) +} diff --git a/x/cdp/keeper/seize_test.go b/x/cdp/keeper/seize_test.go index e769826d..3f0b445a 100644 --- a/x/cdp/keeper/seize_test.go +++ b/x/cdp/keeper/seize_test.go @@ -34,6 +34,32 @@ type liquidationTracker struct { } func (suite *SeizeTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + coins := []sdk.Coins{} + tracker := liquidationTracker{} + + for j := 0; j < 100; j++ { + coins = append(coins, cs(c("btc", 100000000), c("xrp", 10000000000))) + } + _, addrs := app.GeneratePrivKeyAddressPairs(100) + + authGS := app.NewAuthGenState( + addrs, coins) + tApp.InitializeFromGenesisStates( + authGS, + NewPricefeedGenStateMulti(), + NewCDPGenStateMulti(), + ) + suite.ctx = ctx + suite.app = tApp + suite.keeper = tApp.GetCDPKeeper() + suite.cdps = types.CDPs{} + suite.addrs = addrs + suite.liquidations = tracker +} + +func (suite *SeizeTestSuite) createCdps() { tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) cdps := make(types.CDPs, 100) @@ -99,6 +125,7 @@ func (suite *SeizeTestSuite) setPrice(price sdk.Dec, market string) { } func (suite *SeizeTestSuite) TestSeizeCollateral() { + suite.createCdps() sk := suite.app.GetSupplyKeeper() cdp, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2)) p := cdp.Principal[0].Amount @@ -121,6 +148,7 @@ func (suite *SeizeTestSuite) TestSeizeCollateral() { } func (suite *SeizeTestSuite) TestSeizeCollateralMultiDeposit() { + suite.createCdps() sk := suite.app.GetSupplyKeeper() cdp, _ := suite.keeper.GetCDP(suite.ctx, "xrp", uint64(2)) err := suite.keeper.DepositCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], cs(c("xrp", 6999000000))) @@ -145,6 +173,7 @@ func (suite *SeizeTestSuite) TestSeizeCollateralMultiDeposit() { } func (suite *SeizeTestSuite) TestLiquidateCdps() { + suite.createCdps() sk := suite.app.GetSupplyKeeper() acc := sk.GetModuleAccount(suite.ctx, types.ModuleName) originalXrpCollateral := acc.GetCoins().AmountOf("xrp") @@ -159,6 +188,7 @@ func (suite *SeizeTestSuite) TestLiquidateCdps() { } func (suite *SeizeTestSuite) TestHandleNewDebt() { + suite.createCdps() tpb := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") suite.keeper.HandleNewDebt(suite.ctx, "xrp", "usdx", i(31536000)) tpa := suite.keeper.GetTotalPrincipal(suite.ctx, "xrp", "usdx") From ab72433db05fa0fa03ea7b1d3fafd60c70fb29ee Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Tue, 21 Jan 2020 13:40:04 -0800 Subject: [PATCH 73/99] add json/yaml to msgs (#323) * added json/yaml to MsgPostPrice, MsgPlaceBid * Added sdk.Msg to ensure Msg interface compliance at compile time --- x/pricefeed/types/msgs.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/x/pricefeed/types/msgs.go b/x/pricefeed/types/msgs.go index e73e32dd..059c3866 100644 --- a/x/pricefeed/types/msgs.go +++ b/x/pricefeed/types/msgs.go @@ -11,13 +11,16 @@ const ( TypeMsgPostPrice = "post_price" ) +// ensure Msg interface compliance at compile time +var _ sdk.Msg = &MsgPostPrice{} + // 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 - MarketID string // asset code used by exchanges/api - Price sdk.Dec // price in decimal (max precision 18) - Expiry time.Time // expiry time + From sdk.AccAddress `json:"from" yaml:"from"` // client that sent in this address + MarketID string `json:"market_id" yaml:"market_id"` // asset code used by exchanges/api + Price sdk.Dec `json:"price" yaml:"price"` // price in decimal (max precision 18) + Expiry time.Time `json:"expiry" yaml:"expiry"` // expiry time } // NewMsgPostPrice creates a new post price msg From 22dc15f7578f4ab5fea3c846d48d3d0b1d79ed3b Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Wed, 22 Jan 2020 16:50:27 +0000 Subject: [PATCH 74/99] Apply debt floor to repayments (#314) * fix: remove redundant debt limit param * wip: test pricefeed genesis * fix: pricefeed querier * fix: comments, naming * fix: query path * fix: store methods * fix: query methods * feat: Liquidation Penalty * feat: enforce debt floor on repayment * address review comments * fix: remove debt from liquidation penalty * test: remove payment > balance check * feat: handle overpayment * fix: avoid negative coins error for overpayments --- x/cdp/alias.go | 2 ++ x/cdp/keeper/cdp.go | 6 +++--- x/cdp/keeper/draw.go | 38 ++++++++++++++++++++++++++------------ x/cdp/keeper/draw_test.go | 22 ++++++++++++++++------ x/cdp/keeper/params.go | 4 ++-- x/cdp/types/errors.go | 6 ++++++ 6 files changed, 55 insertions(+), 23 deletions(-) diff --git a/x/cdp/alias.go b/x/cdp/alias.go index 6d9d75e2..a54b1ada 100644 --- a/x/cdp/alias.go +++ b/x/cdp/alias.go @@ -27,6 +27,7 @@ const ( CodeInvalidWithdrawAmount = types.CodeInvalidWithdrawAmount CodeCdpNotAvailable = types.CodeCdpNotAvailable CodeBelowDebtFloor = types.CodeBelowDebtFloor + CodePaymentExceedsDebt = types.CodePaymentExceedsDebt EventTypeCreateCdp = types.EventTypeCreateCdp EventTypeCdpDeposit = types.EventTypeCdpDeposit EventTypeCdpDraw = types.EventTypeCdpDraw @@ -74,6 +75,7 @@ var ( ErrInvalidWithdrawAmount = types.ErrInvalidWithdrawAmount ErrCdpNotAvailable = types.ErrCdpNotAvailable ErrBelowDebtFloor = types.ErrBelowDebtFloor + ErrPaymentExceedsDebt = types.ErrPaymentExceedsDebt DefaultGenesisState = types.DefaultGenesisState GetCdpIDBytes = types.GetCdpIDBytes GetCdpIDFromBytes = types.GetCdpIDFromBytes diff --git a/x/cdp/keeper/cdp.go b/x/cdp/keeper/cdp.go index 7a342990..0b41943f 100644 --- a/x/cdp/keeper/cdp.go +++ b/x/cdp/keeper/cdp.go @@ -346,7 +346,7 @@ func (k Keeper) ValidateCollateral(ctx sdk.Context, collateral sdk.Coins) sdk.Er // ValidatePrincipalAdd validates that an asset is valid for use as debt when creating a new cdp func (k Keeper) ValidatePrincipalAdd(ctx sdk.Context, principal sdk.Coins) sdk.Error { for _, dc := range principal { - dp, found := k.GetDebt(ctx, dc.Denom) + dp, found := k.GetDebtParam(ctx, dc.Denom) if !found { return types.ErrDebtNotSupported(k.codespace, dc.Denom) } @@ -360,7 +360,7 @@ func (k Keeper) ValidatePrincipalAdd(ctx sdk.Context, principal sdk.Coins) sdk.E // ValidatePrincipalDraw validates that an asset is valid for use as debt when drawing debt off an existing cdp func (k Keeper) ValidatePrincipalDraw(ctx sdk.Context, principal sdk.Coins) sdk.Error { for _, dc := range principal { - _, found := k.GetDebt(ctx, dc.Denom) + _, found := k.GetDebtParam(ctx, dc.Denom) if !found { return types.ErrDebtNotSupported(k.codespace, dc.Denom) } @@ -440,6 +440,6 @@ func (k Keeper) convertCollateralToBaseUnits(ctx sdk.Context, collateral sdk.Coi // converts the input debt to base units (ie multiplies the input by 10^(-ConversionFactor)) func (k Keeper) convertDebtToBaseUnits(ctx sdk.Context, debt sdk.Coin) (baseUnits sdk.Dec) { - dp, _ := k.GetDebt(ctx, debt.Denom) + dp, _ := k.GetDebtParam(ctx, debt.Denom) return sdk.NewDecFromInt(debt.Amount).Mul(sdk.NewDecFromIntWithPrec(sdk.OneInt(), dp.ConversionFactor.Int64())) } diff --git a/x/cdp/keeper/draw.go b/x/cdp/keeper/draw.go index f286e45a..54abede7 100644 --- a/x/cdp/keeper/draw.go +++ b/x/cdp/keeper/draw.go @@ -85,32 +85,32 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri if !found { return types.ErrCdpNotFound(k.codespace, owner, denom) } - err := k.ValidatePaymentCoins(ctx, cdp, payment) - if err != nil { - return err - } // calculate fees periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix())) fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom) + err := k.ValidatePaymentCoins(ctx, cdp, payment, cdp.Principal.Add(cdp.AccumulatedFees).Add(fees)) + if err != nil { + return err + } // calculate fee and principal payment - feePayment, principalPayment := k.calculatePayment(ctx, cdp.AccumulatedFees.Add(fees), payment) + feePayment, principalPayment := k.calculatePayment(ctx, cdp.Principal.Add(cdp.AccumulatedFees).Add(fees), cdp.AccumulatedFees.Add(fees), payment) // send the payment from the sender to the cpd module - err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, payment) + err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, feePayment.Add(principalPayment)) if err != nil { return err } // burn the payment coins - err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, payment) + err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, feePayment.Add(principalPayment)) if err != nil { panic(err) } // burn the corresponding amount of debt coins - err = k.BurnDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), payment) + err = k.BurnDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), feePayment.Add(principalPayment)) if err != nil { panic(err) } @@ -119,7 +119,7 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeCdpRepay, - sdk.NewAttribute(sdk.AttributeKeyAmount, payment.String()), + sdk.NewAttribute(sdk.AttributeKeyAmount, feePayment.Add(principalPayment).String()), sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)), ), ) @@ -136,7 +136,7 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri cdp.FeesUpdated = ctx.BlockTime() // decrement the total principal for the input collateral type - k.DecrementTotalPrincipal(ctx, denom, payment) + k.DecrementTotalPrincipal(ctx, denom, feePayment.Add(principalPayment)) // if the debt is fully paid, return collateral to depositors, // and remove the cdp and indexes from the store @@ -162,7 +162,7 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri } // ValidatePaymentCoins validates that the input coins are valid for repaying debt -func (k Keeper) ValidatePaymentCoins(ctx sdk.Context, cdp types.CDP, payment sdk.Coins) sdk.Error { +func (k Keeper) ValidatePaymentCoins(ctx sdk.Context, cdp types.CDP, payment sdk.Coins, debt sdk.Coins) sdk.Error { subset := payment.DenomsSubsetOf(cdp.Principal) if !subset { var paymentDenoms []string @@ -175,6 +175,13 @@ func (k Keeper) ValidatePaymentCoins(ctx sdk.Context, cdp types.CDP, payment sdk } return types.ErrInvalidPaymentDenom(k.codespace, cdp.ID, principalDenoms, paymentDenoms) } + for _, dc := range payment { + dp, _ := k.GetDebtParam(ctx, dc.Denom) + proposedBalance := cdp.Principal.AmountOf(dc.Denom).Sub(dc.Amount) + if proposedBalance.GT(sdk.ZeroInt()) && proposedBalance.LT(dp.DebtFloor) { + return types.ErrBelowDebtFloor(k.codespace, sdk.NewCoins(sdk.NewCoin(dc.Denom, proposedBalance)), dp.DebtFloor) + } + } return nil } @@ -190,10 +197,17 @@ func (k Keeper) ReturnCollateral(ctx sdk.Context, cdp types.CDP) { } } -func (k Keeper) calculatePayment(ctx sdk.Context, fees sdk.Coins, payment sdk.Coins) (sdk.Coins, sdk.Coins) { +func (k Keeper) calculatePayment(ctx sdk.Context, owed sdk.Coins, fees sdk.Coins, payment sdk.Coins) (sdk.Coins, sdk.Coins) { // divides repayment into principal and fee components, with fee payment applied first. + feePayment := sdk.NewCoins() principalPayment := sdk.NewCoins() + overpayment := sdk.NewCoins() + // TODO must compare denoms directly if there are multiple principal denoms + if payment.IsAllGT(owed) { + overpayment = payment.Sub(owed) + payment = payment.Sub(overpayment) + } if fees.IsZero() { return sdk.NewCoins(), payment } diff --git a/x/cdp/keeper/draw_test.go b/x/cdp/keeper/draw_test.go index c8f97998..9239ffe8 100644 --- a/x/cdp/keeper/draw_test.go +++ b/x/cdp/keeper/draw_test.go @@ -29,7 +29,7 @@ func (suite *DrawTestSuite) SetupTest() { authGS := app.NewAuthGenState( addrs, []sdk.Coins{ - cs(c("xrp", 500000000), c("btc", 500000000)), + cs(c("xrp", 500000000), c("btc", 500000000), c("usdx", 10000000000)), cs(c("xrp", 200000000)), cs(c("xrp", 10000000000000), c("usdx", 100000000000))}) tApp.InitializeFromGenesisStates( @@ -126,9 +126,9 @@ func (suite *DrawTestSuite) TestAddRepayPrincipal() { suite.Equal(types.CodeInvalidPaymentDenom, err.Result().Code) err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[1], "xrp", cs(c("xusd", 10000000))) suite.Equal(types.CodeCdpNotFound, err.Result().Code) - err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 100000000))) - suite.Error(err) + err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 9000000))) + suite.Equal(types.CodeBelowDebtFloor, err.Result().Code) err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000))) suite.NoError(err) @@ -144,6 +144,16 @@ func (suite *DrawTestSuite) TestAddRepayPrincipal() { } +func (suite *DrawTestSuite) TestRepayPrincipalOverpay() { + err := suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[0], "xrp", cs(c("usdx", 20000000))) + suite.NoError(err) + ak := suite.app.GetAccountKeeper() + acc := ak.GetAccount(suite.ctx, suite.addrs[0]) + suite.Equal(i(10000000000), (acc.GetCoins().AmountOf("usdx"))) + _, found := suite.keeper.GetCDP(suite.ctx, "xrp", 1) + suite.False(found) +} + func (suite *DrawTestSuite) TestAddRepayPrincipalFees() { err := suite.keeper.AddCdp(suite.ctx, suite.addrs[2], cs(c("xrp", 1000000000000)), cs(c("usdx", 100000000000))) suite.NoError(err) @@ -176,9 +186,9 @@ func (suite *DrawTestSuite) TestPricefeedFailure() { ctx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 2)) pfk := suite.app.GetPriceFeedKeeper() pfk.SetCurrentPrices(ctx, "xrp:usd") - err := suite.keeper.AddPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10))) + err := suite.keeper.AddPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000))) suite.Error(err) - err = suite.keeper.RepayPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10))) + err = suite.keeper.RepayPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000))) suite.NoError(err) } @@ -189,7 +199,7 @@ func (suite *DrawTestSuite) TestModuleAccountFailure() { acc := sk.GetModuleAccount(ctx, types.ModuleName) ak := suite.app.GetAccountKeeper() ak.RemoveAccount(ctx, acc) - _ = suite.keeper.RepayPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10))) + _ = suite.keeper.RepayPrincipal(ctx, suite.addrs[0], "xrp", cs(c("usdx", 10000000))) }) } diff --git a/x/cdp/keeper/params.go b/x/cdp/keeper/params.go index 337af5a3..d0bd8a06 100644 --- a/x/cdp/keeper/params.go +++ b/x/cdp/keeper/params.go @@ -30,8 +30,8 @@ func (k Keeper) GetCollateral(ctx sdk.Context, denom string) (types.CollateralPa return types.CollateralParam{}, false } -// GetDebt returns the debt param with matching denom -func (k Keeper) GetDebt(ctx sdk.Context, denom string) (types.DebtParam, bool) { +// GetDebtParam returns the debt param with matching denom +func (k Keeper) GetDebtParam(ctx sdk.Context, denom string) (types.DebtParam, bool) { params := k.GetParams(ctx) for _, dp := range params.DebtParams { if dp.Denom == denom { diff --git a/x/cdp/types/errors.go b/x/cdp/types/errors.go index d7e14d73..56cd5a93 100644 --- a/x/cdp/types/errors.go +++ b/x/cdp/types/errors.go @@ -25,6 +25,7 @@ const ( CodeInvalidWithdrawAmount sdk.CodeType = 13 CodeCdpNotAvailable sdk.CodeType = 14 CodeBelowDebtFloor sdk.CodeType = 15 + CodePaymentExceedsDebt sdk.CodeType = 16 ) // ErrCdpAlreadyExists error for duplicate cdps @@ -101,3 +102,8 @@ func ErrCdpNotAvailable(codespace sdk.CodespaceType, cdpID uint64) sdk.Error { func ErrBelowDebtFloor(codespace sdk.CodespaceType, debt sdk.Coins, floor sdk.Int) sdk.Error { return sdk.NewError(codespace, CodeBelowDebtFloor, fmt.Sprintf("proposed cdp debt of %s is below the minimum of %s", debt, floor)) } + +// ErrPaymentExceedsDebt error for repayments that are greater than the debt amount +func ErrPaymentExceedsDebt(codespace sdk.CodespaceType, payment sdk.Coins, principal sdk.Coins) sdk.Error { + return sdk.NewError(codespace, CodePaymentExceedsDebt, fmt.Sprintf("payment of %s exceeds debt of %s", payment, principal)) +} From 58deb49e5553f99d059dc90fe2cdc2c6271b724d Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Wed, 22 Jan 2020 16:56:30 +0000 Subject: [PATCH 75/99] Fix withdraw panic (#315) * fix: remove redundant debt limit param * wip: test pricefeed genesis * fix: pricefeed querier * fix: comments, naming * fix: query path * fix: store methods * fix: query methods * feat: Liquidation Penalty * feat: enforce debt floor on repayment * fix: don't panic if withdrawing full amount * fix: remove debt from liquidation penalty --- x/cdp/keeper/cdp.go | 3 +++ x/cdp/keeper/deposit_test.go | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/x/cdp/keeper/cdp.go b/x/cdp/keeper/cdp.go index 0b41943f..0675154d 100644 --- a/x/cdp/keeper/cdp.go +++ b/x/cdp/keeper/cdp.go @@ -412,6 +412,9 @@ func (k Keeper) CalculateCollateralToDebtRatio(ctx sdk.Context, collateral sdk.C // CalculateCollateralizationRatio returns the collateralization ratio of the input collateral to the input debt plus fees func (k Keeper) CalculateCollateralizationRatio(ctx sdk.Context, collateral sdk.Coins, principal sdk.Coins, fees sdk.Coins) (sdk.Dec, sdk.Error) { + if collateral.IsZero() { + return sdk.ZeroDec(), nil + } marketID := k.getMarketID(ctx, collateral[0].Denom) price, err := k.pricefeedKeeper.GetCurrentPrice(ctx, marketID) if err != nil { diff --git a/x/cdp/keeper/deposit_test.go b/x/cdp/keeper/deposit_test.go index fab85c8a..d9a5e6b0 100644 --- a/x/cdp/keeper/deposit_test.go +++ b/x/cdp/keeper/deposit_test.go @@ -93,7 +93,9 @@ func (suite *DepositTestSuite) TestDepositCollateral() { } func (suite *DepositTestSuite) TestWithdrawCollateral() { - err := suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 321000000))) + err := suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 400000000))) + suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code) + err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[0], suite.addrs[0], cs(c("xrp", 321000000))) suite.Equal(types.CodeInvalidCollateralRatio, err.Result().Code) err = suite.keeper.WithdrawCollateral(suite.ctx, suite.addrs[1], suite.addrs[0], cs(c("xrp", 10000000))) suite.Equal(types.CodeCdpNotFound, err.Result().Code) From e11b298c28201b1740da7b76038050d8a83841b7 Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Thu, 23 Jan 2020 19:09:36 +0000 Subject: [PATCH 76/99] Add CDP Spec (#328) * add overview and basic structure * add state and params * add basic messages * add state transitions * add begin block state transitions * add missing titles * add concepts * add events * update state and concepts * update for liquidator changes * update events * mention module accounts * update begin block * update params * update page numbering * add fee descriptions * add broken link linter * add broken link linter to CI * move link check to end of CI * update typo Co-Authored-By: Kevin Davis * address review comments * Update x/cdp/spec/06_params.md Co-Authored-By: Kevin Davis * Update x/cdp/spec/README.md Co-Authored-By: Kevin Davis * Update x/cdp/spec/README.md Co-Authored-By: Kevin Davis Co-authored-by: Kevin Davis --- .circleci/config.yml | 12 +++- Makefile | 9 +++ go.mod | 1 + go.sum | 29 +++++++++ x/auction/spec/04_events.md | 6 +- x/auction/spec/README.md | 2 +- x/cdp/spec/01_concepts.md | 62 +++++++++++++++++++ x/cdp/spec/02_state.md | 67 ++++++++++++++++++++ x/cdp/spec/03_messages.md | 116 +++++++++++++++++++++++++++++++++++ x/cdp/spec/04_begin_block.md | 34 ++++++++++ x/cdp/spec/05_events.md | 59 ++++++++++++++++++ x/cdp/spec/06_params.md | 31 ++++++++++ x/cdp/spec/README.md | 19 ++++++ 13 files changed, 442 insertions(+), 5 deletions(-) create mode 100644 x/cdp/spec/01_concepts.md create mode 100644 x/cdp/spec/02_state.md create mode 100644 x/cdp/spec/03_messages.md create mode 100644 x/cdp/spec/04_begin_block.md create mode 100644 x/cdp/spec/05_events.md create mode 100644 x/cdp/spec/06_params.md create mode 100644 x/cdp/spec/README.md diff --git a/.circleci/config.yml b/.circleci/config.yml index bb93ea28..c727d1a7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -96,6 +96,13 @@ jobs: target: start-remote-sims description: "Test multi-seed simulation (long)" + broken-link-check: + executor: golang + steps: + - make: + target: link-check + description: "Check url links are not broken" + workflows: version: 2 @@ -116,4 +123,7 @@ workflows: # These filters ensure that the long sim only runs during release filters: branches: - only: "master" \ No newline at end of file + only: "master" + - broken-link-check: + requires: + - setup-dependencies \ No newline at end of file diff --git a/Makefile b/Makefile index 2dfbf5f8..4cbed021 100644 --- a/Makefile +++ b/Makefile @@ -94,6 +94,15 @@ go.sum: go.mod clean: rm -rf build/ +######################################## +### Linting + +# Check url links in the repo are not broken. +# This tool checks local markdown links as well. +# Set to exclude riot links as they trigger false positives +link-check: + @go run github.com/raviqqe/liche -r . --exclude ^https://riot.im/app + ######################################## ### Testing diff --git a/go.mod b/go.mod index ca6b8e18..ffba0067 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.13 require ( github.com/cosmos/cosmos-sdk v0.34.4-0.20191010193331-18de630d0ae1 github.com/gorilla/mux v1.7.3 + github.com/raviqqe/liche v0.0.0-20191208214012-e144e0808a75 // indirect github.com/spf13/cobra v0.0.5 github.com/spf13/viper v1.4.0 github.com/stretchr/testify v1.4.0 diff --git a/go.sum b/go.sum index b8895b5c..cb578ad0 100644 --- a/go.sum +++ b/go.sum @@ -59,6 +59,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a h1:mq+R6XEM6lJX5VlLyZIrUSP8tSuJp82xTK89hvBwJbU= github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= github.com/etcd-io/bbolt v1.3.2/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= @@ -70,6 +72,8 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojt github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y= github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= @@ -141,6 +145,11 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.8.5 h1:2ucYeik+NtUTg+IAiNQtoFC5ZGs5mIVidI7Ome0Br3Y= +github.com/klauspost/compress v1.8.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -157,6 +166,9 @@ github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDe github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= @@ -200,6 +212,8 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rakyll/statik v0.1.6 h1:uICcfUXpgqtw2VopbIncslhAmE5hwc4g20TEyEENBNs= github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= +github.com/raviqqe/liche v0.0.0-20191208214012-e144e0808a75 h1:o0pGzJnfjk0E+CZg6jxQrADfk9WYO9fMuLOtSP4owtE= +github.com/raviqqe/liche v0.0.0-20191208214012-e144e0808a75/go.mod h1:/L9q8uCsB8BOWdzLK+6WIwkAlcMfKhFCZY0n8/CLHRY= github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 h1:nkcn14uNmFEuGCb2mBZbBb24RdNRL08b/wb+xBOYpuk= github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= @@ -207,7 +221,12 @@ github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa/go.mod h1:oJyF+mSPHbB5mVY2iO9KV3pTt/QbIkGaO8gQ2WrDbP4= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -268,6 +287,11 @@ github.com/tendermint/tm-db v0.2.0/go.mod h1:0cPKWu2Mou3IlxecH+MEUSYc1Ch537alLe6 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.5.0 h1:dhq+O9pmNZFF6qAXpasMO1xSm7dL4qEz2ylfZN8BG9w= +github.com/valyala/fasthttp v1.5.0/go.mod h1:eriCz9OhZjKCGfJ185a/IDgNl0bg9IbzfpcslMZXU1c= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/zondax/hid v0.9.0 h1:eiT3P6vNxAEVxXMw66eZUAAnU2zD33JBkfG/EnfAKl8= @@ -298,6 +322,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 h1:6KET3Sqa7fkVfD63QnAM81ZeYg5n4HwApOJkufONnHA= +golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -311,10 +338,12 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= diff --git a/x/auction/spec/04_events.md b/x/auction/spec/04_events.md index fae55424..39ec4053 100644 --- a/x/auction/spec/04_events.md +++ b/x/auction/spec/04_events.md @@ -5,7 +5,7 @@ The `x/auction` module emits the following events: ## Triggered By Other Modules | Type | Attribute Key | Attribute Value | -| ------------- | ------------- | ------------------- | +|---------------|---------------|---------------------| | auction_start | auction_id | {auction ID} | | auction_start | auction_type | {auction type} | | auction_start | lot_denom | {auction lot denom} | @@ -16,7 +16,7 @@ The `x/auction` module emits the following events: ### MsgPlaceBid | Type | Attribute Key | Attribute Value | -| ----------- | ------------- | ------------------ | +|-------------|---------------|--------------------| | auction_bid | auction_id | {auction ID} | | auction_bid | bidder | {latest bidder} | | auction_bid | bid_amount | {coin amount} | @@ -28,5 +28,5 @@ The `x/auction` module emits the following events: ## EndBlock | Type | Attribute Key | Attribute Value | -| ------------- | ------------- | --------------- | +|---------------|---------------|-----------------| | auction_close | auction_id | {auction ID} | diff --git a/x/auction/spec/README.md b/x/auction/spec/README.md index 32ae71d1..8c2ee2b5 100644 --- a/x/auction/spec/README.md +++ b/x/auction/spec/README.md @@ -6,7 +6,7 @@ 3. **[Messages](03_messages.md)** 4. **[Events](04_events.md)** 5. **[Params](05_params.md)** -6. **[BeginBlock](06_begin_block.md)** +6. **[EndBlock](06_end_block.md)** ## Abstract diff --git a/x/cdp/spec/01_concepts.md b/x/cdp/spec/01_concepts.md new file mode 100644 index 00000000..cde6e9d2 --- /dev/null +++ b/x/cdp/spec/01_concepts.md @@ -0,0 +1,62 @@ +# Concepts + +## Collateralized Debt Positions + +CDPs enable the creation of a stable asset by collateralization with another on chain asset. + +A CDP is scoped to one collateral type. It has one primary owner, and a set of "depositors". The depositors can deposit and withdraw collateral to the CDP. The owner can draw stable assets (creating debt) and repay them to cancel the debt. + +Once created stable assets are free to be transferred between users, but a CDP owner must repay their debt to get their collateral back. + +User interactions with this module: + +- create a new cdp by depositing some type of coin as collateral +- withdraw newly minted stable coin from this CDP (up to a fraction of the value of the collateral) +- repay debt by paying back stable coins (including paying any fees accrued) +- remove collateral and close CDP + +## Liquidation & Stability System + +In the event of a decrease in the price of the collateral, the total value of all collateral in CDPs may drop below the value of all the issued stable assets. This undesirable event is countered through two mechanisms: + +**CDP Liquidations** The ratio of collateral value to debt value in each CDP is monitored. When this drops too low the collateral and debt is automatically seized by the system. The collateral is sold off through an auction to bring in stable asset which is burned against the seized debt. + +**Debt Auctions** In extreme cases where liquidations fail to raise enough to cover the seized debt, another mechanism kicks in: Debt Auctions. System governance tokens are minted and sold through auction to raise enough stable asset to cover the remaining debt. The governors of the system represent the lenders of last resort. + +The system monitors the state of CDPs and debt and triggers these auctions as needed. + +## Internal Debt Tracking + +Users incur debt when they draw new stable assets from their CDP. Within the system this debt is tracked in the form of a "debt coin" stored internally in the module's accounts. Every time a stable coin is created a corresponding debt coin is created. Likewise when debt is repaid stable coin and internal debt coin are burned. + +The cdp module uses two module accounts - one to hold debt coins associated with active CDPs, and another (the "liquidator" account) to hold debt from CDPS that have been seized by the system. + +## Fees + +When a user repays stable asset withdrawn from a CDP, they must also pay a fee. + +This is calculated according to the amount of stable asset withdrawn and the time withdrawn for. Like interest on a loan fees grow at a compounding percentage of original debt. + +Fees create incentives to open or close CDPs and can be changed by governance to help keep the system functioning through changing market conditions. + +A further fee is applied on liquidation of a CDP. Normally when the collateral is sold to cover the debt, any excess not sold is returned to the CDP holder. The liquidation fee reduces the amount of excess collateral returned, representing a cut that the system takes. + +Fees accumulate to the system before being automatically sold at auction for governance token. These are then burned, acting as incentive for safe governance of the system. + +## Governance + +The cdp module's behavior is controlled through several parameters which are updated through a governance mechanism. These parameters are listed in [Parameters](06_params.md). + +Governance is important for actions such as: + +- enabling CDPs to be created with new collateral assets +- changing fee rates to incentivize behavior +- increasing the debt ceiling to allow more stable asset to be created + +## Dependency: supply + +The CDP module relies on a supply keeper to move assets between its module accounts and user accounts. + +## Dependency: pricefeed + +The CDP module needs to know the current price of collateral assets in order to determine if CDPs are under collateralized. This is provided by a "pricefeed" module that returns a price for a given collateral in units (usually US Dollars) which are the target for the stable asset. diff --git a/x/cdp/spec/02_state.md b/x/cdp/spec/02_state.md new file mode 100644 index 00000000..65f66250 --- /dev/null +++ b/x/cdp/spec/02_state.md @@ -0,0 +1,67 @@ +# State + +For detail on the state tracked by the cdp module see the types package. In particular [keys.go](../types/keys.go) describes how state is stored in the key-value store. + +## Module Accounts + +The cdp module account controls two module accounts: + +**CDP Account:** Stores the deposited cdp collateral, and the debt coins for the debt in all the cdps. + +**Liquidator Account:** Stores debt coins that have been seized by the system, and any stable asset that has been raised through auctions. + +## CDP + +A CDP is a struct representing a debt position owned by one address. It has one collateral type and records the debt that has been drawn and how much fees should be repaid. + +Only an owner is authorized to draw or repay debt, but anyone can deposit collateral to a CDP. Deposits are scoped per address and are recorded separately in `Deposit` types. Depositors are free to withdraw their collateral provided it does not put the CDP below the liquidation ratio. + +The CDP's collateral always equal to the total of the deposits. + +```go +type CDP struct { + ID uint64 + Owner sdk.AccAddress + Collateral sdk.Coins + Principal sdk.Coins + AccumulatedFees sdk.Coins + FeesUpdated time.Time +} +``` + +CDPs are stored with a couple of database indexes for faster lookup: + +- by collateral ratio - to look up cdps that are close to the liquidation ratio +- by owner index - to look up cdps that an address is the owner of + +## Deposit + +A Deposit is a struct recording collateral added to a CDP by one address. The address only has authorization to change their deposited amount (provided it does not put the CDP below the liquidation ratio). + +```go +type Deposit struct { + CdpID uint64 + Depositor sdk.AccAddress + Amount sdk.Coins +} +``` + +## Params + +Module parameters controlled by governance. See [Parameters](06_params.md) for details. + +## NextCDPID + +A global counter used to create unique CDP ids. + +## DebtDenom + +The name of the internal debt coin. Its value can be configured at genesis. + +## Total Principle + +Sum of all non seized debt plus accumulated fees. This is used to calculate the new debt created every block due to the fee interest rate. + +## Previous Block Time + +A record of the last block time used to calculate fees. diff --git a/x/cdp/spec/03_messages.md b/x/cdp/spec/03_messages.md new file mode 100644 index 00000000..95304323 --- /dev/null +++ b/x/cdp/spec/03_messages.md @@ -0,0 +1,116 @@ +# Messages + +Users can submit various messages to the cdp module which trigger state changes detailed below. + +## CreateCDP + +CreateCDP sets up and stores a new CDP, adding collateral from the sender, and drawing `Principle` debt. + +```go +type MsgCreateCDP struct { + Sender sdk.AccAddress + Collateral sdk.Coins + Principal sdk.Coins +} +``` + +State changes: + +- a new CDP is created, `Sender` becomes CDP owner +- collateral taken from `Sender` and sent to cdp module account, new `Deposit` created +- `Principal` stable coins are minted and sent to `Sender` +- equal amount of internal debt coins created and stored in cdp module account + +## Deposit + +Deposit adds collateral to a CDP in the form of a deposit. Collateral is taken from `Depositor`. + +```go +type MsgDeposit struct { + Owner sdk.AccAddress + Depositor sdk.AccAddress + Collateral sdk.Coins +} +``` + +State Changes: + +- `Collateral` taken from depositor and sent to cdp module account +- the depositor's `Deposit` struct is updated or a new one created +- cdp fees are updated (see below) + +## Withdraw + +Withdraw removes collateral from a CDP, provided it would not put the CDP under the liquidation ratio. Collateral is removed from one deposit only. + +```go +type MsgWithdraw struct { + Owner sdk.AccAddress + Depositor sdk.AccAddress + Collateral sdk.Coins +} +``` + +State Changes: + +- `Collateral` coins are sent from the cdp module account to `Depositor` +- `Collateral` amount of coins subtracted from the `Deposit` struct +- cdp fees are updated (see below) + +## DrawDebt + +DrawDebt creates debt in a CDP, minting new stable asset which is sent to the sender. + +```go +type MsgDrawDebt struct { + Sender sdk.AccAddress + CdpDenom string + Principal sdk.Coins +} +``` + +State Changes: + +- mint `Principal` coins and send them to `Sender`, updating the CDP's `Principal` field +- mint equal amount of internal debt coins and store in the module account +- increment total principal for principal denom +- cdp fees are updated (see below) + +## RepayDebt + +RepayDebt removes some debt from a CDP and burns the corresponding amount of stable asset from the sender. If all debt is repaid, the collateral is returned to depositors and the cdp is removed from the store + +```go +type MsgRepayDebt struct { + Sender sdk.AccAddress + CdpDenom string + Payment sdk.Coins +} +``` + +State Changes: + +- burn `Payment` coins taken from `Sender`, updating the CDP by reducing `Principal` field by `Paymment` +- burn an equal amount of internal debt coins +- decrement total principal for payment denom +- cdp fees are updated (see below) +- if fees and principal are zero, return collateral to depositors: + - For each deposit, send coins from the cdp module account to the depositor, and delete the deposit struct from store. + +## Fees + +When CDPs are updated by the above messages the fees accumulated since the last update are calculated and added on. + +``` +feesAccumulated = (outstandingDebt * (feeRate^periods)) - outstandingDebt +``` + +where: + +- `outstandingDebt` is the CDP's `Principal` plus `AccumulatedFees` +- `periods` is the number of seconds since last fee update +- `feeRate` is the per second debt interest rate + +## Database Indexes + +When CDPs are update by the above messages the database indexes are also updated. \ No newline at end of file diff --git a/x/cdp/spec/04_begin_block.md b/x/cdp/spec/04_begin_block.md new file mode 100644 index 00000000..e8257c0c --- /dev/null +++ b/x/cdp/spec/04_begin_block.md @@ -0,0 +1,34 @@ +# Begin Blocker + +At the start of every block the BeginBlocker of the cdp module: + +- updates total CDP fees +- liquidates CDPs under the collateral ratio +- nets out system debt and, if necessary, starts auctions to re-balance it +- records the last block time + +## Update Fees + +- The total fees accumulated since the last block across all CDPs are calculated. +- An equal amount of debt coins are minted and sent to the system's CDP module account. +- An equal amount of stable asset coins are minted and sent to the system's liquidator module account + +## Liquidate CDP + +- Get every cdp that is under the liquidation ratio for its collateral type. +- For each cdp: + - Calculate and update fees since last update. + - Remove all collateral and internal debt coins from cdp and deposits and delete it. Send the coins to the liquidator module account. + - Start auctions of a fixed size from this collateral (with any remainder in a smaller sized auction), sending collateral and debt coins to the auction module account. + - Decrement total principal. + +## Net Out System Debt, Re-Balance + +- Burn the maximum possible equal amount of debt and stable asset from the liquidator module account. +- If there is enough debt remaining for an auction, start one. +- If there is enough surplus stable asset remaining for an auction, start one. +- Otherwise do nothing, leave debt/surplus to accumulate over subsequent blocks. + +## Update Previous Block Time + +The current block time is recorded. diff --git a/x/cdp/spec/05_events.md b/x/cdp/spec/05_events.md new file mode 100644 index 00000000..693e91c5 --- /dev/null +++ b/x/cdp/spec/05_events.md @@ -0,0 +1,59 @@ +# Events + +The cdp module emits the following events: + +## Handlers + +### MsgCreateCDP + +| Type | Attribute Key | Attribute Value | +|-------------|---------------|------------------| +| message | module | cdp | +| message | sender | {sender address} | +| create_cdp | cdp_id | {cdp id} | +| cdp_deposit | cdp_id | {cdp id} | +| cdp_deposit | amount | {deposit amount} | +| cdp_draw | cdp_id | {cdp id} | +| cdp_draw | amount | {draw amount} | + +### MsgWithdraw + +| Type | Attribute Key | Attribute Value | +|---------|---------------|------------------| +| message | module | cdp | +| message | sender | {sender address} | + +### MsgDeposit + +| Type | Attribute Key | Attribute Value | +|-------------|---------------|------------------| +| message | module | cdp | +| message | sender | {sender address} | +| cdp_deposit | cdp_id | {cdp id} | +| cdp_deposit | amount | {deposit amount} | + +### MsgDrawDebt + +| Type | Attribute Key | Attribute Value | +|----------|---------------|------------------| +| message | module | cdp | +| message | sender | {sender address} | +| cdp_draw | cdp_id | {cdp id} | +| cdp_draw | amount | {draw amount} | + +### MsgRepayDebt + +| Type | Attribute Key | Attribute Value | +|---------|---------------|------------------| +| message | module | cdp | +| message | sender | {sender address} | + +## BeginBlock + +| Type | Attribute Key | Attribute Value | +|-------------------------|---------------|---------------------| +| cdp_liquidation | module | cdp | +| cdp_liquidation | cdp_id | {cdp id} | +| cdp_liquidation | depositor | {depositor address} | +| cdp_begin_blocker_error | module | cdp | +| cdp_begin_blocker_error | error_message | {error} | diff --git a/x/cdp/spec/06_params.md b/x/cdp/spec/06_params.md new file mode 100644 index 00000000..fbde95d7 --- /dev/null +++ b/x/cdp/spec/06_params.md @@ -0,0 +1,31 @@ +# Parameters + +The cdp module contains the following parameters: + +| Key | Type | Example | Description | +|------------------|-------------------------|------------------------------------|------------------------------------------------------------------| +| CollateralParams | array (CollateralParam) | [{see below}] | array of params for each enabled collateral type | +| DebtParams | array (DebtParam) | [{see below}] | array of params for each enabled pegged asset | +| GlobalDebtLimit | array (coin) | [{"denom":"usdx","amount":"1000"}] | maximum pegged assets that can be minted across the whole system | +| CircuitBreaker | bool | false | flag to disable user interactions with the system | + +Each CollateralParam has the following parameters: + +| Key | Type | Example | Description | +|------------------|---------------|---------------------------------------------|----------------------------------------------------------------------------------------------------------------| +| Denom | string | "pbnb" | collateral coin denom | +| LiquidationRatio | string (dec) | "1.500000000000000000" | the ratio under which a cdp with this collateral type will be liquidated | +| DebtLimit | array (coin) | [{"denom":"pbnb","amount":"1000000000000"}] | maximum pegged asset that can be minted backed by this collateral type | +| StabilityFee | string (dec) | "1.000000001547126" | per second fee | +| Prefix | number (byte) | 34 | identifier used in store keys - **must** be unique across collateral types | +| MarketID | string | "BNB/USD" | price feed identifier for this collateral type | +| ConversionFactor | string (int) | "6" | 10^_ multiplier to go from external amount (say BTC1.50) to internal representation of that amount (150000000) | + +Each DebtParam has the following parameters: + +| Key | Type | Example | Description | +|------------------|--------------|------------|------------------------------------------------------------------------------------------------------------| +| Denom | string | "usdx" | pegged asset coin denom | +| ReferenceAsset | string | "USD" | asset this asset is pegged to, informational purposes only | +| ConversionFactor | string (int) | "6" | 10^_ multiplier to go from external amount (say $1.50) to internal representation of that amount (1500000) | +| DebtFloor | string (int) | "10000000" | minimum amount of debt that a CDP can contain | diff --git a/x/cdp/spec/README.md b/x/cdp/spec/README.md new file mode 100644 index 00000000..99385048 --- /dev/null +++ b/x/cdp/spec/README.md @@ -0,0 +1,19 @@ + +# `cdp` + +## Table of Contents + +1. **[Concepts](01_concepts.md)** +2. **[State](02_state.md)** +3. **[Messages](03_messages.md)** +4. **[BeginBlock](04_begin_block.md)** +5. **[Events](05_events.md)** +6. **[Parameters](06_params.md)** + +## Overview + +The `x/cdp` module stores and manages Collateralized Debt Positions (or CDPs). + +A CDP enables the creation of a stable asset pegged to an external price (usually US Dollar) by collateralization with another asset. Collateral is locked in a CDP and new stable asset can be minted up to some fraction of the value of the collateral. To unlock the collateral, the debt must be repaid by returning some stable asset to the CDP at which point it will be burned and the collateral unlocked. + +Pegged assets remain fully collateralized by the value locked in CDPs. In the event of price changes, this collateral can be seized and sold off by the system to reclaim and reduce the supply of stable assets. From 535094df96ace59c97214305360a8c2d38c4c792 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Thu, 23 Jan 2020 15:41:45 -0500 Subject: [PATCH 77/99] Fix: Avoid panics when adding cdp collaterals via governance (#329) * fix: remove unnecessary accumulator key * fix: correct test comment * fix: avoid panic when cdp collateral type added --- x/cdp/alias.go | 1 - x/cdp/genesis.go | 1 - x/cdp/integration_test.go | 2 +- x/cdp/keeper/fees.go | 23 +++-------------------- x/cdp/keeper/params.go | 9 +++++++++ x/cdp/types/keys.go | 3 +-- 6 files changed, 14 insertions(+), 25 deletions(-) diff --git a/x/cdp/alias.go b/x/cdp/alias.go index a54b1ada..dd553c82 100644 --- a/x/cdp/alias.go +++ b/x/cdp/alias.go @@ -120,7 +120,6 @@ var ( GovDenomKey = types.GovDenomKey DepositKeyPrefix = types.DepositKeyPrefix PrincipalKeyPrefix = types.PrincipalKeyPrefix - AccumulatorKeyPrefix = types.AccumulatorKeyPrefix PreviousBlockTimeKey = types.PreviousBlockTimeKey KeyGlobalDebtLimit = types.KeyGlobalDebtLimit KeyCollateralParams = types.KeyCollateralParams diff --git a/x/cdp/genesis.go b/x/cdp/genesis.go index ba6fa416..47a654f0 100644 --- a/x/cdp/genesis.go +++ b/x/cdp/genesis.go @@ -34,7 +34,6 @@ func InitGenesis(ctx sdk.Context, k Keeper, pk PricefeedKeeper, gs GenesisState) for _, dp := range gs.Params.DebtParams { k.SetTotalPrincipal(ctx, cp.Denom, dp.Denom, sdk.ZeroInt()) } - k.SetFeeRate(ctx, cp.Denom, cp.StabilityFee) } // add cdps diff --git a/x/cdp/integration_test.go b/x/cdp/integration_test.go index 2ae0ae9b..8ea36252 100644 --- a/x/cdp/integration_test.go +++ b/x/cdp/integration_test.go @@ -210,7 +210,7 @@ func badGenStates() []badGenState { return []badGenState{ badGenState{Genesis: g1, Reason: "duplicate collateral denom"}, badGenState{Genesis: g2, Reason: "duplicate collateral prefix"}, - badGenState{Genesis: g3, Reason: "duplicate collateral prefix"}, + badGenState{Genesis: g3, Reason: "invalid debt limit"}, badGenState{Genesis: g4, Reason: "single collateral exceeds debt limit"}, badGenState{Genesis: g5, Reason: "combined collateral exceeds debt limit"}, badGenState{Genesis: g6, Reason: "duplicate debt denom"}, diff --git a/x/cdp/keeper/fees.go b/x/cdp/keeper/fees.go index 3b5a5679..0c5671f6 100644 --- a/x/cdp/keeper/fees.go +++ b/x/cdp/keeper/fees.go @@ -1,7 +1,6 @@ package keeper import ( - "fmt" "time" "github.com/cosmos/cosmos-sdk/store/prefix" @@ -17,7 +16,7 @@ func (k Keeper) CalculateFees(ctx sdk.Context, principal sdk.Coins, periods sdk. // how fees are calculated: // feesAccumulated = (outstandingDebt * (feeRate^periods)) - outstandingDebt // Note that since we can't do x^y using sdk.Decimal, we are converting to int and using RelativePow - feePerSecond := k.GetFeeRate(ctx, denom) + feePerSecond := k.getFeeRate(ctx, denom) scalar := sdk.NewInt(1000000000000000000) feeRateInt := feePerSecond.Mul(sdk.NewDecFromInt(scalar)).TruncateInt() accumulator := sdk.NewDecFromInt(types.RelativePow(feeRateInt, periods, scalar)).Mul(sdk.SmallestDec()) @@ -55,7 +54,8 @@ func (k Keeper) GetTotalPrincipal(ctx sdk.Context, collateralDenom string, princ store := prefix.NewStore(ctx.KVStore(k.key), types.PrincipalKeyPrefix) bz := store.Get([]byte(collateralDenom + principalDenom)) if bz == nil { - panic(fmt.Sprintf("total principal of %s for %s collateral not set in genesis", principalDenom, collateralDenom)) + k.SetTotalPrincipal(ctx, collateralDenom, principalDenom, sdk.ZeroInt()) + return sdk.ZeroInt() } k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &total) return total @@ -67,23 +67,6 @@ func (k Keeper) SetTotalPrincipal(ctx sdk.Context, collateralDenom string, princ store.Set([]byte(collateralDenom+principalDenom), k.cdc.MustMarshalBinaryLengthPrefixed(total)) } -// GetFeeRate returns the per second fee rate for the input denom -func (k Keeper) GetFeeRate(ctx sdk.Context, denom string) (fee sdk.Dec) { - store := prefix.NewStore(ctx.KVStore(k.key), types.AccumulatorKeyPrefix) - bz := store.Get([]byte(denom)) - if bz == nil { - panic(fmt.Sprintf("fee rate for %s not set in genesis", denom)) - } - k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &fee) - return fee -} - -// SetFeeRate sets the per second fee rate for the input denom -func (k Keeper) SetFeeRate(ctx sdk.Context, denom string, fee sdk.Dec) { - store := prefix.NewStore(ctx.KVStore(k.key), types.AccumulatorKeyPrefix) - store.Set([]byte(denom), k.cdc.MustMarshalBinaryLengthPrefixed(fee)) -} - // GetPreviousBlockTime get the blocktime for the previous block func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time, found bool) { store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey) diff --git a/x/cdp/keeper/params.go b/x/cdp/keeper/params.go index d0bd8a06..1d13410b 100644 --- a/x/cdp/keeper/params.go +++ b/x/cdp/keeper/params.go @@ -94,3 +94,12 @@ func (k Keeper) getAuctionSize(ctx sdk.Context, denom string) sdk.Int { } return cp.AuctionSize } + +// GetFeeRate returns the per second fee rate for the input denom +func (k Keeper) getFeeRate(ctx sdk.Context, denom string) (fee sdk.Dec) { + collalateralParam, found := k.GetCollateral(ctx, denom) + if !found { + panic(fmt.Sprintf("could not get fee rate for %s, collateral not found", denom)) + } + return collalateralParam.StabilityFee +} diff --git a/x/cdp/types/keys.go b/x/cdp/types/keys.go index 180ad38e..6f34524a 100644 --- a/x/cdp/types/keys.go +++ b/x/cdp/types/keys.go @@ -54,8 +54,7 @@ var ( GovDenomKey = []byte{0x05} DepositKeyPrefix = []byte{0x06} PrincipalKeyPrefix = []byte{0x07} - AccumulatorKeyPrefix = []byte{0x08} - PreviousBlockTimeKey = []byte{0x09} + PreviousBlockTimeKey = []byte{0x08} ) var lenPositiveDec = len(SortableDecBytes(sdk.OneDec())) From d018aaebcfbddde6157de8d45f2582ca920dd1b0 Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Thu, 23 Jan 2020 22:41:59 +0000 Subject: [PATCH 78/99] fix typo (#330) --- x/cdp/keeper/deposit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/cdp/keeper/deposit.go b/x/cdp/keeper/deposit.go index 35258c9b..955f2302 100644 --- a/x/cdp/keeper/deposit.go +++ b/x/cdp/keeper/deposit.go @@ -98,7 +98,7 @@ func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner sdk.AccAddress, deposi cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees) cdp.FeesUpdated = ctx.BlockTime() cdp.Collateral = cdp.Collateral.Sub(collateral) - collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, collateral, cdp.Principal.Add(cdp.AccumulatedFees)) + collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees)) k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio) deposit.Amount = deposit.Amount.Sub(collateral) From 33ad221e5f7053a2364791fb2c189b7b7da3d33c Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Fri, 24 Jan 2020 11:39:09 +0000 Subject: [PATCH 79/99] Fix gov denom typo (#333) --- x/cdp/keeper/cdp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/cdp/keeper/cdp.go b/x/cdp/keeper/cdp.go index 0675154d..09800b2c 100644 --- a/x/cdp/keeper/cdp.go +++ b/x/cdp/keeper/cdp.go @@ -305,7 +305,7 @@ func (k Keeper) GetDebtDenom(ctx sdk.Context) (denom string) { // GetGovDenom returns the denom of debt in the system func (k Keeper) GetGovDenom(ctx sdk.Context) (denom string) { - store := prefix.NewStore(ctx.KVStore(k.key), types.DebtDenomKey) + store := prefix.NewStore(ctx.KVStore(k.key), types.GovDenomKey) bz := store.Get([]byte{}) k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &denom) return From d24f364ed3aeb3a0936d215419aa5583ef2dada3 Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Fri, 24 Jan 2020 11:39:52 +0000 Subject: [PATCH 80/99] add weighted address tags (#336) --- x/auction/types/auctions.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 583d9765..6391138e 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -222,8 +222,8 @@ func NewCollateralAuction(seller string, lot sdk.Coin, endTime time.Time, maxBid // WeightedAddresses is a type for storing some addresses and associated weights. type WeightedAddresses struct { - Addresses []sdk.AccAddress - Weights []sdk.Int + Addresses []sdk.AccAddress `json:"addresses" yaml:"addresses"` + Weights []sdk.Int `json:"weights" yaml:"weights"` } // NewWeightedAddresses returns a new list addresses with weights. From 07c1a8c126262cc42d86b8e7aac411b7a000f9c7 Mon Sep 17 00:00:00 2001 From: Ruaridh Date: Fri, 24 Jan 2020 18:31:33 +0000 Subject: [PATCH 81/99] fix typo (#332) --- x/cdp/keeper/deposit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/cdp/keeper/deposit.go b/x/cdp/keeper/deposit.go index 955f2302..38b8dd85 100644 --- a/x/cdp/keeper/deposit.go +++ b/x/cdp/keeper/deposit.go @@ -47,7 +47,7 @@ func (k Keeper) DepositCollateral(ctx sdk.Context, owner sdk.AccAddress, deposit cdp.AccumulatedFees = cdp.AccumulatedFees.Add(fees) cdp.FeesUpdated = ctx.BlockTime() cdp.Collateral = cdp.Collateral.Add(collateral) - collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, collateral, cdp.Principal.Add(cdp.AccumulatedFees)) + collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Principal.Add(cdp.AccumulatedFees)) k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio) return nil } From 99be9dd8abc348e63ab824ac39cbae2e5094e608 Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Fri, 24 Jan 2020 13:44:13 -0800 Subject: [PATCH 82/99] [WIP] example proposal.json files for param changes (#331) * proposal examples, copy of cdptestnet genesis * co-located genesis files with their respective proposals in contrib * added genesis file for updatedebtlimit * Removed legacy genesis example * added sample genesis files as starting states of auction tests * auction proposal json files (untested) * max auction duration and bid duration tested * requested changes --- .../genesis_test_collateral_auctions.json | 360 +++++++++++++++++ .../genesis_test_debt_auctions.json | 360 +++++++++++++++++ .../addcollateral/addcollateral.json | 50 +++ .../addcollateral/addcollateral2.json | 72 ++++ .../addcollateral/genesis_addcollateral.json | 344 +++++++++++++++++ .../addmarket/addmarket.json | 45 +++ .../addmarket/genesis_addmarket.json | 359 +++++++++++++++++ .../updatebidduration/updatebidduration.json | 17 + .../genesis_updatedebtlimit.json | 361 ++++++++++++++++++ .../updatedebtlimit/updatedebtlimit.json | 34 ++ .../updatemaxauctionduration.json | 17 + 11 files changed, 2019 insertions(+) create mode 100644 contrib/genesis_examples/genesis_test_collateral_auctions.json create mode 100644 contrib/genesis_examples/genesis_test_debt_auctions.json create mode 100644 contrib/proposal_examples/addcollateral/addcollateral.json create mode 100644 contrib/proposal_examples/addcollateral/addcollateral2.json create mode 100644 contrib/proposal_examples/addcollateral/genesis_addcollateral.json create mode 100644 contrib/proposal_examples/addmarket/addmarket.json create mode 100644 contrib/proposal_examples/addmarket/genesis_addmarket.json create mode 100644 contrib/proposal_examples/updatebidduration/updatebidduration.json create mode 100644 contrib/proposal_examples/updatedebtlimit/genesis_updatedebtlimit.json create mode 100644 contrib/proposal_examples/updatedebtlimit/updatedebtlimit.json create mode 100644 contrib/proposal_examples/updatemaxauctionduration/updatemaxauctionduration.json diff --git a/contrib/genesis_examples/genesis_test_collateral_auctions.json b/contrib/genesis_examples/genesis_test_collateral_auctions.json new file mode 100644 index 00000000..79d1cb2d --- /dev/null +++ b/contrib/genesis_examples/genesis_test_collateral_auctions.json @@ -0,0 +1,360 @@ +{ + "genesis_time": "2020-01-22T22:39:50.083273Z", + "chain_id": "testing", + "consensus_params": { + "block": { + "max_bytes": "200000", + "max_gas": "2000000", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age": "1000000" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + } + }, + "app_hash": "", + "app_state": { + "slashing": { + "missed_blocks": {}, + "params": { + "downtime_jail_duration": "600000000000", + "max_evidence_age": "3600000000000", + "min_signed_per_window": "0.010000000000000000", + "signed_blocks_window": "1000", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.000100000000000000" + }, + "signing_infos": {} + }, + "params": null, + "bank": { + "send_enabled": true + }, + "crisis": { + "constant_fee": { + "amount": "1333000000", + "denom": "ukava" + } + }, + "cdp": { + "cdps": [], + "debt_denom": "debt", + "deposits": [], + "gov_denom": "ukava", + "params": { + "circuit_breaker": false, + "collateral_params": [ + { + "auction_size": "10000000", + "conversion_factor": "8", + "debt_limit": [ + { + "amount": "1000000000000", + "denom": "usdx" + } + ], + "denom": "btc", + "liquidation_penalty": "0.05", + "liquidation_ratio": "1.5", + "market_id": "btc:usd", + "prefix": 0, + "stability_fee": "1.0000000007829977" + }, + { + "auction_size": "10000000", + "conversion_factor": "6", + "debt_limit": [ + { + "amount": "10000000", + "denom": "usdx" + } + ], + "denom": "xrp", + "liquidation_penalty": "0.1", + "liquidation_ratio": "2.0", + "market_id": "xrp:usd", + "prefix": 1, + "stability_fee": "1.0000000007829977" + } + ], + "debt_auction_threshold": "1000000000", + "debt_params": [ + { + "conversion_factor": "6", + "debt_floor": "10000000", + "debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "denom": "usdx", + "reference_asset": "usd" + } + ], + "global_debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "surplus_auction_threshold": "1000000000" + }, + "previous_block_time": "1970-01-01T00:00:00Z", + "starting_cdp_id": "1" + }, + "auction": { + "auctions": [], + "next_auction_id": "0", + "params": { + "bid_duration": "150000000000", + "max_auction_duration": "400000000000" + } + }, + "genutil": { + "gentxs": [ + { + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/MsgCreateValidator", + "value": { + "description": { + "moniker": "validator", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.100000000000000000", + "max_rate": "0.200000000000000000", + "max_change_rate": "0.010000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "validator_address": "kavavaloper15qdefkmwswysgg4qxgqpqr35k3m49pkx8yhpte", + "pubkey": "kavavalconspub1zcjduepq0ykwlzr3guncl5l8npqa5lmq8kc5y2aeqp8zc8e72t3stxujxvxsxqjqwt", + "value": { + "denom": "ukava", + "amount": "100000000" + } + } + } + ], + "fee": { + "amount": [], + "gas": "200000" + }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "Az740XKIPCJtnZLmJfktTfhsEStEJE3n2iRVyJ3wko43" + }, + "signature": "llQmiLi+BQDKT2D2fOSr+eiDTXnce9xvZ7xls8EgoLIn+vz3HpyaTEoUQVes0kXbHfwJVSTz8jN6Z7HWYeEDlw==" + } + ], + "memo": "75917fc405a9af4ed44161a38456ce290a588100@10.21.104.217:26656" + } + } + ] + }, + "supply": { + "supply": [] + }, + "validatorvesting": { + "previous_block_time": "1970-01-01T00:00:00Z" + }, + "mint": { + "minter": { + "annual_provisions": "0.000000000000000000", + "inflation": "0.020000000000000000" + }, + "params": { + "blocks_per_year": "6311520", + "goal_bonded": "0.670000000000000000", + "inflation_max": "0.130000000000000000", + "inflation_min": "0.010000000000000000", + "inflation_rate_change": "0.130000000000000000", + "mint_denom": "ukava" + } + }, + "distribution": { + "base_proposer_reward": "0.010000000000000000", + "bonus_proposer_reward": "0.040000000000000000", + "community_tax": "0.000000000000000000", + "delegator_starting_infos": [], + "delegator_withdraw_infos": [], + "fee_pool": { + "community_pool": [] + }, + "outstanding_rewards": [], + "previous_proposer": "", + "validator_accumulated_commissions": [], + "validator_current_rewards": [], + "validator_historical_rewards": [], + "validator_slash_events": [], + "withdraw_addr_enabled": true + }, + "auth": { + "accounts": [ + { + "type": "cosmos-sdk/Account", + "value": { + "account_number": "0", + "address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "coins": [ + { + "amount": "10000000000", + "denom": "btc" + }, + { + "amount": "9000000000", + "denom": "ukava" + }, + { + "amount": "90000000000", + "denom": "xrp" + } + ], + "public_key": null, + "sequence": "0" + } + }, + { + "type": "cosmos-sdk/Account", + "value": { + "account_number": "0", + "address": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj", + "coins": [ + { + "amount": "10000000000", + "denom": "btc" + }, + { + "amount": "9000000000", + "denom": "ukava" + }, + { + "amount": "90000000000", + "denom": "xrp" + } + ], + "public_key": null, + "sequence": "0" + } + }, + { + "type": "cosmos-sdk/Account", + "value": { + "account_number": "0", + "address": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03", + "coins": [ + { + "amount": "10000000000", + "denom": "btc" + }, + { + "amount": "9000000000", + "denom": "ukava" + }, + { + "amount": "90000000000", + "denom": "xrp" + } + ], + "public_key": null, + "sequence": "0" + } + } + ], + "params": { + "max_memo_characters": "256", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10" + } + }, + "staking": { + "delegations": null, + "exported": false, + "last_total_power": "0", + "last_validator_powers": null, + "params": { + "bond_denom": "ukava", + "max_entries": 7, + "max_validators": 100, + "unbonding_time": "3600000000000" + }, + "redelegations": null, + "unbonding_delegations": null, + "validators": null + }, + "pricefeed": { + "params": { + "markets": [ + { + "active": true, + "base_asset": "xrp", + "market_id": "xrp:usd", + "oracles": [ + "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw" + ], + "quote_asset": "usd" + }, + { + "active": true, + "base_asset": "btc", + "market_id": "btc:usd", + "oracles": [], + "quote_asset": "usd" + } + ] + }, + "posted_prices": [ + { + "expiry": "2050-01-01T00:00:00Z", + "market_id": "btc:usd", + "oracle_address": "", + "price": "8700.0" + }, + { + "expiry": "2050-01-01T00:00:00Z", + "market_id": "xrp:usd", + "oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "price": "0.25" + } + ] + }, + "gov": { + "deposit_params": { + "max_deposit_period": "300000000000", + "min_deposit": [ + { + "amount": "1000000", + "denom": "ukava" + } + ] + }, + "deposits": null, + "proposals": null, + "starting_proposal_id": "1", + "tally_params": { + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto": "0.334000000000000000" + }, + "votes": null, + "voting_params": { + "voting_period": "300000000000" + } + } + } +} diff --git a/contrib/genesis_examples/genesis_test_debt_auctions.json b/contrib/genesis_examples/genesis_test_debt_auctions.json new file mode 100644 index 00000000..6187986a --- /dev/null +++ b/contrib/genesis_examples/genesis_test_debt_auctions.json @@ -0,0 +1,360 @@ +{ + "genesis_time": "2020-01-22T22:39:50.083273Z", + "chain_id": "testing", + "consensus_params": { + "block": { + "max_bytes": "200000", + "max_gas": "2000000", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age": "1000000" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + } + }, + "app_hash": "", + "app_state": { + "slashing": { + "missed_blocks": {}, + "params": { + "downtime_jail_duration": "600000000000", + "max_evidence_age": "3600000000000", + "min_signed_per_window": "0.010000000000000000", + "signed_blocks_window": "1000", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.000100000000000000" + }, + "signing_infos": {} + }, + "params": null, + "bank": { + "send_enabled": true + }, + "crisis": { + "constant_fee": { + "amount": "1333000000", + "denom": "ukava" + } + }, + "cdp": { + "cdps": [], + "debt_denom": "ukava", + "deposits": [], + "gov_denom": "ukava", + "params": { + "circuit_breaker": false, + "collateral_params": [ + { + "auction_size": "10000000000", + "conversion_factor": "8", + "debt_limit": [ + { + "amount": "1000000000", + "denom": "usdx" + } + ], + "denom": "btc", + "liquidation_penalty": "0.05", + "liquidation_ratio": "1.5", + "market_id": "btc:usd", + "prefix": 0, + "stability_fee": "1.0000000007829977" + }, + { + "auction_size": "100000000", + "conversion_factor": "6", + "debt_limit": [ + { + "amount": "10000000", + "denom": "usdx" + } + ], + "denom": "xrp", + "liquidation_penalty": "0.1", + "liquidation_ratio": "2.0", + "market_id": "xrp:usd", + "prefix": 1, + "stability_fee": "1.0000000007829977" + } + ], + "debt_auction_threshold": "9000000", + "debt_params": [ + { + "conversion_factor": "6", + "debt_floor": "10000000", + "debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "denom": "usdx", + "reference_asset": "usd" + } + ], + "global_debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "surplus_auction_threshold": "9000000" + }, + "previous_block_time": "1970-01-01T00:00:00Z", + "starting_cdp_id": "1" + }, + "auction": { + "auctions": [], + "next_auction_id": "0", + "params": { + "bid_duration": "100000000000", + "max_auction_duration": "100000000000" + } + }, + "genutil": { + "gentxs": [ + { + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/MsgCreateValidator", + "value": { + "description": { + "moniker": "validator", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.100000000000000000", + "max_rate": "0.200000000000000000", + "max_change_rate": "0.010000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "validator_address": "kavavaloper15qdefkmwswysgg4qxgqpqr35k3m49pkx8yhpte", + "pubkey": "kavavalconspub1zcjduepq0ykwlzr3guncl5l8npqa5lmq8kc5y2aeqp8zc8e72t3stxujxvxsxqjqwt", + "value": { + "denom": "ukava", + "amount": "100000000" + } + } + } + ], + "fee": { + "amount": [], + "gas": "200000" + }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "Az740XKIPCJtnZLmJfktTfhsEStEJE3n2iRVyJ3wko43" + }, + "signature": "llQmiLi+BQDKT2D2fOSr+eiDTXnce9xvZ7xls8EgoLIn+vz3HpyaTEoUQVes0kXbHfwJVSTz8jN6Z7HWYeEDlw==" + } + ], + "memo": "75917fc405a9af4ed44161a38456ce290a588100@10.21.104.217:26656" + } + } + ] + }, + "supply": { + "supply": [] + }, + "validatorvesting": { + "previous_block_time": "1970-01-01T00:00:00Z" + }, + "mint": { + "minter": { + "annual_provisions": "0.000000000000000000", + "inflation": "0.020000000000000000" + }, + "params": { + "blocks_per_year": "6311520", + "goal_bonded": "0.670000000000000000", + "inflation_max": "0.130000000000000000", + "inflation_min": "0.010000000000000000", + "inflation_rate_change": "0.130000000000000000", + "mint_denom": "ukava" + } + }, + "distribution": { + "base_proposer_reward": "0.010000000000000000", + "bonus_proposer_reward": "0.040000000000000000", + "community_tax": "0.000000000000000000", + "delegator_starting_infos": [], + "delegator_withdraw_infos": [], + "fee_pool": { + "community_pool": [] + }, + "outstanding_rewards": [], + "previous_proposer": "", + "validator_accumulated_commissions": [], + "validator_current_rewards": [], + "validator_historical_rewards": [], + "validator_slash_events": [], + "withdraw_addr_enabled": true + }, + "auth": { + "accounts": [ + { + "type": "cosmos-sdk/Account", + "value": { + "account_number": "0", + "address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "coins": [ + { + "amount": "10000000000", + "denom": "btc" + }, + { + "amount": "9000000000", + "denom": "ukava" + }, + { + "amount": "90000000000", + "denom": "xrp" + } + ], + "public_key": null, + "sequence": "0" + } + }, + { + "type": "cosmos-sdk/Account", + "value": { + "account_number": "0", + "address": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj", + "coins": [ + { + "amount": "10000000000", + "denom": "btc" + }, + { + "amount": "9000000000", + "denom": "ukava" + }, + { + "amount": "90000000000", + "denom": "xrp" + } + ], + "public_key": null, + "sequence": "0" + } + }, + { + "type": "cosmos-sdk/Account", + "value": { + "account_number": "0", + "address": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03", + "coins": [ + { + "amount": "10000000000", + "denom": "btc" + }, + { + "amount": "9000000000", + "denom": "ukava" + }, + { + "amount": "90000000000", + "denom": "xrp" + } + ], + "public_key": null, + "sequence": "0" + } + } + ], + "params": { + "max_memo_characters": "256", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10" + } + }, + "staking": { + "delegations": null, + "exported": false, + "last_total_power": "0", + "last_validator_powers": null, + "params": { + "bond_denom": "ukava", + "max_entries": 7, + "max_validators": 100, + "unbonding_time": "3600000000000" + }, + "redelegations": null, + "unbonding_delegations": null, + "validators": null + }, + "pricefeed": { + "params": { + "markets": [ + { + "active": true, + "base_asset": "xrp", + "market_id": "xrp:usd", + "oracles": [ + "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw" + ], + "quote_asset": "usd" + }, + { + "active": true, + "base_asset": "btc", + "market_id": "btc:usd", + "oracles": [], + "quote_asset": "usd" + } + ] + }, + "posted_prices": [ + { + "expiry": "2050-01-01T00:00:00Z", + "market_id": "btc:usd", + "oracle_address": "", + "price": "8700.0" + }, + { + "expiry": "2050-01-01T00:00:00Z", + "market_id": "xrp:usd", + "oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "price": "0.25" + } + ] + }, + "gov": { + "deposit_params": { + "max_deposit_period": "300000000000", + "min_deposit": [ + { + "amount": "1000000", + "denom": "ukava" + } + ] + }, + "deposits": null, + "proposals": null, + "starting_proposal_id": "1", + "tally_params": { + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto": "0.334000000000000000" + }, + "votes": null, + "voting_params": { + "voting_period": "300000000000" + } + } + } +} diff --git a/contrib/proposal_examples/addcollateral/addcollateral.json b/contrib/proposal_examples/addcollateral/addcollateral.json new file mode 100644 index 00000000..0da6f526 --- /dev/null +++ b/contrib/proposal_examples/addcollateral/addcollateral.json @@ -0,0 +1,50 @@ +{ + "title": "CDP Param Change", + "description": "Add collateral XRP", + "changes": [ + { + "subspace": "cdp", + "key": "CollateralParams", + "value": [ + { + "auction_size": "50000000", + "conversion_factor": "6", + "debt_limit": [ + { + "amount": "1000000000", + "denom": "usdx" + } + ], + "denom": "xrp", + "liquidation_penalty": "0.05", + "liquidation_ratio": "2.0", + "market_id": "xrp:usd", + "prefix": 0, + "stability_fee": "1.000000001547126" + }, + { + "auction_size": "10000000", + "conversion_factor": "8", + "debt_limit": [ + { + "amount": "1000000000000", + "denom": "usdx" + } + ], + "denom": "btc", + "liquidation_penalty": "0.05", + "liquidation_ratio": "1.5", + "market_id": "btc:usd", + "prefix": 1, + "stability_fee": "1.0000000007829977" + } + ] + } + ], + "deposit": [ + { + "denom": "ukava", + "amount": "1000000" + } + ] + } \ No newline at end of file diff --git a/contrib/proposal_examples/addcollateral/addcollateral2.json b/contrib/proposal_examples/addcollateral/addcollateral2.json new file mode 100644 index 00000000..b961b894 --- /dev/null +++ b/contrib/proposal_examples/addcollateral/addcollateral2.json @@ -0,0 +1,72 @@ +{ + "title": "CDP Param Change", + "description": "Add collateral XRP", + "changes": [ + { + "subspace": "cdp", + "key": "CollateralParams", + "value": [ + { + "auction_size": "50000000", + "conversion_factor": "6", + "debt_limit": [ + { + "amount": "1000000000", + "denom": "usdx" + } + ], + "denom": "xrp", + "liquidation_penalty": "0.05", + "liquidation_ratio": "2.0", + "market_id": "xrp:usd", + "prefix": 0, + "stability_fee": "1.000000001547126" + }, + { + "auction_size": "10000000", + "conversion_factor": "8", + "debt_limit": [ + { + "amount": "1000000000000", + "denom": "usdx" + } + ], + "denom": "btc", + "liquidation_penalty": "0.05", + "liquidation_ratio": "1.5", + "market_id": "btc:usd", + "prefix": 1, + "stability_fee": "1.0000000007829977" + } + ] + }, + { + "subspace": "cdp", + "key": "DebtParams", + "value": [ + { + "conversion_factor": "6", + "debt_floor": "10000000", + "denom": "usdx", + "reference_asset": "usd" + } + ] + }, + { + "subspace": "cdp", + "key": "GlobalDebtLimit", + "value": [ + { + "amount": "3000000000000", + "denom": "usdx" + } + ] + } + ], + "deposit": [ + { + "denom": "ukava", + "amount": "1000000" + } + ] +} \ No newline at end of file diff --git a/contrib/proposal_examples/addcollateral/genesis_addcollateral.json b/contrib/proposal_examples/addcollateral/genesis_addcollateral.json new file mode 100644 index 00000000..c798f8c9 --- /dev/null +++ b/contrib/proposal_examples/addcollateral/genesis_addcollateral.json @@ -0,0 +1,344 @@ +{ + "genesis_time": "2020-01-22T22:39:50.083273Z", + "chain_id": "testing", + "consensus_params": { + "block": { + "max_bytes": "200000", + "max_gas": "2000000", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age": "1000000" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + } + }, + "app_hash": "", + "app_state": { + "slashing": { + "missed_blocks": {}, + "params": { + "downtime_jail_duration": "600000000000", + "max_evidence_age": "3600000000000", + "min_signed_per_window": "0.010000000000000000", + "signed_blocks_window": "1000", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.000100000000000000" + }, + "signing_infos": {} + }, + "params": null, + "bank": { + "send_enabled": true + }, + "crisis": { + "constant_fee": { + "amount": "1333000000", + "denom": "ukava" + } + }, + "cdp": { + "cdps": [], + "debt_denom": "debt", + "deposits": [], + "gov_denom": "ukava", + "params": { + "circuit_breaker": false, + "collateral_params": [ + { + "auction_size": "10000000", + "conversion_factor": "8", + "debt_limit": [ + { + "amount": "1000000000000", + "denom": "usdx" + } + ], + "denom": "btc", + "liquidation_penalty": "0.05", + "liquidation_ratio": "1.5", + "market_id": "btc:usd", + "prefix": 1, + "stability_fee": "1.0000000007829977" + } + ], + "debt_auction_threshold": "1000000000", + "debt_params": [ + { + "conversion_factor": "6", + "debt_floor": "10000000", + "debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "denom": "usdx", + "reference_asset": "usd" + } + ], + "global_debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "surplus_auction_threshold": "1000000000" + }, + "previous_block_time": "1970-01-01T00:00:00Z", + "starting_cdp_id": "1" + }, + "auction": { + "auctions": [], + "next_auction_id": "0", + "params": { + "bid_duration": "600000000000", + "max_auction_duration": "172800000000000" + } + }, + "genutil": { + "gentxs": [ + { + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/MsgCreateValidator", + "value": { + "description": { + "moniker": "validator", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.100000000000000000", + "max_rate": "0.200000000000000000", + "max_change_rate": "0.010000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "validator_address": "kavavaloper15qdefkmwswysgg4qxgqpqr35k3m49pkx8yhpte", + "pubkey": "kavavalconspub1zcjduepq0ykwlzr3guncl5l8npqa5lmq8kc5y2aeqp8zc8e72t3stxujxvxsxqjqwt", + "value": { + "denom": "ukava", + "amount": "100000000" + } + } + } + ], + "fee": { + "amount": [], + "gas": "200000" + }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "Az740XKIPCJtnZLmJfktTfhsEStEJE3n2iRVyJ3wko43" + }, + "signature": "llQmiLi+BQDKT2D2fOSr+eiDTXnce9xvZ7xls8EgoLIn+vz3HpyaTEoUQVes0kXbHfwJVSTz8jN6Z7HWYeEDlw==" + } + ], + "memo": "75917fc405a9af4ed44161a38456ce290a588100@10.21.104.217:26656" + } + } + ] + }, + "supply": { + "supply": [] + }, + "validatorvesting": { + "previous_block_time": "1970-01-01T00:00:00Z" + }, + "mint": { + "minter": { + "annual_provisions": "0.000000000000000000", + "inflation": "0.020000000000000000" + }, + "params": { + "blocks_per_year": "6311520", + "goal_bonded": "0.670000000000000000", + "inflation_max": "0.130000000000000000", + "inflation_min": "0.010000000000000000", + "inflation_rate_change": "0.130000000000000000", + "mint_denom": "ukava" + } + }, + "distribution": { + "base_proposer_reward": "0.010000000000000000", + "bonus_proposer_reward": "0.040000000000000000", + "community_tax": "0.000000000000000000", + "delegator_starting_infos": [], + "delegator_withdraw_infos": [], + "fee_pool": { + "community_pool": [] + }, + "outstanding_rewards": [], + "previous_proposer": "", + "validator_accumulated_commissions": [], + "validator_current_rewards": [], + "validator_historical_rewards": [], + "validator_slash_events": [], + "withdraw_addr_enabled": true + }, + "auth": { + "accounts": [ + { + "type": "cosmos-sdk/Account", + "value": { + "account_number": "0", + "address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "coins": [ + { + "amount": "10000000000", + "denom": "btc" + }, + { + "amount": "9000000000", + "denom": "ukava" + }, + { + "amount": "90000000000", + "denom": "xrp" + } + ], + "public_key": null, + "sequence": "0" + } + }, + { + "type": "cosmos-sdk/Account", + "value": { + "account_number": "0", + "address": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj", + "coins": [ + { + "amount": "10000000000", + "denom": "btc" + }, + { + "amount": "9000000000", + "denom": "ukava" + }, + { + "amount": "90000000000", + "denom": "xrp" + } + ], + "public_key": null, + "sequence": "0" + } + }, + { + "type": "cosmos-sdk/Account", + "value": { + "account_number": "0", + "address": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03", + "coins": [ + { + "amount": "10000000000", + "denom": "btc" + }, + { + "amount": "9000000000", + "denom": "ukava" + }, + { + "amount": "90000000000", + "denom": "xrp" + } + ], + "public_key": null, + "sequence": "0" + } + } + ], + "params": { + "max_memo_characters": "256", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10" + } + }, + "staking": { + "delegations": null, + "exported": false, + "last_total_power": "0", + "last_validator_powers": null, + "params": { + "bond_denom": "ukava", + "max_entries": 7, + "max_validators": 100, + "unbonding_time": "3600000000000" + }, + "redelegations": null, + "unbonding_delegations": null, + "validators": null + }, + "pricefeed": { + "params": { + "markets": [ + { + "active": true, + "base_asset": "xrp", + "market_id": "xrp:usd", + "oracles": [ + "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw" + ], + "quote_asset": "usd" + }, + { + "active": true, + "base_asset": "btc", + "market_id": "btc:usd", + "oracles": [], + "quote_asset": "usd" + } + ] + }, + "posted_prices": [ + { + "expiry": "2050-01-01T00:00:00Z", + "market_id": "btc:usd", + "oracle_address": "", + "price": "8700.0" + }, + { + "expiry": "2050-01-01T00:00:00Z", + "market_id": "xrp:usd", + "oracle_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "price": "0.25" + } + ] + }, + "gov": { + "deposit_params": { + "max_deposit_period": "300000000000", + "min_deposit": [ + { + "amount": "1000000", + "denom": "ukava" + } + ] + }, + "deposits": null, + "proposals": null, + "starting_proposal_id": "1", + "tally_params": { + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto": "0.334000000000000000" + }, + "votes": null, + "voting_params": { + "voting_period": "300000000000" + } + } + } +} diff --git a/contrib/proposal_examples/addmarket/addmarket.json b/contrib/proposal_examples/addmarket/addmarket.json new file mode 100644 index 00000000..7f92c432 --- /dev/null +++ b/contrib/proposal_examples/addmarket/addmarket.json @@ -0,0 +1,45 @@ +{ + "title": "Pricefeed Param Change", + "description": "Add market bnb-usd", + "changes": [ + { + "subspace": "pricefeed", + "key": "Markets", + "value": [ + { + "active": true, + "base_asset": "btc", + "market_id": "btc-usd", + "oracles": [ + "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw" + ], + "quote_asset": "usd" + }, + { + "active": true, + "base_asset": "xrp", + "market_id": "xrp-usd", + "oracles": [ + "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw" + ], + "quote_asset": "usd" + }, + { + "active": true, + "base_asset": "bnb", + "market_id": "bnb-usd", + "oracles": [ + "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw" + ], + "quote_asset": "usd" + } + ] + } + ], + "deposit": [ + { + "denom": "ukava", + "amount": "1000000" + } + ] +} \ No newline at end of file diff --git a/contrib/proposal_examples/addmarket/genesis_addmarket.json b/contrib/proposal_examples/addmarket/genesis_addmarket.json new file mode 100644 index 00000000..c95298ae --- /dev/null +++ b/contrib/proposal_examples/addmarket/genesis_addmarket.json @@ -0,0 +1,359 @@ +{ + "genesis_time": "2020-01-22T22:39:50.083273Z", + "chain_id": "testing", + "consensus_params": { + "block": { + "max_bytes": "200000", + "max_gas": "2000000", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age": "1000000" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + } + }, + "app_hash": "", + "app_state": { + "slashing": { + "missed_blocks": {}, + "params": { + "downtime_jail_duration": "600000000000", + "max_evidence_age": "3600000000000", + "min_signed_per_window": "0.010000000000000000", + "signed_blocks_window": "1000", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.000100000000000000" + }, + "signing_infos": {} + }, + "params": null, + "bank": { + "send_enabled": true + }, + "crisis": { + "constant_fee": { + "amount": "1333000000", + "denom": "ukava" + } + }, + "cdp": { + "cdps": [], + "debt_denom": "debt", + "deposits": [], + "gov_denom": "ukava", + "params": { + "circuit_breaker": false, + "collateral_params": [ + { + "auction_size": "5000000000", + "conversion_factor": "6", + "debt_limit": [ + { + "amount": "1000000000000", + "denom": "usdx" + } + ], + "denom": "xrp", + "liquidation_penalty": "0.05", + "liquidation_ratio": "2.0", + "market_id": "xrp:usd", + "prefix": 0, + "stability_fee": "1.000000001547126" + }, + { + "auction_size": "10000000", + "conversion_factor": "8", + "debt_limit": [ + { + "amount": "1000000000000", + "denom": "usdx" + } + ], + "denom": "btc", + "liquidation_penalty": "0.05", + "liquidation_ratio": "1.5", + "market_id": "btc:usd", + "prefix": 1, + "stability_fee": "1.0000000007829977" + } + ], + "debt_auction_threshold": "1000000000", + "debt_params": [ + { + "conversion_factor": "6", + "debt_floor": "10000000", + "debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "denom": "usdx", + "reference_asset": "usd" + } + ], + "global_debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "surplus_auction_threshold": "1000000000" + }, + "previous_block_time": "1970-01-01T00:00:00Z", + "starting_cdp_id": "1" + }, + "auction": { + "auctions": [], + "next_auction_id": "0", + "params": { + "bid_duration": "600000000000", + "max_auction_duration": "172800000000000" + } + }, + "genutil": { + "gentxs": [ + { + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/MsgCreateValidator", + "value": { + "description": { + "moniker": "validator", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.100000000000000000", + "max_rate": "0.200000000000000000", + "max_change_rate": "0.010000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "validator_address": "kavavaloper15qdefkmwswysgg4qxgqpqr35k3m49pkx8yhpte", + "pubkey": "kavavalconspub1zcjduepq0ykwlzr3guncl5l8npqa5lmq8kc5y2aeqp8zc8e72t3stxujxvxsxqjqwt", + "value": { + "denom": "ukava", + "amount": "100000000" + } + } + } + ], + "fee": { + "amount": [], + "gas": "200000" + }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "Az740XKIPCJtnZLmJfktTfhsEStEJE3n2iRVyJ3wko43" + }, + "signature": "llQmiLi+BQDKT2D2fOSr+eiDTXnce9xvZ7xls8EgoLIn+vz3HpyaTEoUQVes0kXbHfwJVSTz8jN6Z7HWYeEDlw==" + } + ], + "memo": "75917fc405a9af4ed44161a38456ce290a588100@10.21.104.217:26656" + } + } + ] + }, + "supply": { + "supply": [] + }, + "validatorvesting": { + "previous_block_time": "1970-01-01T00:00:00Z" + }, + "mint": { + "minter": { + "annual_provisions": "0.000000000000000000", + "inflation": "0.020000000000000000" + }, + "params": { + "blocks_per_year": "6311520", + "goal_bonded": "0.670000000000000000", + "inflation_max": "0.130000000000000000", + "inflation_min": "0.010000000000000000", + "inflation_rate_change": "0.130000000000000000", + "mint_denom": "ukava" + } + }, + "distribution": { + "base_proposer_reward": "0.010000000000000000", + "bonus_proposer_reward": "0.040000000000000000", + "community_tax": "0.000000000000000000", + "delegator_starting_infos": [], + "delegator_withdraw_infos": [], + "fee_pool": { + "community_pool": [] + }, + "outstanding_rewards": [], + "previous_proposer": "", + "validator_accumulated_commissions": [], + "validator_current_rewards": [], + "validator_historical_rewards": [], + "validator_slash_events": [], + "withdraw_addr_enabled": true + }, + "auth": { + "accounts": [ + { + "type": "cosmos-sdk/Account", + "value": { + "account_number": "0", + "address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "coins": [ + { + "amount": "10000000000", + "denom": "btc" + }, + { + "amount": "9000000000", + "denom": "ukava" + }, + { + "amount": "100000000", + "denom": "xrp" + } + ], + "public_key": null, + "sequence": "0" + } + }, + { + "type": "cosmos-sdk/Account", + "value": { + "account_number": "0", + "address": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj", + "coins": [ + { + "amount": "10000000000", + "denom": "btc" + }, + { + "amount": "9000000000", + "denom": "ukava" + }, + { + "amount": "100000000", + "denom": "xrp" + } + ], + "public_key": null, + "sequence": "0" + } + }, + { + "type": "cosmos-sdk/Account", + "value": { + "account_number": "0", + "address": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03", + "coins": [ + { + "amount": "10000000000", + "denom": "btc" + }, + { + "amount": "9000000000", + "denom": "ukava" + }, + { + "amount": "100000000", + "denom": "xrp" + } + ], + "public_key": null, + "sequence": "0" + } + } + ], + "params": { + "max_memo_characters": "256", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10" + } + }, + "staking": { + "delegations": null, + "exported": false, + "last_total_power": "0", + "last_validator_powers": null, + "params": { + "bond_denom": "ukava", + "max_entries": 7, + "max_validators": 100, + "unbonding_time": "3600000000000" + }, + "redelegations": null, + "unbonding_delegations": null, + "validators": null + }, + "pricefeed": { + "params": { + "markets": [ + { + "active": true, + "base_asset": "xrp", + "market_id": "xrp:usd", + "oracles": [], + "quote_asset": "usd" + }, + { + "active": true, + "base_asset": "btc", + "market_id": "btc:usd", + "oracles": [], + "quote_asset": "usd" + } + ] + }, + "posted_prices": [ + { + "expiry": "5000-01-01T00:00:00Z", + "market_id": "xrp:usd", + "oracle_address": "", + "price": "0.25" + }, + { + "expiry": "5000-01-01T00:00:00Z", + "market_id": "btc:usd", + "oracle_address": "", + "price": "8700.0" + } + ] + }, + "gov": { + "deposit_params": { + "max_deposit_period": "300000000000", + "min_deposit": [ + { + "amount": "1000000", + "denom": "ukava" + } + ] + }, + "deposits": null, + "proposals": null, + "starting_proposal_id": "1", + "tally_params": { + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto": "0.334000000000000000" + }, + "votes": null, + "voting_params": { + "voting_period": "300000000000" + } + } + } + } + \ No newline at end of file diff --git a/contrib/proposal_examples/updatebidduration/updatebidduration.json b/contrib/proposal_examples/updatebidduration/updatebidduration.json new file mode 100644 index 00000000..ef0ce942 --- /dev/null +++ b/contrib/proposal_examples/updatebidduration/updatebidduration.json @@ -0,0 +1,17 @@ +{ + "title": "Auction Param Change", + "description": "Update auction bid duration", + "changes": [ + { + "subspace": "auction", + "key": "BidDuration", + "value": "912345000000" + } + ], + "deposit": [ + { + "denom": "ukava", + "amount": "1000000" + } + ] + } \ No newline at end of file diff --git a/contrib/proposal_examples/updatedebtlimit/genesis_updatedebtlimit.json b/contrib/proposal_examples/updatedebtlimit/genesis_updatedebtlimit.json new file mode 100644 index 00000000..0d334dee --- /dev/null +++ b/contrib/proposal_examples/updatedebtlimit/genesis_updatedebtlimit.json @@ -0,0 +1,361 @@ +{ + "genesis_time": "2020-01-22T22:39:50.083273Z", + "chain_id": "testing", + "consensus_params": { + "block": { + "max_bytes": "200000", + "max_gas": "2000000", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age": "1000000" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + } + }, + "app_hash": "", + "app_state": { + "slashing": { + "missed_blocks": {}, + "params": { + "downtime_jail_duration": "600000000000", + "max_evidence_age": "3600000000000", + "min_signed_per_window": "0.010000000000000000", + "signed_blocks_window": "1000", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.000100000000000000" + }, + "signing_infos": {} + }, + "params": null, + "bank": { + "send_enabled": true + }, + "crisis": { + "constant_fee": { + "amount": "1333000000", + "denom": "ukava" + } + }, + "cdp": { + "cdps": [], + "debt_denom": "debt", + "deposits": [], + "gov_denom": "ukava", + "params": { + "circuit_breaker": false, + "collateral_params": [ + { + "auction_size": "5000000000", + "conversion_factor": "6", + "debt_limit": [ + { + "amount": "1000000000000", + "denom": "usdx" + } + ], + "denom": "xrp", + "liquidation_penalty": "0.05", + "liquidation_ratio": "2.0", + "market_id": "xrp:usd", + "prefix": 0, + "stability_fee": "1.000000001547126" + }, + { + "auction_size": "10000000", + "conversion_factor": "8", + "debt_limit": [ + { + "amount": "1000000000000", + "denom": "usdx" + } + ], + "denom": "btc", + "liquidation_penalty": "0.05", + "liquidation_ratio": "1.5", + "market_id": "btc:usd", + "prefix": 1, + "stability_fee": "1.0000000007829977" + } + ], + "debt_auction_threshold": "1000000000", + "debt_params": [ + { + "conversion_factor": "6", + "debt_floor": "10000000", + "debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "denom": "usdx", + "reference_asset": "usd" + } + ], + "global_debt_limit": [ + { + "amount": "2000000000000", + "denom": "usdx" + } + ], + "surplus_auction_threshold": "1000000000" + }, + "previous_block_time": "1970-01-01T00:00:00Z", + "starting_cdp_id": "1" + }, + "auction": { + "auctions": [], + "next_auction_id": "0", + "params": { + "bid_duration": "600000000000", + "max_auction_duration": "172800000000000" + } + }, + "genutil": { + "gentxs": [ + { + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/MsgCreateValidator", + "value": { + "description": { + "moniker": "validator", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.100000000000000000", + "max_rate": "0.200000000000000000", + "max_change_rate": "0.010000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "validator_address": "kavavaloper15qdefkmwswysgg4qxgqpqr35k3m49pkx8yhpte", + "pubkey": "kavavalconspub1zcjduepq0ykwlzr3guncl5l8npqa5lmq8kc5y2aeqp8zc8e72t3stxujxvxsxqjqwt", + "value": { + "denom": "ukava", + "amount": "100000000" + } + } + } + ], + "fee": { + "amount": [], + "gas": "200000" + }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "Az740XKIPCJtnZLmJfktTfhsEStEJE3n2iRVyJ3wko43" + }, + "signature": "llQmiLi+BQDKT2D2fOSr+eiDTXnce9xvZ7xls8EgoLIn+vz3HpyaTEoUQVes0kXbHfwJVSTz8jN6Z7HWYeEDlw==" + } + ], + "memo": "75917fc405a9af4ed44161a38456ce290a588100@10.21.104.217:26656" + } + } + ] + }, + "supply": { + "supply": [] + }, + "validatorvesting": { + "previous_block_time": "1970-01-01T00:00:00Z" + }, + "mint": { + "minter": { + "annual_provisions": "0.000000000000000000", + "inflation": "0.020000000000000000" + }, + "params": { + "blocks_per_year": "6311520", + "goal_bonded": "0.670000000000000000", + "inflation_max": "0.130000000000000000", + "inflation_min": "0.010000000000000000", + "inflation_rate_change": "0.130000000000000000", + "mint_denom": "ukava" + } + }, + "distribution": { + "base_proposer_reward": "0.010000000000000000", + "bonus_proposer_reward": "0.040000000000000000", + "community_tax": "0.000000000000000000", + "delegator_starting_infos": [], + "delegator_withdraw_infos": [], + "fee_pool": { + "community_pool": [] + }, + "outstanding_rewards": [], + "previous_proposer": "", + "validator_accumulated_commissions": [], + "validator_current_rewards": [], + "validator_historical_rewards": [], + "validator_slash_events": [], + "withdraw_addr_enabled": true + }, + "auth": { + "accounts": [ + { + "type": "cosmos-sdk/Account", + "value": { + "account_number": "0", + "address": "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw", + "coins": [ + { + "amount": "10000000000", + "denom": "btc" + }, + { + "amount": "9000000000000", + "denom": "ukava" + }, + { + "amount": "9000000000000", + "denom": "xrp" + } + ], + "public_key": null, + "sequence": "0" + } + }, + { + "type": "cosmos-sdk/Account", + "value": { + "account_number": "0", + "address": "kava1xy7hrjy9r0algz9w3gzm8u6mrpq97kwta747gj", + "coins": [ + { + "amount": "10000000000", + "denom": "btc" + }, + { + "amount": "9000000000000", + "denom": "ukava" + }, + { + "amount": "9000000000000", + "denom": "xrp" + } + ], + "public_key": null, + "sequence": "0" + } + }, + { + "type": "cosmos-sdk/Account", + "value": { + "account_number": "0", + "address": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03", + "coins": [ + { + "amount": "10000000000", + "denom": "btc" + }, + { + "amount": "9000000000000", + "denom": "ukava" + }, + { + "amount": "9000000000000", + "denom": "xrp" + } + ], + "public_key": null, + "sequence": "0" + } + } + ], + "params": { + "max_memo_characters": "256", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10" + } + }, + "staking": { + "delegations": null, + "exported": false, + "last_total_power": "0", + "last_validator_powers": null, + "params": { + "bond_denom": "ukava", + "max_entries": 7, + "max_validators": 100, + "unbonding_time": "3600000000000" + }, + "redelegations": null, + "unbonding_delegations": null, + "validators": null + }, + "pricefeed": { + "params": { + "markets": [ + { + "active": true, + "base_asset": "xrp", + "market_id": "xrp:usd", + "oracles": [ + "kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw" + ], + "quote_asset": "usd" + }, + { + "active": true, + "base_asset": "btc", + "market_id": "btc:usd", + "oracles": [], + "quote_asset": "usd" + } + ] + }, + "posted_prices": [ + { + "expiry": "5000-01-01T00:00:00Z", + "market_id": "xrp:usd", + "oracle_address": "", + "price": "0.25" + }, + { + "expiry": "5000-01-01T00:00:00Z", + "market_id": "btc:usd", + "oracle_address": "", + "price": "8700.0" + } + ] + }, + "gov": { + "deposit_params": { + "max_deposit_period": "300000000000", + "min_deposit": [ + { + "amount": "1000000", + "denom": "ukava" + } + ] + }, + "deposits": null, + "proposals": null, + "starting_proposal_id": "1", + "tally_params": { + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto": "0.334000000000000000" + }, + "votes": null, + "voting_params": { + "voting_period": "300000000000" + } + } + } + } + \ No newline at end of file diff --git a/contrib/proposal_examples/updatedebtlimit/updatedebtlimit.json b/contrib/proposal_examples/updatedebtlimit/updatedebtlimit.json new file mode 100644 index 00000000..53318661 --- /dev/null +++ b/contrib/proposal_examples/updatedebtlimit/updatedebtlimit.json @@ -0,0 +1,34 @@ +{ + "title": "CDP Param Change", + "description": "Update usdx debt limit for collateral type XRP", + "changes": [ + { + "subspace": "cdp", + "key": "CollateralParams", + "value": [ + { + "auction_size": "10000000", + "conversion_factor": "6", + "debt_limit": [ + { + "amount": "750000000", + "denom": "usdx" + } + ], + "denom": "xrp", + "liquidation_penalty": "0.05", + "liquidation_ratio": "2.0", + "market_id": "xrp:usd", + "prefix": 1, + "stability_fee": "1.0000000007829977" + } + ] + } + ], + "deposit": [ + { + "denom": "ukava", + "amount": "1000000" + } + ] +} \ No newline at end of file diff --git a/contrib/proposal_examples/updatemaxauctionduration/updatemaxauctionduration.json b/contrib/proposal_examples/updatemaxauctionduration/updatemaxauctionduration.json new file mode 100644 index 00000000..1521b26c --- /dev/null +++ b/contrib/proposal_examples/updatemaxauctionduration/updatemaxauctionduration.json @@ -0,0 +1,17 @@ +{ + "title": "Auction Param Change", + "description": "Update max auction duration", + "changes": [ + { + "subspace": "auction", + "key": "MaxAuctionDuration", + "value": "987654321000" + } + ], + "deposit": [ + { + "denom": "ukava", + "amount": "1000000" + } + ] + } \ No newline at end of file From 075a3089cef1b09495b47a542f9338ce2a78a0ff Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Mon, 27 Jan 2020 10:07:42 -0800 Subject: [PATCH 83/99] R4R: add events to pricefeed module (#343) * added events, attributes to pricefeed msgs and core functionality * removed added space * conditionally emit market_price_updated if price has changed * updated expiry to Unix format --- x/pricefeed/alias.go | 7 +++++++ x/pricefeed/handler.go | 11 ++++++++++- x/pricefeed/keeper/keeper.go | 35 ++++++++++++++++++++++++++++++++++- x/pricefeed/types/events.go | 9 ++++++++- 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/x/pricefeed/alias.go b/x/pricefeed/alias.go index e3fa62ac..1c434163 100644 --- a/x/pricefeed/alias.go +++ b/x/pricefeed/alias.go @@ -17,7 +17,14 @@ const ( CodeInvalidPrice = types.CodeInvalidPrice CodeInvalidAsset = types.CodeInvalidAsset CodeInvalidOracle = types.CodeInvalidOracle + EventTypeMarketPriceUpdated = types.EventTypeMarketPriceUpdated + EventTypeOracleUpdatedPrice = types.EventTypeOracleUpdatedPrice EventTypeNoValidPrices = types.EventTypeNoValidPrices + AttributeValueCategory = types.AttributeValueCategory + AttributeMarketID = types.AttributeMarketID + AttributeMarketPrice = types.AttributeMarketPrice + AttributeOracle = types.AttributeOracle + AttributeExpiry = types.AttributeExpiry AttributeKeyPriceUpdateFailed = types.AttributeKeyPriceUpdateFailed ModuleName = types.ModuleName StoreKey = types.StoreKey diff --git a/x/pricefeed/handler.go b/x/pricefeed/handler.go index cfcc34e0..628f8951 100644 --- a/x/pricefeed/handler.go +++ b/x/pricefeed/handler.go @@ -36,5 +36,14 @@ func HandleMsgPostPrice( if err != nil { return err.Result() } - return sdk.Result{} + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + ) + + return sdk.Result{Events: ctx.EventManager().Events()} } diff --git a/x/pricefeed/keeper/keeper.go b/x/pricefeed/keeper/keeper.go index 10c8b410..0e4fc8e9 100644 --- a/x/pricefeed/keeper/keeper.go +++ b/x/pricefeed/keeper/keeper.go @@ -1,6 +1,7 @@ package keeper import ( + "fmt" "sort" "time" @@ -67,6 +68,16 @@ func (k Keeper) SetPrice( index = len(prices) - 1 } + // Emit an event containing the oracle's new price + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeOracleUpdatedPrice, + sdk.NewAttribute(types.AttributeMarketID, marketID), + sdk.NewAttribute(types.AttributeOracle, oracle.String()), + sdk.NewAttribute(types.AttributeMarketPrice, price.String()), + sdk.NewAttribute(types.AttributeExpiry, fmt.Sprintf("%d", expiry.Unix())), + ), + ) store.Set( []byte(types.RawPriceFeedPrefix+marketID), k.cdc.MustMarshalBinaryBare(prices), ) @@ -76,12 +87,19 @@ func (k Keeper) SetPrice( } -// SetCurrentPrices updates the price of an asset to the meadian of all valid oracle inputs +// SetCurrentPrices updates the price of an asset to the median of all valid oracle inputs func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) sdk.Error { _, ok := k.GetMarket(ctx, marketID) if !ok { return types.ErrInvalidMarket(k.codespace, marketID) } + // store current price + validPrevPrice := true + prevPrice, err := k.GetCurrentPrice(ctx, marketID) + if err != nil { + validPrevPrice = false + } + prices := k.GetRawPrices(ctx, marketID) var notExpiredPrices []types.CurrentPrice // filter out expired prices @@ -102,11 +120,26 @@ func (k Keeper) SetCurrentPrices(ctx sdk.Context, marketID string) sdk.Error { } medianPrice := k.CalculateMedianPrice(ctx, notExpiredPrices) + // check case that market price was not set in genesis + if validPrevPrice { + // only emit event if price has changed + if !medianPrice.Equal(prevPrice.Price) { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeMarketPriceUpdated, + sdk.NewAttribute(types.AttributeMarketID, fmt.Sprintf("%s", marketID)), + sdk.NewAttribute(types.AttributeMarketPrice, fmt.Sprintf("%s", medianPrice.String())), + ), + ) + } + } + store := ctx.KVStore(k.key) currentPrice := types.CurrentPrice{ MarketID: marketID, Price: medianPrice, } + store.Set( []byte(types.CurrentPricePrefix+marketID), k.cdc.MustMarshalBinaryBare(currentPrice), ) diff --git a/x/pricefeed/types/events.go b/x/pricefeed/types/events.go index 0bf5fa51..07a0007d 100644 --- a/x/pricefeed/types/events.go +++ b/x/pricefeed/types/events.go @@ -2,7 +2,14 @@ package types // Pricefeed module event types const ( - EventTypeNoValidPrices = "no_valid_prices" + EventTypeMarketPriceUpdated = "market_price_updated" + EventTypeOracleUpdatedPrice = "oracle_updated_price" + EventTypeNoValidPrices = "no_valid_prices" + AttributeValueCategory = ModuleName + AttributeMarketID = "market_id" + AttributeMarketPrice = "market_price" + AttributeOracle = "oracle" + AttributeExpiry = "expiry" AttributeKeyPriceUpdateFailed = "price_update_failed" ) From bf64a5c02c57ba59c108532fda4df8605bdf0a44 Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Tue, 28 Jan 2020 09:47:08 -0800 Subject: [PATCH 84/99] R4R: add collateral value, collateralization ratio to CDP querier (#347) * AugmentedCDP type, codec registration, querier update * added unique error for augmented cdp loading * added AugmentedCDPs type for cdps query res * query results for cdps (by denom) & cdps-by-ratio (by denom & ratio) * status: converting collateral value into debt coin denom * collateral value denominated in debt coin * query cdps-by-ratio now searches by collateralization ratio instead of absolute ratio * updated alias, code comments * updated querier tests * support multiple principal coins and their associated fees * collateralization ratio calculations on updated fees * include calculated fees in total debt calculation --- x/cdp/alias.go | 4 +++ x/cdp/client/cli/query.go | 16 ++++----- x/cdp/keeper/cdp.go | 49 +++++++++++++++++++++++++ x/cdp/keeper/querier.go | 34 +++++++++++++++--- x/cdp/keeper/querier_test.go | 69 +++++++++++++++++++++++++++--------- x/cdp/types/cdp.go | 60 +++++++++++++++++++++++++++++++ x/cdp/types/errors.go | 6 ++++ 7 files changed, 209 insertions(+), 29 deletions(-) diff --git a/x/cdp/alias.go b/x/cdp/alias.go index dd553c82..78080754 100644 --- a/x/cdp/alias.go +++ b/x/cdp/alias.go @@ -28,6 +28,7 @@ const ( CodeCdpNotAvailable = types.CodeCdpNotAvailable CodeBelowDebtFloor = types.CodeBelowDebtFloor CodePaymentExceedsDebt = types.CodePaymentExceedsDebt + CodeLoadingAugmentedCDP = types.CodeLoadingAugmentedCDP EventTypeCreateCdp = types.EventTypeCreateCdp EventTypeCdpDeposit = types.EventTypeCdpDeposit EventTypeCdpDraw = types.EventTypeCdpDraw @@ -76,6 +77,7 @@ var ( ErrCdpNotAvailable = types.ErrCdpNotAvailable ErrBelowDebtFloor = types.ErrBelowDebtFloor ErrPaymentExceedsDebt = types.ErrPaymentExceedsDebt + ErrLoadingAugmentedCDP = types.ErrLoadingAugmentedCDP DefaultGenesisState = types.DefaultGenesisState GetCdpIDBytes = types.GetCdpIDBytes GetCdpIDFromBytes = types.GetCdpIDFromBytes @@ -143,6 +145,8 @@ var ( type ( CDP = types.CDP CDPs = types.CDPs + AugmentedCDP = types.AugmentedCDP + AugmentedCDPs = types.AugmentedCDPs Deposit = types.Deposit Deposits = types.Deposits SupplyKeeper = types.SupplyKeeper diff --git a/x/cdp/client/cli/query.go b/x/cdp/client/cli/query.go index 01247bd7..30a8ff08 100644 --- a/x/cdp/client/cli/query.go +++ b/x/cdp/client/cli/query.go @@ -69,7 +69,7 @@ $ %s query %s cdp kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw uatom } // Decode and print results - var cdp types.CDP + var cdp types.AugmentedCDP cdc.MustUnmarshalJSON(res, &cdp) return cliCtx.PrintOutput(cdp) }, @@ -105,9 +105,9 @@ $ %s query %s cdps uatom } // Decode and print results - var out types.CDPs - cdc.MustUnmarshalJSON(res, &out) - return cliCtx.PrintOutput(out) + var cdps types.AugmentedCDPs + cdc.MustUnmarshalJSON(res, &cdps) + return cliCtx.PrintOutput(cdps) }, } } @@ -119,7 +119,7 @@ func QueryCdpsByDenomAndRatioCmd(queryRoute string, cdc *codec.Codec) *cobra.Com Use: "cdps-by-ratio [collateral-name] [collateralization-ratio]", Short: "get cdps under a collateralization ratio", Long: strings.TrimSpace( - fmt.Sprintf(`List all CDPs under a collateralization ratios. + fmt.Sprintf(`List all CDPs under a specified collateralization ratio. Collateralization ratio is: collateral * price / debt. Example: @@ -150,9 +150,9 @@ $ %s query %s cdps-by-ratio uatom 1.5 } // Decode and print results - var out types.CDPs - cdc.MustUnmarshalJSON(res, &out) - return cliCtx.PrintOutput(out) + var cdps types.AugmentedCDPs + cdc.MustUnmarshalJSON(res, &cdps) + return cliCtx.PrintOutput(cdps) }, } } diff --git a/x/cdp/keeper/cdp.go b/x/cdp/keeper/cdp.go index 09800b2c..3fb52fe4 100644 --- a/x/cdp/keeper/cdp.go +++ b/x/cdp/keeper/cdp.go @@ -9,6 +9,9 @@ import ( "github.com/kava-labs/kava/x/cdp/types" ) +// BaseDigitFactor is 10**18, used during coin calculations +const BaseDigitFactor = 1000000000000000000 + // AddCdp adds a cdp for a specific owner and collateral type func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coins, principal sdk.Coins) sdk.Error { // validation @@ -410,6 +413,38 @@ func (k Keeper) CalculateCollateralToDebtRatio(ctx sdk.Context, collateral sdk.C return collateralBaseUnits.Quo(debtTotal) } +// LoadAugmentedCDP creates a new augmented CDP from an existing CDP +func (k Keeper) LoadAugmentedCDP(ctx sdk.Context, cdp types.CDP) (types.AugmentedCDP, sdk.Error) { + // calculate additional fees + periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix())) + fees := k.CalculateFees(ctx, cdp.Principal.Add(cdp.AccumulatedFees), periods, cdp.Collateral[0].Denom) + totalFees := cdp.AccumulatedFees.Add(fees) + + // calculate collateralization ratio + collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, cdp.Collateral, cdp.Principal, totalFees) + if err != nil { + return types.AugmentedCDP{}, err + } + + // total debt is the sum of all oustanding principal and fees + var totalDebt int64 + for _, principalCoin := range cdp.Principal { + totalDebt += principalCoin.Amount.Int64() + } + for _, feeCoin := range cdp.AccumulatedFees.Add(fees) { + totalDebt += feeCoin.Amount.Int64() + } + + // convert collateral value to debt coin + debtBaseAdjusted := sdk.NewDec(totalDebt).QuoInt64(BaseDigitFactor) + collateralValueInDebtDenom := collateralizationRatio.Mul(debtBaseAdjusted) + collateralValueInDebt := sdk.NewInt64Coin(cdp.Principal[0].Denom, collateralValueInDebtDenom.Int64()) + + // create new augmuented cdp + augmentedCDP := types.NewAugmentedCDP(cdp, collateralValueInDebt, collateralizationRatio) + return augmentedCDP, nil +} + // CalculateCollateralizationRatio returns the collateralization ratio of the input collateral to the input debt plus fees func (k Keeper) CalculateCollateralizationRatio(ctx sdk.Context, collateral sdk.Coins, principal sdk.Coins, fees sdk.Coins) (sdk.Dec, sdk.Error) { if collateral.IsZero() { @@ -422,6 +457,7 @@ func (k Keeper) CalculateCollateralizationRatio(ctx sdk.Context, collateral sdk. } collateralBaseUnits := k.convertCollateralToBaseUnits(ctx, collateral[0]) collateralValue := collateralBaseUnits.Mul(price.Price) + principalTotal := sdk.ZeroDec() for _, pc := range principal { prinicpalBaseUnits := k.convertDebtToBaseUnits(ctx, pc) @@ -435,6 +471,19 @@ func (k Keeper) CalculateCollateralizationRatio(ctx sdk.Context, collateral sdk. return collateralRatio, nil } +// CalculateCollateralizationRatioFromAbsoluteRatio takes a coin's denom and an absolute ratio and returns the respective collateralization ratio +func (k Keeper) CalculateCollateralizationRatioFromAbsoluteRatio(ctx sdk.Context, collateralDenom string, absoluteRatio sdk.Dec) (sdk.Dec, sdk.Error) { + // get price collateral + marketID := k.getMarketID(ctx, collateralDenom) + price, err := k.pricefeedKeeper.GetCurrentPrice(ctx, marketID) + if err != nil { + return sdk.Dec{}, err + } + // convert absolute ratio to collateralization ratio + respectiveCollateralRatio := absoluteRatio.Quo(price.Price) + return respectiveCollateralRatio, nil +} + // converts the input collateral to base units (ie multiplies the input by 10^(-ConversionFactor)) func (k Keeper) convertCollateralToBaseUnits(ctx sdk.Context, collateral sdk.Coin) (baseUnits sdk.Dec) { cp, _ := k.GetCollateral(ctx, collateral.Denom) diff --git a/x/cdp/keeper/querier.go b/x/cdp/keeper/querier.go index dada19ca..fdd8c86f 100644 --- a/x/cdp/keeper/querier.go +++ b/x/cdp/keeper/querier.go @@ -44,7 +44,12 @@ func queryGetCdp(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, return nil, types.ErrCdpNotFound(keeper.codespace, requestParams.Owner, requestParams.CollateralDenom) } - bz, err := codec.MarshalJSONIndent(keeper.cdc, cdp) + augmentedCDP, err := keeper.LoadAugmentedCDP(ctx, cdp) + if err != nil { + return nil, types.ErrLoadingAugmentedCDP(keeper.codespace, cdp.ID) + } + + bz, err := codec.MarshalJSONIndent(keeper.cdc, augmentedCDP) if err != nil { return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) } @@ -64,8 +69,21 @@ func queryGetCdpsByRatio(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) return nil, types.ErrInvalidCollateralDenom(keeper.codespace, requestParams.CollateralDenom) } - cdps := keeper.GetAllCdpsByDenomAndRatio(ctx, requestParams.CollateralDenom, requestParams.Ratio) - bz, err := codec.MarshalJSONIndent(keeper.cdc, cdps) + ratio, err := keeper.CalculateCollateralizationRatioFromAbsoluteRatio(ctx, requestParams.CollateralDenom, requestParams.Ratio) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could get collateralization ratio from absolute ratio", err.Error())) + } + + cdps := keeper.GetAllCdpsByDenomAndRatio(ctx, requestParams.CollateralDenom, ratio) + // augment CDPs by adding collateral value and collateralization ratio + var augmentedCDPs types.AugmentedCDPs + for _, cdp := range cdps { + augmentedCDP, err := keeper.LoadAugmentedCDP(ctx, cdp) + if err == nil { + augmentedCDPs = append(augmentedCDPs, augmentedCDP) + } + } + bz, err := codec.MarshalJSONIndent(keeper.cdc, augmentedCDPs) if err != nil { return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) } @@ -85,7 +103,15 @@ func queryGetCdpsByDenom(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) } cdps := keeper.GetAllCdpsByDenom(ctx, requestParams.CollateralDenom) - bz, err := codec.MarshalJSONIndent(keeper.cdc, cdps) + // augment CDPs by adding collateral value and collateralization ratio + var augmentedCDPs types.AugmentedCDPs + for _, cdp := range cdps { + augmentedCDP, err := keeper.LoadAugmentedCDP(ctx, cdp) + if err == nil { + augmentedCDPs = append(augmentedCDPs, augmentedCDP) + } + } + bz, err := codec.MarshalJSONIndent(keeper.cdc, augmentedCDPs) if err != nil { return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) } diff --git a/x/cdp/keeper/querier_test.go b/x/cdp/keeper/querier_test.go index d13ec917..0f2310e8 100644 --- a/x/cdp/keeper/querier_test.go +++ b/x/cdp/keeper/querier_test.go @@ -5,12 +5,15 @@ import ( "sort" "strings" "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/simulation" "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/cdp/keeper" "github.com/kava-labs/kava/x/cdp/types" + pfkeeper "github.com/kava-labs/kava/x/pricefeed/keeper" + pftypes "github.com/kava-labs/kava/x/pricefeed/types" "github.com/stretchr/testify/suite" abci "github.com/tendermint/tendermint/abci/types" tmtime "github.com/tendermint/tendermint/types/time" @@ -23,18 +26,21 @@ const ( type QuerierTestSuite struct { suite.Suite - keeper keeper.Keeper - addrs []sdk.AccAddress - app app.TestApp - cdps types.CDPs - ctx sdk.Context - querier sdk.Querier + keeper keeper.Keeper + pricefeedKeeper pfkeeper.Keeper + addrs []sdk.AccAddress + app app.TestApp + cdps types.CDPs + augmentedCDPs types.AugmentedCDPs + ctx sdk.Context + querier sdk.Querier } func (suite *QuerierTestSuite) SetupTest() { tApp := app.NewTestApp() ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) cdps := make(types.CDPs, 100) + augmentedCDPs := make(types.AugmentedCDPs, 100) _, addrs := app.GeneratePrivKeyAddressPairs(100) coins := []sdk.Coins{} @@ -53,6 +59,30 @@ func (suite *QuerierTestSuite) SetupTest() { suite.ctx = ctx suite.app = tApp suite.keeper = tApp.GetCDPKeeper() + suite.pricefeedKeeper = tApp.GetPriceFeedKeeper() + + // Set up markets + oracle := addrs[9] + marketParams := pftypes.Params{ + Markets: pftypes.Markets{ + pftypes.Market{MarketID: "xrp-usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: []sdk.AccAddress{oracle}, Active: true}, + pftypes.Market{MarketID: "btc-usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{oracle}, Active: true}, + }, + } + suite.pricefeedKeeper.SetParams(ctx, marketParams) + + // Set collateral prices for use in collateralization calculations + _, err := suite.pricefeedKeeper.SetPrice( + ctx, oracle, "xrp-usd", + sdk.MustNewDecFromStr("0.75"), + time.Now().Add(1*time.Hour)) + suite.Nil(err) + + _, err = suite.pricefeedKeeper.SetPrice( + ctx, oracle, "btc-usd", + sdk.MustNewDecFromStr("5000"), + time.Now().Add(1*time.Hour)) + suite.Nil(err) for j := 0; j < 100; j++ { collateral := "xrp" @@ -67,9 +97,12 @@ func (suite *QuerierTestSuite) SetupTest() { c, f := suite.keeper.GetCDP(suite.ctx, collateral, uint64(j+1)) suite.True(f) cdps[j] = c + aCDP, _ := suite.keeper.LoadAugmentedCDP(suite.ctx, c) + augmentedCDPs[j] = aCDP } suite.cdps = cdps + suite.augmentedCDPs = augmentedCDPs suite.querier = keeper.NewQuerier(suite.keeper) suite.addrs = addrs } @@ -84,9 +117,9 @@ func (suite *QuerierTestSuite) TestQueryCdp() { suite.Nil(err) suite.NotNil(bz) - var c types.CDP + var c types.AugmentedCDP suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c)) - suite.Equal(suite.cdps[0], c) + suite.Equal(suite.augmentedCDPs[0], c) query = abci.RequestQuery{ Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdp}, "/"), @@ -125,7 +158,7 @@ func (suite *QuerierTestSuite) TestQueryCdpsByDenom() { suite.Nil(err) suite.NotNil(bz) - var c types.CDPs + var c types.AugmentedCDPs suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c)) suite.Equal(50, len(c)) @@ -140,19 +173,21 @@ func (suite *QuerierTestSuite) TestQueryCdpsByDenom() { func (suite *QuerierTestSuite) TestQueryCdpsByRatio() { ratioCountBtc := 0 ratioCountXrp := 0 - xrpRatio := d("50.0") - btcRatio := d("0.003") + xrpRatio := d("2.0") + btcRatio := d("2500") expectedXrpIds := []int{} expectedBtcIds := []int{} for _, cdp := range suite.cdps { - r := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Principal) + absoluteRatio := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Principal) + collateralizationRatio, err := suite.keeper.CalculateCollateralizationRatioFromAbsoluteRatio(suite.ctx, cdp.Collateral[0].Denom, absoluteRatio) + suite.Nil(err) if cdp.Collateral[0].Denom == "xrp" { - if r.LT(xrpRatio) { + if collateralizationRatio.LT(xrpRatio) { ratioCountXrp += 1 expectedXrpIds = append(expectedXrpIds, int(cdp.ID)) } } else { - if r.LT(btcRatio) { + if collateralizationRatio.LT(btcRatio) { ratioCountBtc += 1 expectedBtcIds = append(expectedBtcIds, int(cdp.ID)) } @@ -168,7 +203,7 @@ func (suite *QuerierTestSuite) TestQueryCdpsByRatio() { suite.Nil(err) suite.NotNil(bz) - var c types.CDPs + var c types.AugmentedCDPs actualXrpIds := []int{} suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c)) for _, k := range c { @@ -185,7 +220,7 @@ func (suite *QuerierTestSuite) TestQueryCdpsByRatio() { suite.Nil(err) suite.NotNil(bz) - c = types.CDPs{} + c = types.AugmentedCDPs{} actualBtcIds := []int{} suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c)) for _, k := range c { @@ -201,7 +236,7 @@ func (suite *QuerierTestSuite) TestQueryCdpsByRatio() { bz, err = suite.querier(ctx, []string{types.QueryGetCdpsByCollateralization}, query) suite.Nil(err) suite.NotNil(bz) - c = types.CDPs{} + c = types.AugmentedCDPs{} suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &c)) suite.Equal(0, len(c)) } diff --git a/x/cdp/types/cdp.go b/x/cdp/types/cdp.go index c807b12a..8c5365be 100644 --- a/x/cdp/types/cdp.go +++ b/x/cdp/types/cdp.go @@ -62,3 +62,63 @@ func (cdps CDPs) String() string { } return out } + +// AugmentedCDP provides additional information about an active CDP +type AugmentedCDP struct { + CDP `json:"cdp" yaml:"cdp"` + CollateralValue sdk.Coin `json:"collateral_value" yaml:"collateral_value"` // collateral's market value in debt coin + CollateralizationRatio sdk.Dec `json:"collateralization_ratio" yaml:"collateralization_ratio"` // current collateralization ratio +} + +// NewAugmentedCDP creates a new AugmentedCDP object +func NewAugmentedCDP(cdp CDP, collateralValue sdk.Coin, collateralizationRatio sdk.Dec) AugmentedCDP { + augmentedCDP := AugmentedCDP{ + CDP: CDP{ + ID: cdp.ID, + Owner: cdp.Owner, + Collateral: cdp.Collateral, + Principal: cdp.Principal, + AccumulatedFees: cdp.AccumulatedFees, + FeesUpdated: cdp.FeesUpdated, + }, + CollateralValue: collateralValue, + CollateralizationRatio: collateralizationRatio, + } + return augmentedCDP +} + +// String implements fmt.stringer +func (augCDP AugmentedCDP) String() string { + return strings.TrimSpace(fmt.Sprintf(`AugmentedCDP: + Owner: %s + ID: %d + Collateral Type: %s + Collateral: %s + Collateral Value: %s + Principal: %s + Fees: %s + Fees Last Updated: %s + Collateralization ratio: %s`, + augCDP.Owner, + augCDP.ID, + augCDP.Collateral[0].Denom, + augCDP.Collateral, + augCDP.CollateralValue, + augCDP.Principal, + augCDP.AccumulatedFees, + augCDP.FeesUpdated, + augCDP.CollateralizationRatio, + )) +} + +// AugmentedCDPs a collection of AugmentedCDP objects +type AugmentedCDPs []AugmentedCDP + +// String implements stringer +func (augcdps AugmentedCDPs) String() string { + out := "" + for _, augcdp := range augcdps { + out += augcdp.String() + "\n" + } + return out +} diff --git a/x/cdp/types/errors.go b/x/cdp/types/errors.go index 56cd5a93..95dc05b9 100644 --- a/x/cdp/types/errors.go +++ b/x/cdp/types/errors.go @@ -26,6 +26,7 @@ const ( CodeCdpNotAvailable sdk.CodeType = 14 CodeBelowDebtFloor sdk.CodeType = 15 CodePaymentExceedsDebt sdk.CodeType = 16 + CodeLoadingAugmentedCDP sdk.CodeType = 17 ) // ErrCdpAlreadyExists error for duplicate cdps @@ -107,3 +108,8 @@ func ErrBelowDebtFloor(codespace sdk.CodespaceType, debt sdk.Coins, floor sdk.In func ErrPaymentExceedsDebt(codespace sdk.CodespaceType, payment sdk.Coins, principal sdk.Coins) sdk.Error { return sdk.NewError(codespace, CodePaymentExceedsDebt, fmt.Sprintf("payment of %s exceeds debt of %s", payment, principal)) } + +// ErrLoadingAugmentedCDP error loading augmented cdp +func ErrLoadingAugmentedCDP(codespace sdk.CodespaceType, cdpID uint64) sdk.Error { + return sdk.NewError(codespace, CodeCdpNotFound, fmt.Sprintf("augmented cdp could not be loaded from cdp id %d", cdpID)) +} From 024001888cd969958595c1343b499d7e5c6eac5d Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Tue, 28 Jan 2020 14:08:17 -0600 Subject: [PATCH 85/99] Add query for cdp deposits (#346) * feat: query cdp deposits * feat: test querying deposits * fix: update comment * fix: use single word command --- x/cdp/client/cli/query.go | 44 ++++++++++++++++++++++++++++++++++++ x/cdp/client/rest/query.go | 33 +++++++++++++++++++++++++++ x/cdp/keeper/querier.go | 30 ++++++++++++++++++++++++ x/cdp/keeper/querier_test.go | 19 ++++++++++++++++ x/cdp/types/querier.go | 15 ++++++++++++ 5 files changed, 141 insertions(+) diff --git a/x/cdp/client/cli/query.go b/x/cdp/client/cli/query.go index 30a8ff08..095b3fcb 100644 --- a/x/cdp/client/cli/query.go +++ b/x/cdp/client/cli/query.go @@ -27,6 +27,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { QueryCdpCmd(queryRoute, cdc), QueryCdpsByDenomCmd(queryRoute, cdc), QueryCdpsByDenomAndRatioCmd(queryRoute, cdc), + QueryCdpDepositsCmd(queryRoute, cdc), QueryParamsCmd(queryRoute, cdc), )...) @@ -157,6 +158,49 @@ $ %s query %s cdps-by-ratio uatom 1.5 } } +// QueryCdpDepositsCmd returns the command handler for querying the deposits of a particular cdp +func QueryCdpDepositsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "deposits [owner-addr] [collateral-name]", + Short: "get deposits for a cdp", + Long: strings.TrimSpace( + fmt.Sprintf(`Get the deposits of a CDP. + +Example: +$ %s query %s deposits kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw uatom +`, version.ClientName, types.ModuleName)), + 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 + } + bz, err := cdc.MarshalJSON(types.QueryCdpParams{ + CollateralDenom: args[1], + Owner: ownerAddress, + }) + if err != nil { + return err + } + + // Query + route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetCdpDeposits) + res, _, err := cliCtx.QueryWithData(route, bz) + if err != nil { + return err + } + + // Decode and print results + var deposits types.Deposits + cdc.MustUnmarshalJSON(res, &deposits) + return cliCtx.PrintOutput(deposits) + }, + } +} + // QueryParamsCmd returns the command handler for cdp parameter querying func QueryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { return &cobra.Command{ diff --git a/x/cdp/client/rest/query.go b/x/cdp/client/rest/query.go index c3f00067..79bd5774 100644 --- a/x/cdp/client/rest/query.go +++ b/x/cdp/client/rest/query.go @@ -19,6 +19,7 @@ func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { r.HandleFunc(fmt.Sprintf("/cdp/{%s}/{%s}", types.RestOwner, types.RestCollateralDenom), queryCdpHandlerFn(cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/cdp/{%s}", types.RestCollateralDenom), queryCdpsHandlerFn(cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/cdp/{%s}/ratio/{%s}", types.RestCollateralDenom, types.RestRatio), queryCdpsByRatioHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/cdp/deposits/{%s}/{%s}", types.RestOwner, types.RestCollateralDenom), queryCdpDepositsHandlerFn(cliCtx)).Methods("GET") r.HandleFunc("/cdps/params", getParamsHandlerFn(cliCtx)).Methods("GET") } @@ -110,6 +111,38 @@ func queryCdpsByRatioHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } } +func queryCdpDepositsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + ownerBech32 := vars[types.RestOwner] + collateralDenom := vars[types.RestCollateralDenom] + + owner, err := sdk.AccAddressFromBech32(ownerBech32) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + params := types.NewQueryCdpDeposits(owner, collateralDenom) + + bz, err := cliCtx.Codec.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetCdpDeposits), bz) + 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 diff --git a/x/cdp/keeper/querier.go b/x/cdp/keeper/querier.go index fdd8c86f..440b4e1a 100644 --- a/x/cdp/keeper/querier.go +++ b/x/cdp/keeper/querier.go @@ -20,6 +20,8 @@ func NewQuerier(keeper Keeper) sdk.Querier { return queryGetCdpsByRatio(ctx, req, keeper) case types.QueryGetParams: return queryGetParams(ctx, req, keeper) + case types.QueryGetCdpDeposits: + return queryGetDeposits(ctx, req, keeper) default: return nil, sdk.ErrUnknownRequest("unknown cdp query endpoint") } @@ -57,6 +59,34 @@ func queryGetCdp(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, } +// query deposits on a particular cdp +func queryGetDeposits(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + var requestParams types.QueryCdpDeposits + err := keeper.cdc.UnmarshalJSON(req.Data, &requestParams) + if err != nil { + return nil, sdk.ErrUnknownRequest(sdk.AppendMsgToErr("incorrectly formatted request data", err.Error())) + } + + _, valid := keeper.GetDenomPrefix(ctx, requestParams.CollateralDenom) + if !valid { + return nil, types.ErrInvalidCollateralDenom(keeper.codespace, requestParams.CollateralDenom) + } + + cdp, found := keeper.GetCdpByOwnerAndDenom(ctx, requestParams.Owner, requestParams.CollateralDenom) + if !found { + return nil, types.ErrCdpNotFound(keeper.codespace, requestParams.Owner, requestParams.CollateralDenom) + } + + deposits := keeper.GetDeposits(ctx, cdp.ID) + + bz, err := codec.MarshalJSONIndent(keeper.cdc, deposits) + if err != nil { + return nil, sdk.ErrInternal(sdk.AppendMsgToErr("could not marshal result to JSON", err.Error())) + } + return bz, nil + +} + // query cdps with matching denom and ratio LESS THAN the input ratio func queryGetCdpsByRatio(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { var requestParams types.QueryCdpsByRatioParams diff --git a/x/cdp/keeper/querier_test.go b/x/cdp/keeper/querier_test.go index 0f2310e8..41b65467 100644 --- a/x/cdp/keeper/querier_test.go +++ b/x/cdp/keeper/querier_test.go @@ -256,6 +256,25 @@ func (suite *QuerierTestSuite) TestQueryParams() { suite.Equal(gs.Params, p) } +func (suite *QuerierTestSuite) TestQueryDeposits() { + ctx := suite.ctx.WithIsCheckTx(false) + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetCdpDeposits}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryCdpDeposits(suite.cdps[0].Owner, suite.cdps[0].Collateral[0].Denom)), + } + + bz, err := suite.querier(ctx, []string{types.QueryGetCdpDeposits}, query) + suite.Nil(err) + suite.NotNil(bz) + + deposits := suite.keeper.GetDeposits(ctx, suite.cdps[0].ID) + + var d types.Deposits + suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &d)) + suite.Equal(deposits, d) + +} + func TestQuerierTestSuite(t *testing.T) { suite.Run(t, new(QuerierTestSuite)) } diff --git a/x/cdp/types/querier.go b/x/cdp/types/querier.go index fa3c8ed9..77f74eaf 100644 --- a/x/cdp/types/querier.go +++ b/x/cdp/types/querier.go @@ -7,6 +7,7 @@ import ( // Querier routes for the cdp module const ( QueryGetCdp = "cdp" + QueryGetCdpDeposits = "deposits" QueryGetCdps = "cdps" QueryGetCdpsByCollateralization = "ratio" QueryGetParams = "params" @@ -41,6 +42,20 @@ func NewQueryCdpParams(owner sdk.AccAddress, denom string) QueryCdpParams { } } +// QueryCdpDeposits params for query /cdp/deposits +type QueryCdpDeposits struct { + CollateralDenom string // get CDPs with this collateral denom + Owner sdk.AccAddress // get CDPs belonging to this owner +} + +// NewQueryCdpDeposits returns QueryCdpDeposits +func NewQueryCdpDeposits(owner sdk.AccAddress, denom string) QueryCdpDeposits { + return QueryCdpDeposits{ + Owner: owner, + CollateralDenom: denom, + } +} + // QueryCdpsByRatioParams params for query /cdp/cdps/ratio type QueryCdpsByRatioParams struct { CollateralDenom string // get CDPs with this collateral denom From 84fa0ef51d5bea1c9e059831e54b9d874ef0f712 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Tue, 28 Jan 2020 14:38:38 -0600 Subject: [PATCH 86/99] fix: apply penalty to partial deposits (#350) --- x/cdp/keeper/auctions.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x/cdp/keeper/auctions.go b/x/cdp/keeper/auctions.go index 5e6e422b..6e39bbbb 100644 --- a/x/cdp/keeper/auctions.go +++ b/x/cdp/keeper/auctions.go @@ -142,11 +142,13 @@ func (k Keeper) CreateAuctionFromPartialDeposits(ctx sdk.Context, partialDeps pa returnAddrs := []sdk.AccAddress{} returnWeights := []sdk.Int{} + depositDenom := partialDeps[0].Amount[0].Denom for _, pd := range partialDeps { returnAddrs = append(returnAddrs, pd.Depositor) returnWeights = append(returnWeights, pd.DebtShare) } - _, err = k.auctionKeeper.StartCollateralAuction(ctx, types.LiquidatorMacc, sdk.NewCoin(partialDeps[0].Amount[0].Denom, auctionSize), sdk.NewCoin(bidDenom, partialDeps.SumDebt()), returnAddrs, returnWeights, sdk.NewCoin(k.GetDebtDenom(ctx), partialDeps.SumDebt())) + penalty := k.ApplyLiquidationPenalty(ctx, depositDenom, partialDeps.SumDebt()) + _, err = k.auctionKeeper.StartCollateralAuction(ctx, types.LiquidatorMacc, sdk.NewCoin(partialDeps[0].Amount[0].Denom, auctionSize), sdk.NewCoin(bidDenom, partialDeps.SumDebt().Add(penalty)), returnAddrs, returnWeights, sdk.NewCoin(k.GetDebtDenom(ctx), partialDeps.SumDebt())) if err != nil { return sdk.ZeroInt(), sdk.ZeroInt(), err } From 55f0f8d980bb1d8983056e4411112c985cc975b7 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Wed, 29 Jan 2020 08:42:03 -0600 Subject: [PATCH 87/99] Display auction type and phase when querying auctions (#345) * feat: differentiate auction types when queried * feat: display auction type * feat: add phase of collateral auctions * fix: set reverse phase directly * feat: revert base auction, use querying specifc structs * fix: pass auction as interface to handlers * set reverse phase on max bid (#348) * Revert "set reverse phase on max bid (#348)" (#351) This reverts commit 4b855250d529a4cbecb16d9d32b25ffeaffa3a68. * fix: missing return * fix: include collateral auction type * fix: always include phase field for queries Co-authored-by: Denali Marsh --- x/auction/client/cli/query.go | 10 ++++++++-- x/auction/client/rest/query.go | 27 ++++++++++++++++++++++++--- x/auction/keeper/auctions.go | 9 ++++----- x/auction/types/auctions.go | 32 +++++++++++++++++++++++--------- x/auction/types/querier.go | 29 +++++++++++++++++++++++++++-- 5 files changed, 86 insertions(+), 21 deletions(-) diff --git a/x/auction/client/cli/query.go b/x/auction/client/cli/query.go index 96bc15b7..60bf2080 100644 --- a/x/auction/client/cli/query.go +++ b/x/auction/client/cli/query.go @@ -60,7 +60,8 @@ func QueryGetAuctionCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { // Decode and print results var auction types.Auction cdc.MustUnmarshalJSON(res, &auction) - return cliCtx.PrintOutput(auction) + auctionWithPhase := types.NewAuctionWithPhase(auction) + return cliCtx.PrintOutput(auctionWithPhase) }, } } @@ -83,7 +84,12 @@ func QueryGetAuctionsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { // Decode and print results var auctions types.Auctions cdc.MustUnmarshalJSON(res, &auctions) - return cliCtx.PrintOutput(auctions) + + var auctionsWithPhase []types.AuctionWithPhase + for _, a := range auctions { + auctionsWithPhase = append(auctionsWithPhase, types.NewAuctionWithPhase(a)) + } + return cliCtx.PrintOutput(auctionsWithPhase) }, } } diff --git a/x/auction/client/rest/query.go b/x/auction/client/rest/query.go index 3a315445..97e27aa7 100644 --- a/x/auction/client/rest/query.go +++ b/x/auction/client/rest/query.go @@ -51,10 +51,18 @@ func queryAuctionHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - + var auction types.Auction + err = cliCtx.Codec.UnmarshalJSON(res, auction) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } // Decode and return results cliCtx = cliCtx.WithHeight(height) - rest.PostProcessResponse(w, cliCtx, res) + + auctionWithPhase := types.NewAuctionWithPhase(auction) + + rest.PostProcessResponse(w, cliCtx, cliCtx.Codec.MustMarshalJSON(auctionWithPhase)) } } @@ -73,7 +81,20 @@ func queryAuctionsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } // Return auctions cliCtx = cliCtx.WithHeight(height) - rest.PostProcessResponse(w, cliCtx, res) + + var auctions types.Auctions + err = cliCtx.Codec.UnmarshalJSON(res, auctions) + if err != nil { + rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) + return + } + + var auctionsWithPhase []types.AuctionWithPhase + for _, a := range auctions { + auctionsWithPhase = append(auctionsWithPhase, types.NewAuctionWithPhase(a)) + } + + rest.PostProcessResponse(w, cliCtx, cliCtx.Codec.MustMarshalJSON(auctionsWithPhase)) } } diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index b8079c20..e192a503 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -32,7 +32,7 @@ func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin sdk.NewEvent( types.EventTypeAuctionStart, sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())), - sdk.NewAttribute(types.AttributeKeyAuctionType, auction.Name()), + sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()), sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom), sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom), ), @@ -70,7 +70,7 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in sdk.NewEvent( types.EventTypeAuctionStart, sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())), - sdk.NewAttribute(types.AttributeKeyAuctionType, auction.Name()), + sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()), sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom), sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom), ), @@ -111,7 +111,7 @@ func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.C sdk.NewEvent( types.EventTypeAuctionStart, sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())), - sdk.NewAttribute(types.AttributeKeyAuctionType, auction.Name()), + sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()), sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom), sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom), ), @@ -506,9 +506,8 @@ func (k Keeper) CloseExpiredAuctions(ctx sdk.Context) sdk.Error { func earliestTime(t1, t2 time.Time) time.Time { if t1.Before(t2) { return t1 - } else { - return t2 // also returned if times are equal } + return t2 // also returned if times are equal } // splitCoinIntoWeightedBuckets divides up some amount of coins according to some weights. diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 6391138e..a9b0b3a5 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -22,6 +22,7 @@ type Auction interface { GetBidder() sdk.AccAddress GetBid() sdk.Coin GetEndTime() time.Time + GetType() string } // Auctions is a slice of auctions. @@ -57,6 +58,9 @@ func (a BaseAuction) GetBid() sdk.Coin { return a.Bid } // GetEndTime is a getter for auction end time. func (a BaseAuction) GetEndTime() time.Time { return a.EndTime } +// GetType returns theauction type. Used to identify auctions in event attributes. +func (a BaseAuction) GetType() string { return "base" } + // Validate verifies that the auction end time is before max end time func (a BaseAuction) Validate() error { if a.EndTime.After(a.MaxEndTime) { @@ -82,14 +86,14 @@ func (a BaseAuction) String() string { // SurplusAuction is a forward auction that burns what it receives from bids. // It is normally used to sell off excess pegged asset acquired by the CDP system. type SurplusAuction struct { - BaseAuction `json:"base_auction" yaml:"base_auction"` + BaseAuction } // WithID returns an auction with the ID set. func (a SurplusAuction) WithID(id uint64) Auction { a.ID = id; return a } -// Name returns a name for this auction type. Used to identify auctions in event attributes. -func (a SurplusAuction) Name() string { return "surplus" } +// GetType returns the auction type. Used to identify auctions in event attributes. +func (a SurplusAuction) GetType() string { return "surplus" } // GetModuleAccountCoins returns the total number of coins held in the module account for this auction. // It is used in genesis initialize the module account correctly. @@ -116,15 +120,16 @@ func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime tim // DebtAuction is a reverse auction that mints what it pays out. // It is normally used to acquire pegged asset to cover the CDP system's debts that were not covered by selling collateral. type DebtAuction struct { - BaseAuction `json:"base_auction" yaml:"base_auction"` + BaseAuction + CorrespondingDebt sdk.Coin `json:"corresponding_debt" yaml:"corresponding_debt"` } // WithID returns an auction with the ID set. func (a DebtAuction) WithID(id uint64) Auction { a.ID = id; return a } -// Name returns a name for this auction type. Used to identify auctions in event attributes. -func (a DebtAuction) Name() string { return "debt" } +// GetType returns the auction type. Used to identify auctions in event attributes. +func (a DebtAuction) GetType() string { return "debt" } // GetModuleAccountCoins returns the total number of coins held in the module account for this auction. // It is used in genesis initialize the module account correctly. @@ -160,7 +165,8 @@ func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, e // Unsold Lot is sent to LotReturns, being divided among the addresses by weight. // Collateral auctions are normally used to sell off collateral seized from CDPs. type CollateralAuction struct { - BaseAuction `json:"base_auction" yaml:"base_auction"` + BaseAuction + CorrespondingDebt sdk.Coin `json:"corresponding_debt" yaml:"corresponding_debt"` MaxBid sdk.Coin `json:"max_bid" yaml:"max_bid"` LotReturns WeightedAddresses `json:"lot_returns" yaml:"lot_returns"` @@ -169,8 +175,8 @@ type CollateralAuction struct { // WithID returns an auction with the ID set. func (a CollateralAuction) WithID(id uint64) Auction { a.ID = id; return a } -// Name returns a name for this auction type. Used to identify auctions in event attributes. -func (a CollateralAuction) Name() string { return "collateral" } +// GetType returns the auction type. Used to identify auctions in event attributes. +func (a CollateralAuction) GetType() string { return "collateral" } // GetModuleAccountCoins returns the total number of coins held in the module account for this auction. // It is used in genesis initialize the module account correctly. @@ -185,6 +191,14 @@ func (a CollateralAuction) IsReversePhase() bool { return a.Bid.IsEqual(a.MaxBid) } +// GetPhase returns the phase of a collateral auction +func (a CollateralAuction) GetPhase() string { + if a.IsReversePhase() { + return "reverse" + } + return "forward" +} + func (a CollateralAuction) String() string { return fmt.Sprintf(`Auction %d: Initiator: %s diff --git a/x/auction/types/querier.go b/x/auction/types/querier.go index c8019f10..44b1ca67 100644 --- a/x/auction/types/querier.go +++ b/x/auction/types/querier.go @@ -3,9 +3,9 @@ package types const ( // QueryGetAuction is the query path for querying one auction QueryGetAuction = "auction" - // QueryGetAuction is the query path for querying all auctions + // QueryGetAuctions is the query path for querying all auctions QueryGetAuctions = "auctions" - // QueryGetAuction is the query path for querying the global auction params + // QueryGetParams is the query path for querying the global auction params QueryGetParams = "params" ) @@ -27,3 +27,28 @@ func NewQueryAllAuctionParams(page int, limit int) QueryAllAuctionParams { Limit: limit, } } + +// AuctionWithPhase augmented type for collateral auctions which includes auction phase for querying +type AuctionWithPhase struct { + Auction Auction + + Type string `json:"type" yaml:"type"` + Phase string `json:"phase" yaml:"phase"` +} + +// NewAuctionWithPhase returns new AuctionWithPhase +func NewAuctionWithPhase(a Auction) AuctionWithPhase { + switch auc := a.(type) { + case CollateralAuction: + return AuctionWithPhase{ + Auction: auc, + Type: auc.GetType(), + Phase: auc.GetPhase(), + } + default: + return AuctionWithPhase{ + Auction: auc, + Type: auc.GetType(), + } + } +} From 3c8209cbcc01f5d5b544a0f51558f136d8499c65 Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Wed, 29 Jan 2020 16:12:47 -0800 Subject: [PATCH 88/99] fix panic(err) if posted_prices=[] in genesis file (#352) --- x/pricefeed/genesis.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x/pricefeed/genesis.go b/x/pricefeed/genesis.go index 158d84e6..41a10705 100644 --- a/x/pricefeed/genesis.go +++ b/x/pricefeed/genesis.go @@ -25,9 +25,12 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, gs GenesisState) { // Set the current price (if any) based on what's now in the store for _, market := range params.Markets { if market.Active { - err := keeper.SetCurrentPrices(ctx, market.MarketID) - if err != nil { - panic(err) + rps := keeper.GetRawPrices(ctx, market.MarketID) + if len(rps) > 0 { + err := keeper.SetCurrentPrices(ctx, market.MarketID) + if err != nil { + panic(err) + } } } } From bdff81b2a261fe847cef143dac81ecdceab4c1a6 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Thu, 30 Jan 2020 18:50:42 -0500 Subject: [PATCH 89/99] fix: avoid panic when closing cdp via repayment (#353) --- x/cdp/keeper/cdp.go | 2 +- x/cdp/keeper/draw.go | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/x/cdp/keeper/cdp.go b/x/cdp/keeper/cdp.go index 3fb52fe4..5d729263 100644 --- a/x/cdp/keeper/cdp.go +++ b/x/cdp/keeper/cdp.go @@ -110,7 +110,7 @@ func (k Keeper) MintDebtCoins(ctx sdk.Context, moduleAccount string, denom strin return nil } -// BurnDebtCoins burns debts coins from the cdp module account +// BurnDebtCoins burns debt coins from the cdp module account func (k Keeper) BurnDebtCoins(ctx sdk.Context, moduleAccount string, denom string, paymentCoins sdk.Coins) sdk.Error { coinsToBurn := sdk.NewCoins() for _, pc := range paymentCoins { diff --git a/x/cdp/keeper/draw.go b/x/cdp/keeper/draw.go index 54abede7..94665f5b 100644 --- a/x/cdp/keeper/draw.go +++ b/x/cdp/keeper/draw.go @@ -110,7 +110,16 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, denom stri } // burn the corresponding amount of debt coins - err = k.BurnDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), feePayment.Add(principalPayment)) + cdpDebt := k.getModAccountDebt(ctx, types.ModuleName) + paymentAmount := sdk.ZeroInt() + for _, c := range feePayment.Add(principalPayment) { + paymentAmount = paymentAmount.Add(c.Amount) + } + coinsToBurn := sdk.NewCoins(sdk.NewCoin(k.GetDebtDenom(ctx), paymentAmount)) + if paymentAmount.GT(cdpDebt) { + coinsToBurn = sdk.NewCoins(sdk.NewCoin(k.GetDebtDenom(ctx), cdpDebt)) + } + err = k.BurnDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), coinsToBurn) if err != nil { panic(err) } From cd754a25d9db40c286800dbd434fc7f3526f157c Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Fri, 31 Jan 2020 17:31:22 -0500 Subject: [PATCH 90/99] [R4R] Minor rest fixes (#355) * feat: add swagger * add version info for mainnet * feat: update swagger * fix: type in get params path * feat: example for creating cdp * feat: example broadcast signed tx * feat: examples for depositing to cdp * added README with example cdp create rest-server request (#358) * fix: make link-check happy Co-authored-by: Denali Marsh --- Makefile | 2 +- README.md | 5 + contrib/README.md | 40 + contrib/requests/broadcast-create-cdp.json | 1 + contrib/requests/broadcast-deposit-cdp.json | 34 + contrib/requests/create-cdp-unsigned.json | 31 + contrib/requests/create-cdp.json | 25 + contrib/requests/deposit-cdp.json | 20 + contrib/requests/example-create-cdp.json | 25 + swagger-ui/favicon-16x16.png | Bin 0 -> 665 bytes swagger-ui/favicon-32x32.png | Bin 0 -> 628 bytes swagger-ui/index.html | 60 + swagger-ui/oauth2-redirect.html | 68 + swagger-ui/swagger-ui-bundle.js | 134 + swagger-ui/swagger-ui-bundle.js.map | 1 + swagger-ui/swagger-ui-standalone-preset.js | 22 + .../swagger-ui-standalone-preset.js.map | 1 + swagger-ui/swagger-ui.css | 4 + swagger-ui/swagger-ui.css.map | 1 + swagger-ui/swagger-ui.js | 9 + swagger-ui/swagger-ui.js.map | 1 + swagger-ui/swagger.yaml | 2564 +++++++++++++++++ x/cdp/client/rest/query.go | 2 +- 23 files changed, 3048 insertions(+), 2 deletions(-) create mode 100644 contrib/README.md create mode 100644 contrib/requests/broadcast-create-cdp.json create mode 100644 contrib/requests/broadcast-deposit-cdp.json create mode 100644 contrib/requests/create-cdp-unsigned.json create mode 100644 contrib/requests/create-cdp.json create mode 100644 contrib/requests/deposit-cdp.json create mode 100644 contrib/requests/example-create-cdp.json create mode 100644 swagger-ui/favicon-16x16.png create mode 100644 swagger-ui/favicon-32x32.png create mode 100644 swagger-ui/index.html create mode 100644 swagger-ui/oauth2-redirect.html create mode 100644 swagger-ui/swagger-ui-bundle.js create mode 100644 swagger-ui/swagger-ui-bundle.js.map create mode 100644 swagger-ui/swagger-ui-standalone-preset.js create mode 100644 swagger-ui/swagger-ui-standalone-preset.js.map create mode 100644 swagger-ui/swagger-ui.css create mode 100644 swagger-ui/swagger-ui.css.map create mode 100644 swagger-ui/swagger-ui.js create mode 100644 swagger-ui/swagger-ui.js.map create mode 100644 swagger-ui/swagger.yaml diff --git a/Makefile b/Makefile index 4cbed021..d2712d6f 100644 --- a/Makefile +++ b/Makefile @@ -101,7 +101,7 @@ clean: # This tool checks local markdown links as well. # Set to exclude riot links as they trigger false positives link-check: - @go run github.com/raviqqe/liche -r . --exclude ^https://riot.im/app + @go run github.com/raviqqe/liche -r . --exclude "^http://127.*|^https://riot.im/app*" ######################################## ### Testing diff --git a/README.md b/README.md index cbfa254a..b91ae1ae 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,17 @@ Reference implementation of Kava, a blockchain for cross-chain DeFi. Built using the [comsos-sdk](https://github.com/cosmos/cosmos-sdk). + ## Quick Start ```sh make install ``` +## Mainnet + +Note, the current reccomended version of the software for mainnet is v0.3.1. + To join the latest testnet, head over to the [testnet repo](https://github.com/Kava-Labs/kava-testnets). ## License diff --git a/contrib/README.md b/contrib/README.md new file mode 100644 index 00000000..679c5730 --- /dev/null +++ b/contrib/README.md @@ -0,0 +1,40 @@ + +# Contrib + +## Requests + +### Create CDP example request + + First, query account information for the signing account. Note the 'accountnumber' and 'sequence' fields, we'll need them later in order to send our request: + +```bash + kvcli q auth account $(kvcli keys show accB -a) +``` + +If testing locally, start the Kava rest server: + +```bash + kvcli rest-server +``` + + Format the base request in create-cdp.json. You'll need to update the 'from', 'chain-id', 'account_number', 'sequence', and 'gas' as appropriate. Then, populate the CDP creation request's params 'owner', 'collateral', and 'principal'. An example formatted base request can be found in `example-create-cdp.json`. + + Now we'll create an unsigned request, sign it, and broadcast it to the Kava blockchain via the rest server. + Note that if you're using the mainnet or testnet, the host IP address will need to be updated to point at an active rest server. + +```bash + # Create an unsigned request + curl -H "Content-Type: application/json" -X PUT -d @./contrib/requests/create-cdp.json http://127.0.0.1:1317/cdp | jq > ./contrib/requests/create-cdp-unsigned.json + + # Sign the request + kvcli tx sign ./contrib/requests/create-cdp-unsigned.json --from accB --offline --chain-id testing --sequence 1 --account-number 2 > ./contrib/requests/broadcast-create-cdp.json + + # Broadcast the request + kvcli tx broadcast ./contrib/requests/broadcast-create-cdp.json +``` + +Congratulations, you've just created a CDP on Kava using the rest server! + +## Governance proposals + +Example governance proposals are located in `/proposal_examples`. \ No newline at end of file diff --git a/contrib/requests/broadcast-create-cdp.json b/contrib/requests/broadcast-create-cdp.json new file mode 100644 index 00000000..94ef98fb --- /dev/null +++ b/contrib/requests/broadcast-create-cdp.json @@ -0,0 +1 @@ +{"type":"cosmos-sdk/StdTx","value":{"msg":[{"type":"cdp/MsgCreateCDP","value":{"sender":"kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03","collateral":[{"denom":"xrp","amount":"110000000"}],"principal":[{"denom":"usdx","amount":"10000000"}]}}],"fee":{"amount":[],"gas":"500000"},"signatures":[{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AxYhgA8CL/ZmTZCQDS2dGcccu+dv7zOBT0Wz0+grIufW"},"signature":"KMGQekAgj9qSqnu38swM/jTnIkrSskL4m9L3QQlBIbVZdzDQVh9Rkb0UL7TPeXTaswlSTiwFic3hvZZOcjM4SA=="}],"memo":""}} diff --git a/contrib/requests/broadcast-deposit-cdp.json b/contrib/requests/broadcast-deposit-cdp.json new file mode 100644 index 00000000..a52c0c54 --- /dev/null +++ b/contrib/requests/broadcast-deposit-cdp.json @@ -0,0 +1,34 @@ +{ + "tx": { + "msg": [ + { + "type": "cdp/MsgDeposit", + "value": { + "depositor": "kava1rjnv8ns2wvz9calje5pmcnpxm055pegprywvjj", + "owner": "kava1mzqz3jfs9nzfp6v7qp647rv0afxlu2csl0txmq", + "collateral": [ + { + "denom": "xrp", + "amount": "110000000" + } + ] + } + } + ], + "fee": { + "amount": [], + "gas": "500000" + }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "A2SB6wV+X9cVl6URZ28mmfxNQwFJD1u3oo5G332jMa2L" + }, + "signature": "mJCjAhEVS/2KCvmGpcnvmeHCBo/UEu3/SbdhoyWkp8sDOLe5FO6016HyjUMJ2jJh80aSBJpPFQyyh2gOFQluXw==" + } + ], + "memo": "" + }, + "mode": "sync" +} \ No newline at end of file diff --git a/contrib/requests/create-cdp-unsigned.json b/contrib/requests/create-cdp-unsigned.json new file mode 100644 index 00000000..6ad631aa --- /dev/null +++ b/contrib/requests/create-cdp-unsigned.json @@ -0,0 +1,31 @@ +{ + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cdp/MsgCreateCDP", + "value": { + "sender": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03", + "collateral": [ + { + "denom": "xrp", + "amount": "110000000" + } + ], + "principal": [ + { + "denom": "usdx", + "amount": "10000000" + } + ] + } + } + ], + "fee": { + "amount": [], + "gas": "500000" + }, + "signatures": null, + "memo": "" + } +} diff --git a/contrib/requests/create-cdp.json b/contrib/requests/create-cdp.json new file mode 100644 index 00000000..743ffb8d --- /dev/null +++ b/contrib/requests/create-cdp.json @@ -0,0 +1,25 @@ +{ + "base_req": { + "from": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03", + "memo": "", + "chain_id": "testing", + "account_number": "2", + "sequence": "0", + "gas": "500000", + "gas_adjustment": "1.0", + "simulate": false + }, + "owner": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03", + "collateral": [ + { + "denom": "xrp", + "amount": "110000000" + } + ], + "principal": [ + { + "denom": "usdx", + "amount": "10000000" + } + ] +} \ No newline at end of file diff --git a/contrib/requests/deposit-cdp.json b/contrib/requests/deposit-cdp.json new file mode 100644 index 00000000..b7770c9b --- /dev/null +++ b/contrib/requests/deposit-cdp.json @@ -0,0 +1,20 @@ +{ + "base_req": { + "from": "kava1rjnv8ns2wvz9calje5pmcnpxm055pegprywvjj", + "memo": "", + "chain_id": "kava-internal", + "account_number": "12", + "sequence": "0", + "gas": "500000", + "gas_adjustment": "1.0", + "simulate": false + }, + "owner": "kava1mzqz3jfs9nzfp6v7qp647rv0afxlu2csl0txmq", + "depositor": "kava1rjnv8ns2wvz9calje5pmcnpxm055pegprywvjj", + "collateral": [ + { + "denom": "xrp", + "amount": "110000000" + } + ] +} \ No newline at end of file diff --git a/contrib/requests/example-create-cdp.json b/contrib/requests/example-create-cdp.json new file mode 100644 index 00000000..743ffb8d --- /dev/null +++ b/contrib/requests/example-create-cdp.json @@ -0,0 +1,25 @@ +{ + "base_req": { + "from": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03", + "memo": "", + "chain_id": "testing", + "account_number": "2", + "sequence": "0", + "gas": "500000", + "gas_adjustment": "1.0", + "simulate": false + }, + "owner": "kava12jk3szk45afmvjc3xc6kvj4e40tuy2m8ckgs03", + "collateral": [ + { + "denom": "xrp", + "amount": "110000000" + } + ], + "principal": [ + { + "denom": "usdx", + "amount": "10000000" + } + ] +} \ No newline at end of file diff --git a/swagger-ui/favicon-16x16.png b/swagger-ui/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..8b194e617af1c135e6b37939591d24ac3a5efa18 GIT binary patch literal 665 zcmV;K0%rY*P)}JKSduyL>)s!A4EhTMMEM%Q;aL6%l#xiZiF>S;#Y{N2Zz%pvTGHJduXuC6Lx-)0EGfRy*N{Tv4i8@4oJ41gw zKzThrcRe|7J~(YYIBq{SYCkn-KQm=N8$CrEK1CcqMI1dv9z#VRL_{D)L|`QmF8}}l zJ9JV`Q}p!p_4f7m_U`WQ@apR4;o;!mnU<7}iG_qr zF(e)x9~BG-3IzcG2M4an0002kNkl41`ZiN1i62V%{PM@Ry|IS_+Yc7{bb`MM~xm(7p4|kMHP&!VGuDW4kFixat zXw43VmgwEvB$hXt_u=vZ>+v4i7E}n~eG6;n4Z=zF1n?T*yg<;W6kOfxpC6nao>VR% z?fpr=asSJ&`L*wu^rLJ5Peq*PB0;alL#XazZCBxJLd&giTfw@!hW167F^`7kobi;( ze<<>qNlP|xy7S1zl@lZNIBR7#o9ybJsptO#%}P0hz~sBp00000NkvXXu0mjfUsDF? literal 0 HcmV?d00001 diff --git a/swagger-ui/favicon-32x32.png b/swagger-ui/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..249737fe44558e679f0b67134e274461d988fa98 GIT binary patch literal 628 zcmV-)0*n2LP)Ma*GM0}OV<074bNCP7P7GVd{iMr*I6y~TMLss@FjvgL~HxU z%Vvj33AwpD(Z4*$Mfx=HaU16axM zt2xG_rloN<$iy9j9I5 + + + + + Swagger UI + + + + + + + +
+ + + + + + diff --git a/swagger-ui/oauth2-redirect.html b/swagger-ui/oauth2-redirect.html new file mode 100644 index 00000000..a013fc82 --- /dev/null +++ b/swagger-ui/oauth2-redirect.html @@ -0,0 +1,68 @@ + + +Swagger UI: OAuth2 Redirect + + + + diff --git a/swagger-ui/swagger-ui-bundle.js b/swagger-ui/swagger-ui-bundle.js new file mode 100644 index 00000000..559df3ca --- /dev/null +++ b/swagger-ui/swagger-ui-bundle.js @@ -0,0 +1,134 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(function(){try{return require("esprima")}catch(e){}}()):"function"==typeof define&&define.amd?define(["esprima"],t):"object"==typeof exports?exports.SwaggerUIBundle=t(function(){try{return require("esprima")}catch(e){}}()):e.SwaggerUIBundle=t(e.esprima)}(window,function(e){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist",n(n.s=488)}([function(e,t,n){"use strict";e.exports=n(104)},function(e,t,n){e.exports=function(){"use strict";var e=Array.prototype.slice;function t(e,t){t&&(e.prototype=Object.create(t.prototype)),e.prototype.constructor=e}function n(e){return a(e)?e:J(e)}function r(e){return s(e)?e:K(e)}function o(e){return u(e)?e:Y(e)}function i(e){return a(e)&&!c(e)?e:$(e)}function a(e){return!(!e||!e[p])}function s(e){return!(!e||!e[f])}function u(e){return!(!e||!e[h])}function c(e){return s(e)||u(e)}function l(e){return!(!e||!e[d])}t(r,n),t(o,n),t(i,n),n.isIterable=a,n.isKeyed=s,n.isIndexed=u,n.isAssociative=c,n.isOrdered=l,n.Keyed=r,n.Indexed=o,n.Set=i;var p="@@__IMMUTABLE_ITERABLE__@@",f="@@__IMMUTABLE_KEYED__@@",h="@@__IMMUTABLE_INDEXED__@@",d="@@__IMMUTABLE_ORDERED__@@",m=5,v=1<>>0;if(""+n!==t||4294967295===n)return NaN;t=n}return t<0?C(e)+t:t}function O(){return!0}function A(e,t,n){return(0===e||void 0!==n&&e<=-n)&&(void 0===t||void 0!==n&&t>=n)}function T(e,t){return P(e,t,0)}function j(e,t){return P(e,t,t)}function P(e,t,n){return void 0===e?n:e<0?Math.max(0,t+e):void 0===t?e:Math.min(t,e)}var I=0,M=1,N=2,R="function"==typeof Symbol&&Symbol.iterator,D="@@iterator",L=R||D;function U(e){this.next=e}function q(e,t,n,r){var o=0===e?t:1===e?n:[t,n];return r?r.value=o:r={value:o,done:!1},r}function F(){return{value:void 0,done:!0}}function B(e){return!!H(e)}function z(e){return e&&"function"==typeof e.next}function V(e){var t=H(e);return t&&t.call(e)}function H(e){var t=e&&(R&&e[R]||e[D]);if("function"==typeof t)return t}function W(e){return e&&"number"==typeof e.length}function J(e){return null==e?ie():a(e)?e.toSeq():function(e){var t=ue(e)||"object"==typeof e&&new te(e);if(!t)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+e);return t}(e)}function K(e){return null==e?ie().toKeyedSeq():a(e)?s(e)?e.toSeq():e.fromEntrySeq():ae(e)}function Y(e){return null==e?ie():a(e)?s(e)?e.entrySeq():e.toIndexedSeq():se(e)}function $(e){return(null==e?ie():a(e)?s(e)?e.entrySeq():e:se(e)).toSetSeq()}U.prototype.toString=function(){return"[Iterator]"},U.KEYS=I,U.VALUES=M,U.ENTRIES=N,U.prototype.inspect=U.prototype.toSource=function(){return this.toString()},U.prototype[L]=function(){return this},t(J,n),J.of=function(){return J(arguments)},J.prototype.toSeq=function(){return this},J.prototype.toString=function(){return this.__toString("Seq {","}")},J.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},J.prototype.__iterate=function(e,t){return ce(this,e,t,!0)},J.prototype.__iterator=function(e,t){return le(this,e,t,!0)},t(K,J),K.prototype.toKeyedSeq=function(){return this},t(Y,J),Y.of=function(){return Y(arguments)},Y.prototype.toIndexedSeq=function(){return this},Y.prototype.toString=function(){return this.__toString("Seq [","]")},Y.prototype.__iterate=function(e,t){return ce(this,e,t,!1)},Y.prototype.__iterator=function(e,t){return le(this,e,t,!1)},t($,J),$.of=function(){return $(arguments)},$.prototype.toSetSeq=function(){return this},J.isSeq=oe,J.Keyed=K,J.Set=$,J.Indexed=Y;var G,Z,X,Q="@@__IMMUTABLE_SEQ__@@";function ee(e){this._array=e,this.size=e.length}function te(e){var t=Object.keys(e);this._object=e,this._keys=t,this.size=t.length}function ne(e){this._iterable=e,this.size=e.length||e.size}function re(e){this._iterator=e,this._iteratorCache=[]}function oe(e){return!(!e||!e[Q])}function ie(){return G||(G=new ee([]))}function ae(e){var t=Array.isArray(e)?new ee(e).fromEntrySeq():z(e)?new re(e).fromEntrySeq():B(e)?new ne(e).fromEntrySeq():"object"==typeof e?new te(e):void 0;if(!t)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+e);return t}function se(e){var t=ue(e);if(!t)throw new TypeError("Expected Array or iterable object of values: "+e);return t}function ue(e){return W(e)?new ee(e):z(e)?new re(e):B(e)?new ne(e):void 0}function ce(e,t,n,r){var o=e._cache;if(o){for(var i=o.length-1,a=0;a<=i;a++){var s=o[n?i-a:a];if(!1===t(s[1],r?s[0]:a,e))return a+1}return a}return e.__iterateUncached(t,n)}function le(e,t,n,r){var o=e._cache;if(o){var i=o.length-1,a=0;return new U(function(){var e=o[n?i-a:a];return a++>i?{value:void 0,done:!0}:q(t,r?e[0]:a-1,e[1])})}return e.__iteratorUncached(t,n)}function pe(e,t){return t?function e(t,n,r,o){return Array.isArray(n)?t.call(o,r,Y(n).map(function(r,o){return e(t,r,o,n)})):he(n)?t.call(o,r,K(n).map(function(r,o){return e(t,r,o,n)})):n}(t,e,"",{"":e}):fe(e)}function fe(e){return Array.isArray(e)?Y(e).map(fe).toList():he(e)?K(e).map(fe).toMap():e}function he(e){return e&&(e.constructor===Object||void 0===e.constructor)}function de(e,t){if(e===t||e!=e&&t!=t)return!0;if(!e||!t)return!1;if("function"==typeof e.valueOf&&"function"==typeof t.valueOf){if((e=e.valueOf())===(t=t.valueOf())||e!=e&&t!=t)return!0;if(!e||!t)return!1}return!("function"!=typeof e.equals||"function"!=typeof t.equals||!e.equals(t))}function me(e,t){if(e===t)return!0;if(!a(t)||void 0!==e.size&&void 0!==t.size&&e.size!==t.size||void 0!==e.__hash&&void 0!==t.__hash&&e.__hash!==t.__hash||s(e)!==s(t)||u(e)!==u(t)||l(e)!==l(t))return!1;if(0===e.size&&0===t.size)return!0;var n=!c(e);if(l(e)){var r=e.entries();return t.every(function(e,t){var o=r.next().value;return o&&de(o[1],e)&&(n||de(o[0],t))})&&r.next().done}var o=!1;if(void 0===e.size)if(void 0===t.size)"function"==typeof e.cacheResult&&e.cacheResult();else{o=!0;var i=e;e=t,t=i}var p=!0,f=t.__iterate(function(t,r){if(n?!e.has(t):o?!de(t,e.get(r,y)):!de(e.get(r,y),t))return p=!1,!1});return p&&e.size===f}function ve(e,t){if(!(this instanceof ve))return new ve(e,t);if(this._value=e,this.size=void 0===t?1/0:Math.max(0,t),0===this.size){if(Z)return Z;Z=this}}function ge(e,t){if(!e)throw new Error(t)}function ye(e,t,n){if(!(this instanceof ye))return new ye(e,t,n);if(ge(0!==n,"Cannot step a Range by 0"),e=e||0,void 0===t&&(t=1/0),n=void 0===n?1:Math.abs(n),tr?{value:void 0,done:!0}:q(e,o,n[t?r-o++:o++])})},t(te,K),te.prototype.get=function(e,t){return void 0===t||this.has(e)?this._object[e]:t},te.prototype.has=function(e){return this._object.hasOwnProperty(e)},te.prototype.__iterate=function(e,t){for(var n=this._object,r=this._keys,o=r.length-1,i=0;i<=o;i++){var a=r[t?o-i:i];if(!1===e(n[a],a,this))return i+1}return i},te.prototype.__iterator=function(e,t){var n=this._object,r=this._keys,o=r.length-1,i=0;return new U(function(){var a=r[t?o-i:i];return i++>o?{value:void 0,done:!0}:q(e,a,n[a])})},te.prototype[d]=!0,t(ne,Y),ne.prototype.__iterateUncached=function(e,t){if(t)return this.cacheResult().__iterate(e,t);var n=V(this._iterable),r=0;if(z(n))for(var o;!(o=n.next()).done&&!1!==e(o.value,r++,this););return r},ne.prototype.__iteratorUncached=function(e,t){if(t)return this.cacheResult().__iterator(e,t);var n=V(this._iterable);if(!z(n))return new U(F);var r=0;return new U(function(){var t=n.next();return t.done?t:q(e,r++,t.value)})},t(re,Y),re.prototype.__iterateUncached=function(e,t){if(t)return this.cacheResult().__iterate(e,t);for(var n,r=this._iterator,o=this._iteratorCache,i=0;i=r.length){var t=n.next();if(t.done)return t;r[o]=t.value}return q(e,o,r[o++])})},t(ve,Y),ve.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},ve.prototype.get=function(e,t){return this.has(e)?this._value:t},ve.prototype.includes=function(e){return de(this._value,e)},ve.prototype.slice=function(e,t){var n=this.size;return A(e,t,n)?this:new ve(this._value,j(t,n)-T(e,n))},ve.prototype.reverse=function(){return this},ve.prototype.indexOf=function(e){return de(this._value,e)?0:-1},ve.prototype.lastIndexOf=function(e){return de(this._value,e)?this.size:-1},ve.prototype.__iterate=function(e,t){for(var n=0;n=0&&t=0&&nn?{value:void 0,done:!0}:q(e,i++,a)})},ye.prototype.equals=function(e){return e instanceof ye?this._start===e._start&&this._end===e._end&&this._step===e._step:me(this,e)},t(be,n),t(_e,be),t(we,be),t(xe,be),be.Keyed=_e,be.Indexed=we,be.Set=xe;var Ee="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function(e,t){var n=65535&(e|=0),r=65535&(t|=0);return n*r+((e>>>16)*r+n*(t>>>16)<<16>>>0)|0};function Se(e){return e>>>1&1073741824|3221225471&e}function Ce(e){if(!1===e||null==e)return 0;if("function"==typeof e.valueOf&&(!1===(e=e.valueOf())||null==e))return 0;if(!0===e)return 1;var t=typeof e;if("number"===t){if(e!=e||e===1/0)return 0;var n=0|e;for(n!==e&&(n^=4294967295*e);e>4294967295;)n^=e/=4294967295;return Se(n)}if("string"===t)return e.length>Me?function(e){var t=De[e];return void 0===t&&(t=ke(e),Re===Ne&&(Re=0,De={}),Re++,De[e]=t),t}(e):ke(e);if("function"==typeof e.hashCode)return e.hashCode();if("object"===t)return function(e){var t;if(je&&void 0!==(t=Oe.get(e)))return t;if(void 0!==(t=e[Ie]))return t;if(!Te){if(void 0!==(t=e.propertyIsEnumerable&&e.propertyIsEnumerable[Ie]))return t;if(void 0!==(t=function(e){if(e&&e.nodeType>0)switch(e.nodeType){case 1:return e.uniqueID;case 9:return e.documentElement&&e.documentElement.uniqueID}}(e)))return t}if(t=++Pe,1073741824&Pe&&(Pe=0),je)Oe.set(e,t);else{if(void 0!==Ae&&!1===Ae(e))throw new Error("Non-extensible objects are not allowed as keys.");if(Te)Object.defineProperty(e,Ie,{enumerable:!1,configurable:!1,writable:!1,value:t});else if(void 0!==e.propertyIsEnumerable&&e.propertyIsEnumerable===e.constructor.prototype.propertyIsEnumerable)e.propertyIsEnumerable=function(){return this.constructor.prototype.propertyIsEnumerable.apply(this,arguments)},e.propertyIsEnumerable[Ie]=t;else{if(void 0===e.nodeType)throw new Error("Unable to set a non-enumerable property on object.");e[Ie]=t}}return t}(e);if("function"==typeof e.toString)return ke(e.toString());throw new Error("Value type "+t+" cannot be hashed.")}function ke(e){for(var t=0,n=0;n=t.length)throw new Error("Missing value for key: "+t[n]);e.set(t[n],t[n+1])}})},Ue.prototype.toString=function(){return this.__toString("Map {","}")},Ue.prototype.get=function(e,t){return this._root?this._root.get(0,void 0,e,t):t},Ue.prototype.set=function(e,t){return Qe(this,e,t)},Ue.prototype.setIn=function(e,t){return this.updateIn(e,y,function(){return t})},Ue.prototype.remove=function(e){return Qe(this,e,y)},Ue.prototype.deleteIn=function(e){return this.updateIn(e,function(){return y})},Ue.prototype.update=function(e,t,n){return 1===arguments.length?e(this):this.updateIn([e],t,n)},Ue.prototype.updateIn=function(e,t,n){n||(n=t,t=void 0);var r=function e(t,n,r,o){var i=t===y,a=n.next();if(a.done){var s=i?r:t,u=o(s);return u===s?t:u}ge(i||t&&t.set,"invalid keyPath");var c=a.value,l=i?y:t.get(c,y),p=e(l,n,r,o);return p===l?t:p===y?t.remove(c):(i?Xe():t).set(c,p)}(this,rn(e),t,n);return r===y?void 0:r},Ue.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):Xe()},Ue.prototype.merge=function(){return rt(this,void 0,arguments)},Ue.prototype.mergeWith=function(t){var n=e.call(arguments,1);return rt(this,t,n)},Ue.prototype.mergeIn=function(t){var n=e.call(arguments,1);return this.updateIn(t,Xe(),function(e){return"function"==typeof e.merge?e.merge.apply(e,n):n[n.length-1]})},Ue.prototype.mergeDeep=function(){return rt(this,ot,arguments)},Ue.prototype.mergeDeepWith=function(t){var n=e.call(arguments,1);return rt(this,it(t),n)},Ue.prototype.mergeDeepIn=function(t){var n=e.call(arguments,1);return this.updateIn(t,Xe(),function(e){return"function"==typeof e.mergeDeep?e.mergeDeep.apply(e,n):n[n.length-1]})},Ue.prototype.sort=function(e){return Tt(Jt(this,e))},Ue.prototype.sortBy=function(e,t){return Tt(Jt(this,t,e))},Ue.prototype.withMutations=function(e){var t=this.asMutable();return e(t),t.wasAltered()?t.__ensureOwner(this.__ownerID):this},Ue.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new E)},Ue.prototype.asImmutable=function(){return this.__ensureOwner()},Ue.prototype.wasAltered=function(){return this.__altered},Ue.prototype.__iterator=function(e,t){return new Ye(this,e,t)},Ue.prototype.__iterate=function(e,t){var n=this,r=0;return this._root&&this._root.iterate(function(t){return r++,e(t[1],t[0],n)},t),r},Ue.prototype.__ensureOwner=function(e){return e===this.__ownerID?this:e?Ze(this.size,this._root,e,this.__hash):(this.__ownerID=e,this.__altered=!1,this)},Ue.isMap=qe;var Fe,Be="@@__IMMUTABLE_MAP__@@",ze=Ue.prototype;function Ve(e,t){this.ownerID=e,this.entries=t}function He(e,t,n){this.ownerID=e,this.bitmap=t,this.nodes=n}function We(e,t,n){this.ownerID=e,this.count=t,this.nodes=n}function Je(e,t,n){this.ownerID=e,this.keyHash=t,this.entries=n}function Ke(e,t,n){this.ownerID=e,this.keyHash=t,this.entry=n}function Ye(e,t,n){this._type=t,this._reverse=n,this._stack=e._root&&Ge(e._root)}function $e(e,t){return q(e,t[0],t[1])}function Ge(e,t){return{node:e,index:0,__prev:t}}function Ze(e,t,n,r){var o=Object.create(ze);return o.size=e,o._root=t,o.__ownerID=n,o.__hash=r,o.__altered=!1,o}function Xe(){return Fe||(Fe=Ze(0))}function Qe(e,t,n){var r,o;if(e._root){var i=w(b),a=w(_);if(r=et(e._root,e.__ownerID,0,void 0,t,n,i,a),!a.value)return e;o=e.size+(i.value?n===y?-1:1:0)}else{if(n===y)return e;o=1,r=new Ve(e.__ownerID,[[t,n]])}return e.__ownerID?(e.size=o,e._root=r,e.__hash=void 0,e.__altered=!0,e):r?Ze(o,r):Xe()}function et(e,t,n,r,o,i,a,s){return e?e.update(t,n,r,o,i,a,s):i===y?e:(x(s),x(a),new Ke(t,r,[o,i]))}function tt(e){return e.constructor===Ke||e.constructor===Je}function nt(e,t,n,r,o){if(e.keyHash===r)return new Je(t,r,[e.entry,o]);var i,a=(0===n?e.keyHash:e.keyHash>>>n)&g,s=(0===n?r:r>>>n)&g;return new He(t,1<>1&1431655765))+(e>>2&858993459))+(e>>4)&252645135,e+=e>>8,127&(e+=e>>16)}function ut(e,t,n,r){var o=r?e:S(e);return o[t]=n,o}ze[Be]=!0,ze.delete=ze.remove,ze.removeIn=ze.deleteIn,Ve.prototype.get=function(e,t,n,r){for(var o=this.entries,i=0,a=o.length;i=ct)return function(e,t,n,r){e||(e=new E);for(var o=new Ke(e,Ce(n),[n,r]),i=0;i>>e)&g),i=this.bitmap;return 0==(i&o)?r:this.nodes[st(i&o-1)].get(e+m,t,n,r)},He.prototype.update=function(e,t,n,r,o,i,a){void 0===n&&(n=Ce(r));var s=(0===t?n:n>>>t)&g,u=1<=lt)return function(e,t,n,r,o){for(var i=0,a=new Array(v),s=0;0!==n;s++,n>>>=1)a[s]=1&n?t[i++]:void 0;return a[r]=o,new We(e,i+1,a)}(e,f,c,s,d);if(l&&!d&&2===f.length&&tt(f[1^p]))return f[1^p];if(l&&d&&1===f.length&&tt(d))return d;var b=e&&e===this.ownerID,_=l?d?c:c^u:c|u,w=l?d?ut(f,p,d,b):function(e,t,n){var r=e.length-1;if(n&&t===r)return e.pop(),e;for(var o=new Array(r),i=0,a=0;a>>e)&g,i=this.nodes[o];return i?i.get(e+m,t,n,r):r},We.prototype.update=function(e,t,n,r,o,i,a){void 0===n&&(n=Ce(r));var s=(0===t?n:n>>>t)&g,u=o===y,c=this.nodes,l=c[s];if(u&&!l)return this;var p=et(l,e,t+m,n,r,o,i,a);if(p===l)return this;var f=this.count;if(l){if(!p&&--f0&&r=0&&e=e.size||t<0)return e.withMutations(function(e){t<0?kt(e,t).set(0,n):kt(e,0,t+1).set(t,n)});t+=e._origin;var r=e._tail,o=e._root,i=w(_);return t>=At(e._capacity)?r=Et(r,e.__ownerID,0,t,n,i):o=Et(o,e.__ownerID,e._level,t,n,i),i.value?e.__ownerID?(e._root=o,e._tail=r,e.__hash=void 0,e.__altered=!0,e):wt(e._origin,e._capacity,e._level,o,r):e}(this,e,t)},ft.prototype.remove=function(e){return this.has(e)?0===e?this.shift():e===this.size-1?this.pop():this.splice(e,1):this},ft.prototype.insert=function(e,t){return this.splice(e,0,t)},ft.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=this._origin=this._capacity=0,this._level=m,this._root=this._tail=null,this.__hash=void 0,this.__altered=!0,this):xt()},ft.prototype.push=function(){var e=arguments,t=this.size;return this.withMutations(function(n){kt(n,0,t+e.length);for(var r=0;r>>t&g;if(r>=this.array.length)return new vt([],e);var o,i=0===r;if(t>0){var a=this.array[r];if((o=a&&a.removeBefore(e,t-m,n))===a&&i)return this}if(i&&!o)return this;var s=St(this,e);if(!i)for(var u=0;u>>t&g;if(o>=this.array.length)return this;if(t>0){var i=this.array[o];if((r=i&&i.removeAfter(e,t-m,n))===i&&o===this.array.length-1)return this}var a=St(this,e);return a.array.splice(o+1),r&&(a.array[o]=r),a};var gt,yt,bt={};function _t(e,t){var n=e._origin,r=e._capacity,o=At(r),i=e._tail;return a(e._root,e._level,0);function a(e,s,u){return 0===s?function(e,a){var s=a===o?i&&i.array:e&&e.array,u=a>n?0:n-a,c=r-a;return c>v&&(c=v),function(){if(u===c)return bt;var e=t?--c:u++;return s&&s[e]}}(e,u):function(e,o,i){var s,u=e&&e.array,c=i>n?0:n-i>>o,l=1+(r-i>>o);return l>v&&(l=v),function(){for(;;){if(s){var e=s();if(e!==bt)return e;s=null}if(c===l)return bt;var n=t?--l:c++;s=a(u&&u[n],o-m,i+(n<>>n&g,u=e&&s0){var c=e&&e.array[s],l=Et(c,t,n-m,r,o,i);return l===c?e:((a=St(e,t)).array[s]=l,a)}return u&&e.array[s]===o?e:(x(i),a=St(e,t),void 0===o&&s===a.array.length-1?a.array.pop():a.array[s]=o,a)}function St(e,t){return t&&e&&t===e.ownerID?e:new vt(e?e.array.slice():[],t)}function Ct(e,t){if(t>=At(e._capacity))return e._tail;if(t<1<0;)n=n.array[t>>>r&g],r-=m;return n}}function kt(e,t,n){void 0!==t&&(t|=0),void 0!==n&&(n|=0);var r=e.__ownerID||new E,o=e._origin,i=e._capacity,a=o+t,s=void 0===n?i:n<0?i+n:o+n;if(a===o&&s===i)return e;if(a>=s)return e.clear();for(var u=e._level,c=e._root,l=0;a+l<0;)c=new vt(c&&c.array.length?[void 0,c]:[],r),l+=1<<(u+=m);l&&(a+=l,o+=l,s+=l,i+=l);for(var p=At(i),f=At(s);f>=1<p?new vt([],r):h;if(h&&f>p&&am;y-=m){var b=p>>>y&g;v=v.array[b]=St(v.array[b],r)}v.array[p>>>m&g]=h}if(s=f)a-=f,s-=f,u=m,c=null,d=d&&d.removeBefore(r,0,a);else if(a>o||f>>u&g;if(_!==f>>>u&g)break;_&&(l+=(1<o&&(c=c.removeBefore(r,u,a-l)),c&&fi&&(i=c.size),a(u)||(c=c.map(function(e){return pe(e)})),r.push(c)}return i>e.size&&(e=e.setSize(i)),at(e,t,r)}function At(e){return e>>m<=v&&a.size>=2*i.size?(r=(o=a.filter(function(e,t){return void 0!==e&&s!==t})).toKeyedSeq().map(function(e){return e[0]}).flip().toMap(),e.__ownerID&&(r.__ownerID=o.__ownerID=e.__ownerID)):(r=i.remove(t),o=s===a.size-1?a.pop():a.set(s,void 0))}else if(u){if(n===a.get(s)[1])return e;r=i,o=a.set(s,[t,n])}else r=i.set(t,a.size),o=a.set(a.size,[t,n]);return e.__ownerID?(e.size=r.size,e._map=r,e._list=o,e.__hash=void 0,e):Pt(r,o)}function Nt(e,t){this._iter=e,this._useKeys=t,this.size=e.size}function Rt(e){this._iter=e,this.size=e.size}function Dt(e){this._iter=e,this.size=e.size}function Lt(e){this._iter=e,this.size=e.size}function Ut(e){var t=en(e);return t._iter=e,t.size=e.size,t.flip=function(){return e},t.reverse=function(){var t=e.reverse.apply(this);return t.flip=function(){return e.reverse()},t},t.has=function(t){return e.includes(t)},t.includes=function(t){return e.has(t)},t.cacheResult=tn,t.__iterateUncached=function(t,n){var r=this;return e.__iterate(function(e,n){return!1!==t(n,e,r)},n)},t.__iteratorUncached=function(t,n){if(t===N){var r=e.__iterator(t,n);return new U(function(){var e=r.next();if(!e.done){var t=e.value[0];e.value[0]=e.value[1],e.value[1]=t}return e})}return e.__iterator(t===M?I:M,n)},t}function qt(e,t,n){var r=en(e);return r.size=e.size,r.has=function(t){return e.has(t)},r.get=function(r,o){var i=e.get(r,y);return i===y?o:t.call(n,i,r,e)},r.__iterateUncached=function(r,o){var i=this;return e.__iterate(function(e,o,a){return!1!==r(t.call(n,e,o,a),o,i)},o)},r.__iteratorUncached=function(r,o){var i=e.__iterator(N,o);return new U(function(){var o=i.next();if(o.done)return o;var a=o.value,s=a[0];return q(r,s,t.call(n,a[1],s,e),o)})},r}function Ft(e,t){var n=en(e);return n._iter=e,n.size=e.size,n.reverse=function(){return e},e.flip&&(n.flip=function(){var t=Ut(e);return t.reverse=function(){return e.flip()},t}),n.get=function(n,r){return e.get(t?n:-1-n,r)},n.has=function(n){return e.has(t?n:-1-n)},n.includes=function(t){return e.includes(t)},n.cacheResult=tn,n.__iterate=function(t,n){var r=this;return e.__iterate(function(e,n){return t(e,n,r)},!n)},n.__iterator=function(t,n){return e.__iterator(t,!n)},n}function Bt(e,t,n,r){var o=en(e);return r&&(o.has=function(r){var o=e.get(r,y);return o!==y&&!!t.call(n,o,r,e)},o.get=function(r,o){var i=e.get(r,y);return i!==y&&t.call(n,i,r,e)?i:o}),o.__iterateUncached=function(o,i){var a=this,s=0;return e.__iterate(function(e,i,u){if(t.call(n,e,i,u))return s++,o(e,r?i:s-1,a)},i),s},o.__iteratorUncached=function(o,i){var a=e.__iterator(N,i),s=0;return new U(function(){for(;;){var i=a.next();if(i.done)return i;var u=i.value,c=u[0],l=u[1];if(t.call(n,l,c,e))return q(o,r?c:s++,l,i)}})},o}function zt(e,t,n,r){var o=e.size;if(void 0!==t&&(t|=0),void 0!==n&&(n===1/0?n=o:n|=0),A(t,n,o))return e;var i=T(t,o),a=j(n,o);if(i!=i||a!=a)return zt(e.toSeq().cacheResult(),t,n,r);var s,u=a-i;u==u&&(s=u<0?0:u);var c=en(e);return c.size=0===s?s:e.size&&s||void 0,!r&&oe(e)&&s>=0&&(c.get=function(t,n){return(t=k(this,t))>=0&&ts)return{value:void 0,done:!0};var e=o.next();return r||t===M?e:q(t,u-1,t===I?void 0:e.value[1],e)})},c}function Vt(e,t,n,r){var o=en(e);return o.__iterateUncached=function(o,i){var a=this;if(i)return this.cacheResult().__iterate(o,i);var s=!0,u=0;return e.__iterate(function(e,i,c){if(!s||!(s=t.call(n,e,i,c)))return u++,o(e,r?i:u-1,a)}),u},o.__iteratorUncached=function(o,i){var a=this;if(i)return this.cacheResult().__iterator(o,i);var s=e.__iterator(N,i),u=!0,c=0;return new U(function(){var e,i,l;do{if((e=s.next()).done)return r||o===M?e:q(o,c++,o===I?void 0:e.value[1],e);var p=e.value;i=p[0],l=p[1],u&&(u=t.call(n,l,i,a))}while(u);return o===N?e:q(o,i,l,e)})},o}function Ht(e,t){var n=s(e),o=[e].concat(t).map(function(e){return a(e)?n&&(e=r(e)):e=n?ae(e):se(Array.isArray(e)?e:[e]),e}).filter(function(e){return 0!==e.size});if(0===o.length)return e;if(1===o.length){var i=o[0];if(i===e||n&&s(i)||u(e)&&u(i))return i}var c=new ee(o);return n?c=c.toKeyedSeq():u(e)||(c=c.toSetSeq()),(c=c.flatten(!0)).size=o.reduce(function(e,t){if(void 0!==e){var n=t.size;if(void 0!==n)return e+n}},0),c}function Wt(e,t,n){var r=en(e);return r.__iterateUncached=function(r,o){var i=0,s=!1;return function e(u,c){var l=this;u.__iterate(function(o,u){return(!t||c0}function $t(e,t,r){var o=en(e);return o.size=new ee(r).map(function(e){return e.size}).min(),o.__iterate=function(e,t){for(var n,r=this.__iterator(M,t),o=0;!(n=r.next()).done&&!1!==e(n.value,o++,this););return o},o.__iteratorUncached=function(e,o){var i=r.map(function(e){return e=n(e),V(o?e.reverse():e)}),a=0,s=!1;return new U(function(){var n;return s||(n=i.map(function(e){return e.next()}),s=n.some(function(e){return e.done})),s?{value:void 0,done:!0}:q(e,a++,t.apply(null,n.map(function(e){return e.value})))})},o}function Gt(e,t){return oe(e)?t:e.constructor(t)}function Zt(e){if(e!==Object(e))throw new TypeError("Expected [K, V] tuple: "+e)}function Xt(e){return Le(e.size),C(e)}function Qt(e){return s(e)?r:u(e)?o:i}function en(e){return Object.create((s(e)?K:u(e)?Y:$).prototype)}function tn(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):J.prototype.cacheResult.call(this)}function nn(e,t){return e>t?1:e=0;n--)t={value:arguments[n],next:t};return this.__ownerID?(this.size=e,this._head=t,this.__hash=void 0,this.__altered=!0,this):An(e,t)},En.prototype.pushAll=function(e){if(0===(e=o(e)).size)return this;Le(e.size);var t=this.size,n=this._head;return e.reverse().forEach(function(e){t++,n={value:e,next:n}}),this.__ownerID?(this.size=t,this._head=n,this.__hash=void 0,this.__altered=!0,this):An(t,n)},En.prototype.pop=function(){return this.slice(1)},En.prototype.unshift=function(){return this.push.apply(this,arguments)},En.prototype.unshiftAll=function(e){return this.pushAll(e)},En.prototype.shift=function(){return this.pop.apply(this,arguments)},En.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):Tn()},En.prototype.slice=function(e,t){if(A(e,t,this.size))return this;var n=T(e,this.size);if(j(t,this.size)!==this.size)return we.prototype.slice.call(this,e,t);for(var r=this.size-n,o=this._head;n--;)o=o.next;return this.__ownerID?(this.size=r,this._head=o,this.__hash=void 0,this.__altered=!0,this):An(r,o)},En.prototype.__ensureOwner=function(e){return e===this.__ownerID?this:e?An(this.size,this._head,e,this.__hash):(this.__ownerID=e,this.__altered=!1,this)},En.prototype.__iterate=function(e,t){if(t)return this.reverse().__iterate(e);for(var n=0,r=this._head;r&&!1!==e(r.value,n++,this);)r=r.next;return n},En.prototype.__iterator=function(e,t){if(t)return this.reverse().__iterator(e);var n=0,r=this._head;return new U(function(){if(r){var t=r.value;return r=r.next,q(e,n++,t)}return{value:void 0,done:!0}})},En.isStack=Sn;var Cn,kn="@@__IMMUTABLE_STACK__@@",On=En.prototype;function An(e,t,n,r){var o=Object.create(On);return o.size=e,o._head=t,o.__ownerID=n,o.__hash=r,o.__altered=!1,o}function Tn(){return Cn||(Cn=An(0))}function jn(e,t){var n=function(n){e.prototype[n]=t[n]};return Object.keys(t).forEach(n),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(t).forEach(n),e}On[kn]=!0,On.withMutations=ze.withMutations,On.asMutable=ze.asMutable,On.asImmutable=ze.asImmutable,On.wasAltered=ze.wasAltered,n.Iterator=U,jn(n,{toArray:function(){Le(this.size);var e=new Array(this.size||0);return this.valueSeq().__iterate(function(t,n){e[n]=t}),e},toIndexedSeq:function(){return new Rt(this)},toJS:function(){return this.toSeq().map(function(e){return e&&"function"==typeof e.toJS?e.toJS():e}).__toJS()},toJSON:function(){return this.toSeq().map(function(e){return e&&"function"==typeof e.toJSON?e.toJSON():e}).__toJS()},toKeyedSeq:function(){return new Nt(this,!0)},toMap:function(){return Ue(this.toKeyedSeq())},toObject:function(){Le(this.size);var e={};return this.__iterate(function(t,n){e[n]=t}),e},toOrderedMap:function(){return Tt(this.toKeyedSeq())},toOrderedSet:function(){return gn(s(this)?this.valueSeq():this)},toSet:function(){return cn(s(this)?this.valueSeq():this)},toSetSeq:function(){return new Dt(this)},toSeq:function(){return u(this)?this.toIndexedSeq():s(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return En(s(this)?this.valueSeq():this)},toList:function(){return ft(s(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(e,t){return 0===this.size?e+t:e+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+t},concat:function(){var t=e.call(arguments,0);return Gt(this,Ht(this,t))},includes:function(e){return this.some(function(t){return de(t,e)})},entries:function(){return this.__iterator(N)},every:function(e,t){Le(this.size);var n=!0;return this.__iterate(function(r,o,i){if(!e.call(t,r,o,i))return n=!1,!1}),n},filter:function(e,t){return Gt(this,Bt(this,e,t,!0))},find:function(e,t,n){var r=this.findEntry(e,t);return r?r[1]:n},forEach:function(e,t){return Le(this.size),this.__iterate(t?e.bind(t):e)},join:function(e){Le(this.size),e=void 0!==e?""+e:",";var t="",n=!0;return this.__iterate(function(r){n?n=!1:t+=e,t+=null!=r?r.toString():""}),t},keys:function(){return this.__iterator(I)},map:function(e,t){return Gt(this,qt(this,e,t))},reduce:function(e,t,n){var r,o;return Le(this.size),arguments.length<2?o=!0:r=t,this.__iterate(function(t,i,a){o?(o=!1,r=t):r=e.call(n,r,t,i,a)}),r},reduceRight:function(e,t,n){var r=this.toKeyedSeq().reverse();return r.reduce.apply(r,arguments)},reverse:function(){return Gt(this,Ft(this,!0))},slice:function(e,t){return Gt(this,zt(this,e,t,!0))},some:function(e,t){return!this.every(Rn(e),t)},sort:function(e){return Gt(this,Jt(this,e))},values:function(){return this.__iterator(M)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some(function(){return!0})},count:function(e,t){return C(e?this.toSeq().filter(e,t):this)},countBy:function(e,t){return function(e,t,n){var r=Ue().asMutable();return e.__iterate(function(o,i){r.update(t.call(n,o,i,e),0,function(e){return e+1})}),r.asImmutable()}(this,e,t)},equals:function(e){return me(this,e)},entrySeq:function(){var e=this;if(e._cache)return new ee(e._cache);var t=e.toSeq().map(Nn).toIndexedSeq();return t.fromEntrySeq=function(){return e.toSeq()},t},filterNot:function(e,t){return this.filter(Rn(e),t)},findEntry:function(e,t,n){var r=n;return this.__iterate(function(n,o,i){if(e.call(t,n,o,i))return r=[o,n],!1}),r},findKey:function(e,t){var n=this.findEntry(e,t);return n&&n[0]},findLast:function(e,t,n){return this.toKeyedSeq().reverse().find(e,t,n)},findLastEntry:function(e,t,n){return this.toKeyedSeq().reverse().findEntry(e,t,n)},findLastKey:function(e,t){return this.toKeyedSeq().reverse().findKey(e,t)},first:function(){return this.find(O)},flatMap:function(e,t){return Gt(this,function(e,t,n){var r=Qt(e);return e.toSeq().map(function(o,i){return r(t.call(n,o,i,e))}).flatten(!0)}(this,e,t))},flatten:function(e){return Gt(this,Wt(this,e,!0))},fromEntrySeq:function(){return new Lt(this)},get:function(e,t){return this.find(function(t,n){return de(n,e)},void 0,t)},getIn:function(e,t){for(var n,r=this,o=rn(e);!(n=o.next()).done;){var i=n.value;if((r=r&&r.get?r.get(i,y):y)===y)return t}return r},groupBy:function(e,t){return function(e,t,n){var r=s(e),o=(l(e)?Tt():Ue()).asMutable();e.__iterate(function(i,a){o.update(t.call(n,i,a,e),function(e){return(e=e||[]).push(r?[a,i]:i),e})});var i=Qt(e);return o.map(function(t){return Gt(e,i(t))})}(this,e,t)},has:function(e){return this.get(e,y)!==y},hasIn:function(e){return this.getIn(e,y)!==y},isSubset:function(e){return e="function"==typeof e.includes?e:n(e),this.every(function(t){return e.includes(t)})},isSuperset:function(e){return(e="function"==typeof e.isSubset?e:n(e)).isSubset(this)},keyOf:function(e){return this.findKey(function(t){return de(t,e)})},keySeq:function(){return this.toSeq().map(Mn).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(e){return this.toKeyedSeq().reverse().keyOf(e)},max:function(e){return Kt(this,e)},maxBy:function(e,t){return Kt(this,t,e)},min:function(e){return Kt(this,e?Dn(e):qn)},minBy:function(e,t){return Kt(this,t?Dn(t):qn,e)},rest:function(){return this.slice(1)},skip:function(e){return this.slice(Math.max(0,e))},skipLast:function(e){return Gt(this,this.toSeq().reverse().skip(e).reverse())},skipWhile:function(e,t){return Gt(this,Vt(this,e,t,!0))},skipUntil:function(e,t){return this.skipWhile(Rn(e),t)},sortBy:function(e,t){return Gt(this,Jt(this,t,e))},take:function(e){return this.slice(0,Math.max(0,e))},takeLast:function(e){return Gt(this,this.toSeq().reverse().take(e).reverse())},takeWhile:function(e,t){return Gt(this,function(e,t,n){var r=en(e);return r.__iterateUncached=function(r,o){var i=this;if(o)return this.cacheResult().__iterate(r,o);var a=0;return e.__iterate(function(e,o,s){return t.call(n,e,o,s)&&++a&&r(e,o,i)}),a},r.__iteratorUncached=function(r,o){var i=this;if(o)return this.cacheResult().__iterator(r,o);var a=e.__iterator(N,o),s=!0;return new U(function(){if(!s)return{value:void 0,done:!0};var e=a.next();if(e.done)return e;var o=e.value,u=o[0],c=o[1];return t.call(n,c,u,i)?r===N?e:q(r,u,c,e):(s=!1,{value:void 0,done:!0})})},r}(this,e,t))},takeUntil:function(e,t){return this.takeWhile(Rn(e),t)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=function(e){if(e.size===1/0)return 0;var t=l(e),n=s(e),r=t?1:0;return function(e,t){return t=Ee(t,3432918353),t=Ee(t<<15|t>>>-15,461845907),t=Ee(t<<13|t>>>-13,5),t=Ee((t=(t+3864292196|0)^e)^t>>>16,2246822507),t=Se((t=Ee(t^t>>>13,3266489909))^t>>>16)}(e.__iterate(n?t?function(e,t){r=31*r+Fn(Ce(e),Ce(t))|0}:function(e,t){r=r+Fn(Ce(e),Ce(t))|0}:t?function(e){r=31*r+Ce(e)|0}:function(e){r=r+Ce(e)|0}),r)}(this))}});var Pn=n.prototype;Pn[p]=!0,Pn[L]=Pn.values,Pn.__toJS=Pn.toArray,Pn.__toStringMapper=Ln,Pn.inspect=Pn.toSource=function(){return this.toString()},Pn.chain=Pn.flatMap,Pn.contains=Pn.includes,jn(r,{flip:function(){return Gt(this,Ut(this))},mapEntries:function(e,t){var n=this,r=0;return Gt(this,this.toSeq().map(function(o,i){return e.call(t,[i,o],r++,n)}).fromEntrySeq())},mapKeys:function(e,t){var n=this;return Gt(this,this.toSeq().flip().map(function(r,o){return e.call(t,r,o,n)}).flip())}});var In=r.prototype;function Mn(e,t){return t}function Nn(e,t){return[t,e]}function Rn(e){return function(){return!e.apply(this,arguments)}}function Dn(e){return function(){return-e.apply(this,arguments)}}function Ln(e){return"string"==typeof e?JSON.stringify(e):String(e)}function Un(){return S(arguments)}function qn(e,t){return et?-1:0}function Fn(e,t){return e^t+2654435769+(e<<6)+(e>>2)|0}return In[f]=!0,In[L]=Pn.entries,In.__toJS=Pn.toObject,In.__toStringMapper=function(e,t){return JSON.stringify(t)+": "+Ln(e)},jn(o,{toKeyedSeq:function(){return new Nt(this,!1)},filter:function(e,t){return Gt(this,Bt(this,e,t,!1))},findIndex:function(e,t){var n=this.findEntry(e,t);return n?n[0]:-1},indexOf:function(e){var t=this.keyOf(e);return void 0===t?-1:t},lastIndexOf:function(e){var t=this.lastKeyOf(e);return void 0===t?-1:t},reverse:function(){return Gt(this,Ft(this,!1))},slice:function(e,t){return Gt(this,zt(this,e,t,!1))},splice:function(e,t){var n=arguments.length;if(t=Math.max(0|t,0),0===n||2===n&&!t)return this;e=T(e,e<0?this.count():this.size);var r=this.slice(0,e);return Gt(this,1===n?r:r.concat(S(arguments,2),this.slice(e+t)))},findLastIndex:function(e,t){var n=this.findLastEntry(e,t);return n?n[0]:-1},first:function(){return this.get(0)},flatten:function(e){return Gt(this,Wt(this,e,!1))},get:function(e,t){return(e=k(this,e))<0||this.size===1/0||void 0!==this.size&&e>this.size?t:this.find(function(t,n){return n===e},void 0,t)},has:function(e){return(e=k(this,e))>=0&&(void 0!==this.size?this.size===1/0||e5e3)return e.textContent;return function(e){for(var n,r,o,i,a,s=e.textContent,u=0,c=s[0],l=1,p=e.innerHTML="",f=0;r=n,n=f<7&&"\\"==n?1:l;){if(l=c,c=s[++u],i=p.length>1,!l||f>8&&"\n"==l||[/\S/.test(l),1,1,!/[$\w]/.test(l),("/"==n||"\n"==n)&&i,'"'==n&&i,"'"==n&&i,s[u-4]+r+n=="--\x3e",r+n=="*/"][f])for(p&&(e.appendChild(a=t.createElement("span")).setAttribute("style",["color: #555; font-weight: bold;","","","color: #555;",""][f?f<3?2:f>6?4:f>3?3:+/^(a(bstract|lias|nd|rguments|rray|s(m|sert)?|uto)|b(ase|egin|ool(ean)?|reak|yte)|c(ase|atch|har|hecked|lass|lone|ompl|onst|ontinue)|de(bugger|cimal|clare|f(ault|er)?|init|l(egate|ete)?)|do|double|e(cho|ls?if|lse(if)?|nd|nsure|num|vent|x(cept|ec|p(licit|ort)|te(nds|nsion|rn)))|f(allthrough|alse|inal(ly)?|ixed|loat|or(each)?|riend|rom|unc(tion)?)|global|goto|guard|i(f|mp(lements|licit|ort)|n(it|clude(_once)?|line|out|stanceof|t(erface|ernal)?)?|s)|l(ambda|et|ock|ong)|m(icrolight|odule|utable)|NaN|n(amespace|ative|ext|ew|il|ot|ull)|o(bject|perator|r|ut|verride)|p(ackage|arams|rivate|rotected|rotocol|ublic)|r(aise|e(adonly|do|f|gister|peat|quire(_once)?|scue|strict|try|turn))|s(byte|ealed|elf|hort|igned|izeof|tatic|tring|truct|ubscript|uper|ynchronized|witch)|t(emplate|hen|his|hrows?|ransient|rue|ry|ype(alias|def|id|name|of))|u(n(checked|def(ined)?|ion|less|signed|til)|se|sing)|v(ar|irtual|oid|olatile)|w(char_t|hen|here|hile|ith)|xor|yield)$/.test(p):0]),a.appendChild(t.createTextNode(p))),o=f&&f<7?f:o,p="",f=11;![1,/[\/{}[(\-+*=<>:;|\\.,?!&@~]/.test(l),/[\])]/.test(l),/[$\w]/.test(l),"/"==l&&o<2&&"<"!=n,'"'==l,"'"==l,l+c+s[u+1]+s[u+2]=="\x3c!--",l+c=="/*",l+c=="//","#"==l][--f];);p+=l}}(e)}function Q(e){var t;if([/filename\*=[^']+'\w*'"([^"]+)";?/i,/filename\*=[^']+'\w*'([^;]+);?/i,/filename="([^;]*);?"/i,/filename=([^;]*);?/i].some(function(n){return null!==(t=n.exec(e))}),null!==t&&t.length>1)try{return decodeURIComponent(t[1])}catch(e){console.error(e)}return null}function ee(e){return t=e.replace(/\.[^.\/]*$/,""),b()(g()(t));var t}var te=function(e,t){if(e>t)return"Value must be less than Maximum"},ne=function(e,t){if(et)return"Value must be less than MaxLength"},pe=function(e,t){if(e.length2&&void 0!==arguments[2]?arguments[2]:{},r=n.isOAS3,o=void 0!==r&&r,i=n.bypassRequiredCheck,a=void 0!==i&&i,s=[],u=e.get("required"),c=Object(P.a)(e,{isOAS3:o}),p=c.schema,h=c.parameterContentMediaType;if(!p)return s;var m=p.get("required"),v=p.get("maximum"),g=p.get("minimum"),y=p.get("type"),b=p.get("format"),_=p.get("maxLength"),w=p.get("minLength"),x=p.get("pattern");if(y&&(u||m||t)){var E="string"===y&&t,S="array"===y&&l()(t)&&t.length,C="array"===y&&d.a.List.isList(t)&&t.count(),k="array"===y&&"string"==typeof t&&t,O="file"===y&&t instanceof A.a.File,T="boolean"===y&&(t||!1===t),j="number"===y&&(t||0===t),I="integer"===y&&(t||0===t),M="object"===y&&"object"===f()(t)&&null!==t,N="object"===y&&"string"==typeof t&&t,R=[E,S,C,k,O,T,j,I,M,N],D=R.some(function(e){return!!e});if((u||m)&&!D&&!a)return s.push("Required field is not provided"),s;if("object"===y&&"string"==typeof t&&(null===h||"application/json"===h))try{JSON.parse(t)}catch(e){return s.push("Parameter string value must be valid JSON"),s}if(x){var L=fe(t,x);L&&s.push(L)}if(_||0===_){var U=le(t,_);U&&s.push(U)}if(w){var q=pe(t,w);q&&s.push(q)}if(v||0===v){var F=te(t,v);F&&s.push(F)}if(g||0===g){var B=ne(t,g);B&&s.push(B)}if("string"===y){var z;if(!(z="date-time"===b?ue(t):"uuid"===b?ce(t):se(t)))return s;s.push(z)}else if("boolean"===y){var V=ae(t);if(!V)return s;s.push(V)}else if("number"===y){var H=re(t);if(!H)return s;s.push(H)}else if("integer"===y){var W=oe(t);if(!W)return s;s.push(W)}else if("array"===y){var J;if(!C||!t.count())return s;J=p.getIn(["items","type"]),t.forEach(function(e,t){var n;"number"===J?n=re(e):"integer"===J?n=oe(e):"string"===J&&(n=se(e)),n&&s.push({index:t,error:n})})}else if("file"===y){var K=ie(t);if(!K)return s;s.push(K)}}return s},de=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(/xml/.test(t)){if(!e.xml||!e.xml.name){if(e.xml=e.xml||{},!e.$$ref)return e.type||e.items||e.properties||e.additionalProperties?'\n\x3c!-- XML example cannot be generated; root element name is undefined --\x3e':null;var r=e.$$ref.match(/\S*\/(\S+)$/);e.xml.name=r[1]}return Object(k.memoizedCreateXMLExample)(e,n)}var i=Object(k.memoizedSampleFromSchema)(e,n);return"object"===f()(i)?o()(i,null,2):i},me=function(){var e={},t=A.a.location.search;if(!t)return{};if(""!=t){var n=t.substr(1).split("&");for(var r in n)n.hasOwnProperty(r)&&(r=n[r].split("="),e[decodeURIComponent(r[0])]=r[1]&&decodeURIComponent(r[1])||"")}return e},ve=function(t){return(t instanceof e?t:new e(t.toString(),"utf-8")).toString("base64")},ge={operationsSorter:{alpha:function(e,t){return e.get("path").localeCompare(t.get("path"))},method:function(e,t){return e.get("method").localeCompare(t.get("method"))}},tagsSorter:{alpha:function(e,t){return e.localeCompare(t)}}},ye=function(e){var t=[];for(var n in e){var r=e[n];void 0!==r&&""!==r&&t.push([n,"=",encodeURIComponent(r).replace(/%20/g,"+")].join(""))}return t.join("&")},be=function(e,t,n){return!!E()(n,function(n){return C()(e[n],t[n])})};function _e(e){return"string"!=typeof e||""===e?"":Object(m.sanitizeUrl)(e)}function we(e){if(!d.a.OrderedMap.isOrderedMap(e))return null;if(!e.size)return null;var t=e.find(function(e,t){return t.startsWith("2")&&u()(e.get("content")||{}).length>0}),n=e.get("default")||d.a.OrderedMap(),r=(n.get("content")||d.a.OrderedMap()).keySeq().toJS().length?n:null;return t||r}var xe=function(e){return"string"==typeof e||e instanceof String?e.trim().replace(/\s/g,"%20"):""},Ee=function(e){return j()(xe(e).replace(/%20/g,"_"))},Se=function(e){return e.filter(function(e,t){return/^x-/.test(t)})},Ce=function(e){return e.filter(function(e,t){return/^pattern|maxLength|minLength|maximum|minimum/.test(t)})};function ke(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){return!0};if("object"!==f()(e)||l()(e)||null===e||!t)return e;var r=a()({},e);return u()(r).forEach(function(e){e===t&&n(r[e],e)?delete r[e]:r[e]=ke(r[e],t,n)}),r}function Oe(e){if("string"==typeof e)return e;if(e&&e.toJS&&(e=e.toJS()),"object"===f()(e)&&null!==e)try{return o()(e,null,2)}catch(t){return String(e)}return null==e?"":e.toString()}function Ae(e){return"number"==typeof e?e.toString():e}function Te(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.returnAll,r=void 0!==n&&n,o=t.allowHashes,i=void 0===o||o;if(!d.a.Map.isMap(e))throw new Error("paramToIdentifier: received a non-Im.Map parameter as input");var a=e.get("name"),s=e.get("in"),u=[];return e&&e.hashCode&&s&&a&&i&&u.push("".concat(s,".").concat(a,".hash-").concat(e.hashCode())),s&&a&&u.push("".concat(s,".").concat(a)),u.push(a),r?u:u[0]||""}function je(e,t){return Te(e,{returnAll:!0}).map(function(e){return t[e]}).filter(function(e){return void 0!==e})[0]}function Pe(){return Me(M()(32).toString("base64"))}function Ie(e){return Me(R()("sha256").update(e).digest("base64"))}function Me(e){return e.replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}}).call(this,n(64).Buffer)},function(e,t){e.exports=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}},function(e,t,n){var r=n(54);function o(e,t){for(var n=0;n1?t-1:0),o=1;o2?n-2:0),i=2;i>",i={listOf:function(e){return c(e,"List",r.List.isList)},mapOf:function(e,t){return l(e,t,"Map",r.Map.isMap)},orderedMapOf:function(e,t){return l(e,t,"OrderedMap",r.OrderedMap.isOrderedMap)},setOf:function(e){return c(e,"Set",r.Set.isSet)},orderedSetOf:function(e){return c(e,"OrderedSet",r.OrderedSet.isOrderedSet)},stackOf:function(e){return c(e,"Stack",r.Stack.isStack)},iterableOf:function(e){return c(e,"Iterable",r.Iterable.isIterable)},recordOf:function(e){return s(function(t,n,o,i,s){for(var u=arguments.length,c=Array(u>5?u-5:0),l=5;l6?u-6:0),l=6;l5?c-5:0),p=5;p5?i-5:0),s=5;s key("+l[p]+")"].concat(a));if(h instanceof Error)return h}})).apply(void 0,i);var u})}function p(e){var t=void 0===arguments[1]?"Iterable":arguments[1],n=void 0===arguments[2]?r.Iterable.isIterable:arguments[2];return s(function(r,o,i,s,u){for(var c=arguments.length,l=Array(c>5?c-5:0),p=5;p4)}function u(e){var t=e.get("swagger");return"string"==typeof t&&t.startsWith("2.0")}function c(e){return function(t,n){return function(r){return n&&n.specSelectors&&n.specSelectors.specJson?s(n.specSelectors.specJson())?a.a.createElement(e,o()({},r,n,{Ori:t})):a.a.createElement(t,r):(console.warn("OAS3 wrapper: couldn't get spec"),null)}}}},function(e,t,n){"use strict"; +/* +object-assign +(c) Sindre Sorhus +@license MIT +*/var r=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable;function a(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map(function(e){return t[e]}).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach(function(e){r[e]=e}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,t){for(var n,s,u=a(e),c=1;c0){var o=n.map(function(e){return console.error(e),e.line=e.fullPath?g(y,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",A()(e,"message",{enumerable:!0,value:e.message}),e});i.newThrownErrBatch(o)}return r.updateResolved(t)})}},_e=[],we=V()(k()(S.a.mark(function e(){var t,n,r,o,i,a,s,u,c,l,p,f,h,d,m,v,g;return S.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:if(t=_e.system){e.next=4;break}return console.error("debResolveSubtrees: don't have a system to operate on, aborting."),e.abrupt("return");case 4:if(n=t.errActions,r=t.errSelectors,o=t.fn,i=o.resolveSubtree,a=o.AST,s=void 0===a?{}:a,u=t.specSelectors,c=t.specActions,i){e.next=8;break}return console.error("Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing."),e.abrupt("return");case 8:return l=s.getLineNumberForPath?s.getLineNumberForPath:function(){},p=u.specStr(),f=t.getConfigs(),h=f.modelPropertyMacro,d=f.parameterMacro,m=f.requestInterceptor,v=f.responseInterceptor,e.prev=11,e.next=14,_e.reduce(function(){var e=k()(S.a.mark(function e(t,o){var a,s,c,f,g,y,b;return S.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,t;case 2:return a=e.sent,s=a.resultMap,c=a.specWithCurrentSubtrees,e.next=7,i(c,o,{baseDoc:u.url(),modelPropertyMacro:h,parameterMacro:d,requestInterceptor:m,responseInterceptor:v});case 7:return f=e.sent,g=f.errors,y=f.spec,r.allErrors().size&&n.clearBy(function(e){return"thrown"!==e.get("type")||"resolver"!==e.get("source")||!e.get("fullPath").every(function(e,t){return e===o[t]||void 0===o[t]})}),j()(g)&&g.length>0&&(b=g.map(function(e){return e.line=e.fullPath?l(p,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",A()(e,"message",{enumerable:!0,value:e.message}),e}),n.newThrownErrBatch(b)),W()(s,o,y),W()(c,o,y),e.abrupt("return",{resultMap:s,specWithCurrentSubtrees:c});case 15:case"end":return e.stop()}},e)}));return function(t,n){return e.apply(this,arguments)}}(),x.a.resolve({resultMap:(u.specResolvedSubtree([])||Object(R.Map)()).toJS(),specWithCurrentSubtrees:u.specJson().toJS()}));case 14:g=e.sent,delete _e.system,_e=[],e.next=22;break;case 19:e.prev=19,e.t0=e.catch(11),console.error(e.t0);case 22:c.updateResolvedSubtree([],g.resultMap);case 23:case"end":return e.stop()}},e,null,[[11,19]])})),35),xe=function(e){return function(t){_e.map(function(e){return e.join("@@")}).indexOf(e.join("@@"))>-1||(_e.push(e),_e.system=t,we())}};function Ee(e,t,n,r,o){return{type:X,payload:{path:e,value:r,paramName:t,paramIn:n,isXml:o}}}function Se(e,t,n,r){return{type:X,payload:{path:e,param:t,value:n,isXml:r}}}var Ce=function(e,t){return{type:le,payload:{path:e,value:t}}},ke=function(){return{type:le,payload:{path:[],value:Object(R.Map)()}}},Oe=function(e,t){return{type:ee,payload:{pathMethod:e,isOAS3:t}}},Ae=function(e,t,n,r){return{type:Q,payload:{pathMethod:e,paramName:t,paramIn:n,includeEmptyValue:r}}};function Te(e){return{type:se,payload:{pathMethod:e}}}function je(e,t){return{type:ue,payload:{path:e,value:t,key:"consumes_value"}}}function Pe(e,t){return{type:ue,payload:{path:e,value:t,key:"produces_value"}}}var Ie=function(e,t,n){return{payload:{path:e,method:t,res:n},type:te}},Me=function(e,t,n){return{payload:{path:e,method:t,req:n},type:ne}},Ne=function(e,t,n){return{payload:{path:e,method:t,req:n},type:re}},Re=function(e){return{payload:e,type:oe}},De=function(e){return function(t){var n=t.fn,r=t.specActions,o=t.specSelectors,i=t.getConfigs,a=t.oas3Selectors,s=e.pathName,u=e.method,c=e.operation,l=i(),p=l.requestInterceptor,f=l.responseInterceptor,h=c.toJS();if(c&&c.get("parameters")&&c.get("parameters").filter(function(e){return e&&!0===e.get("allowEmptyValue")}).forEach(function(t){if(o.parameterInclusionSettingFor([s,u],t.get("name"),t.get("in"))){e.parameters=e.parameters||{};var n=Object(J.C)(t,e.parameters);(!n||n&&0===n.size)&&(e.parameters[t.get("name")]="")}}),e.contextUrl=L()(o.url()).toString(),h&&h.operationId?e.operationId=h.operationId:h&&s&&u&&(e.operationId=n.opId(h,s,u)),o.isOAS3()){var d="".concat(s,":").concat(u);e.server=a.selectedServer(d)||a.selectedServer();var m=a.serverVariables({server:e.server,namespace:d}).toJS(),g=a.serverVariables({server:e.server}).toJS();e.serverVariables=_()(m).length?m:g,e.requestContentType=a.requestContentType(s,u),e.responseContentType=a.responseContentType(s,u)||"*/*";var b=a.requestBodyValue(s,u);Object(J.t)(b)?e.requestBody=JSON.parse(b):b&&b.toJS?e.requestBody=b.toJS():e.requestBody=b}var w=y()({},e);w=n.buildRequest(w),r.setRequest(e.pathName,e.method,w);e.requestInterceptor=function(t){var n=p.apply(this,[t]),o=y()({},n);return r.setMutatedRequest(e.pathName,e.method,o),n},e.responseInterceptor=f;var x=v()();return n.execute(e).then(function(t){t.duration=v()()-x,r.setResponse(e.pathName,e.method,t)}).catch(function(t){console.error(t),r.setResponse(e.pathName,e.method,{error:!0,err:q()(t)})})}},Le=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.path,n=e.method,r=d()(e,["path","method"]);return function(e){var o=e.fn.fetch,i=e.specSelectors,a=e.specActions,s=i.specJsonWithResolvedSubtrees().toJS(),u=i.operationScheme(t,n),c=i.contentTypeValues([t,n]).toJS(),l=c.requestContentType,p=c.responseContentType,f=/xml/i.test(l),h=i.parameterValues([t,n],f).toJS();return a.executeRequest(Y({},r,{fetch:o,spec:s,pathName:t,method:n,parameters:h,requestContentType:l,scheme:u,responseContentType:p}))}};function Ue(e,t){return{type:ie,payload:{path:e,method:t}}}function qe(e,t){return{type:ae,payload:{path:e,method:t}}}function Fe(e,t,n){return{type:pe,payload:{scheme:e,path:t,method:n}}}},function(e,t,n){var r=n(32),o=n(22),i=n(63),a=n(77),s=n(75),u=function(e,t,n){var c,l,p,f=e&u.F,h=e&u.G,d=e&u.S,m=e&u.P,v=e&u.B,g=e&u.W,y=h?o:o[t]||(o[t]={}),b=y.prototype,_=h?r:d?r[t]:(r[t]||{}).prototype;for(c in h&&(n=t),n)(l=!f&&_&&void 0!==_[c])&&s(y,c)||(p=l?_[c]:n[c],y[c]=h&&"function"!=typeof _[c]?n[c]:v&&l?i(p,r):g&&_[c]==p?function(e){var t=function(t,n,r){if(this instanceof e){switch(arguments.length){case 0:return new e;case 1:return new e(t);case 2:return new e(t,n)}return new e(t,n,r)}return e.apply(this,arguments)};return t.prototype=e.prototype,t}(p):m&&"function"==typeof p?i(Function.call,p):p,m&&((y.virtual||(y.virtual={}))[c]=p,e&u.R&&b&&!b[c]&&a(b,c,p)))};u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,e.exports=u},function(e,t,n){"use strict";var r=n(138),o=["kind","resolve","construct","instanceOf","predicate","represent","defaultStyle","styleAliases"],i=["scalar","sequence","mapping"];e.exports=function(e,t){var n,a;if(t=t||{},Object.keys(t).forEach(function(t){if(-1===o.indexOf(t))throw new r('Unknown option "'+t+'" is met in definition of "'+e+'" YAML type.')}),this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(e){return e},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.defaultStyle=t.defaultStyle||null,this.styleAliases=(n=t.styleAliases||null,a={},null!==n&&Object.keys(n).forEach(function(e){n[e].forEach(function(t){a[String(t)]=e})}),a),-1===i.indexOf(this.kind))throw new r('Unknown kind "'+this.kind+'" is specified for "'+e+'" YAML type.')}},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t,n){var r=n(197)("wks"),o=n(199),i=n(41).Symbol,a="function"==typeof i;(e.exports=function(e){return r[e]||(r[e]=a&&i[e]||(a?i:o)("Symbol."+e))}).store=r},function(e,t,n){var r=n(214)("wks"),o=n(159),i=n(32).Symbol,a="function"==typeof i;(e.exports=function(e){return r[e]||(r[e]=a&&i[e]||(a?i:o)("Symbol."+e))}).store=r},function(e,t,n){var r=n(41),o=n(72),i=n(81),a=n(97),s=n(153),u=function(e,t,n){var c,l,p,f,h=e&u.F,d=e&u.G,m=e&u.S,v=e&u.P,g=e&u.B,y=d?r:m?r[t]||(r[t]={}):(r[t]||{}).prototype,b=d?o:o[t]||(o[t]={}),_=b.prototype||(b.prototype={});for(c in d&&(n=t),n)p=((l=!h&&y&&void 0!==y[c])?y:n)[c],f=g&&l?s(p,r):v&&"function"==typeof p?s(Function.call,p):p,y&&a(y,c,p,e&u.U),b[c]!=p&&i(b,c,f),v&&_[c]!=p&&(_[c]=p)};r.core=o,u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,e.exports=u},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t){var n=Array.isArray;e.exports=n},function(e,t,n){"use strict";var r=!("undefined"==typeof window||!window.document||!window.document.createElement),o={canUseDOM:r,canUseWorkers:"undefined"!=typeof Worker,canUseEventListeners:r&&!(!window.addEventListener&&!window.attachEvent),canUseViewport:r&&!!window.screen,isInWorker:!r};e.exports=o},function(e,t,n){"use strict";var r=Object.prototype.hasOwnProperty;function o(e,t){return!!e&&r.call(e,t)}var i=/\\([\\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g;function a(e){return!(e>=55296&&e<=57343)&&(!(e>=64976&&e<=65007)&&(65535!=(65535&e)&&65534!=(65535&e)&&(!(e>=0&&e<=8)&&(11!==e&&(!(e>=14&&e<=31)&&(!(e>=127&&e<=159)&&!(e>1114111)))))))}function s(e){if(e>65535){var t=55296+((e-=65536)>>10),n=56320+(1023&e);return String.fromCharCode(t,n)}return String.fromCharCode(e)}var u=/&([a-z#][a-z0-9]{1,31});/gi,c=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))/i,l=n(463);function p(e,t){var n=0;return o(l,t)?l[t]:35===t.charCodeAt(0)&&c.test(t)&&a(n="x"===t[1].toLowerCase()?parseInt(t.slice(2),16):parseInt(t.slice(1),10))?s(n):e}var f=/[&<>"]/,h=/[&<>"]/g,d={"&":"&","<":"<",">":">",'"':"""};function m(e){return d[e]}t.assign=function(e){return[].slice.call(arguments,1).forEach(function(t){if(t){if("object"!=typeof t)throw new TypeError(t+"must be object");Object.keys(t).forEach(function(n){e[n]=t[n]})}}),e},t.isString=function(e){return"[object String]"===function(e){return Object.prototype.toString.call(e)}(e)},t.has=o,t.unescapeMd=function(e){return e.indexOf("\\")<0?e:e.replace(i,"$1")},t.isValidEntityCode=a,t.fromCodePoint=s,t.replaceEntities=function(e){return e.indexOf("&")<0?e:e.replace(u,p)},t.escapeHtml=function(e){return f.test(e)?e.replace(h,m):e}},function(e,t,n){var r=n(55),o=n(771);e.exports=function(e,t){if(null==e)return{};var n,i,a=o(e,t);if(r){var s=r(e);for(i=0;i=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t,n){var r=n(35),o=n(99),i=n(73),a=/"/g,s=function(e,t,n,r){var o=String(i(e)),s="<"+t;return""!==n&&(s+=" "+n+'="'+String(r).replace(a,""")+'"'),s+">"+o+""};e.exports=function(e,t){var n={};n[e]=t(s),r(r.P+r.F*o(function(){var t=""[e]('"');return t!==t.toLowerCase()||t.split('"').length>3}),"String",n)}},function(e,t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,t,n){"use strict";n.r(t),n.d(t,"NEW_THROWN_ERR",function(){return i}),n.d(t,"NEW_THROWN_ERR_BATCH",function(){return a}),n.d(t,"NEW_SPEC_ERR",function(){return s}),n.d(t,"NEW_SPEC_ERR_BATCH",function(){return u}),n.d(t,"NEW_AUTH_ERR",function(){return c}),n.d(t,"CLEAR",function(){return l}),n.d(t,"CLEAR_BY",function(){return p}),n.d(t,"newThrownErr",function(){return f}),n.d(t,"newThrownErrBatch",function(){return h}),n.d(t,"newSpecErr",function(){return d}),n.d(t,"newSpecErrBatch",function(){return m}),n.d(t,"newAuthErr",function(){return v}),n.d(t,"clear",function(){return g}),n.d(t,"clearBy",function(){return y});var r=n(119),o=n.n(r),i="err_new_thrown_err",a="err_new_thrown_err_batch",s="err_new_spec_err",u="err_new_spec_err_batch",c="err_new_auth_err",l="err_clear",p="err_clear_by";function f(e){return{type:i,payload:o()(e)}}function h(e){return{type:a,payload:e}}function d(e){return{type:s,payload:e}}function m(e){return{type:u,payload:e}}function v(e){return{type:c,payload:e}}function g(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return{type:l,payload:e}}function y(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!0};return{type:p,payload:e}}},function(e,t,n){var r=n(98);e.exports=function(e){if(!r(e))throw TypeError(e+" is not an object!");return e}},function(e,t,n){var r=n(43);e.exports=function(e){if(!r(e))throw TypeError(e+" is not an object!");return e}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t,n){var r=n(64),o=r.Buffer;function i(e,t){for(var n in e)t[n]=e[n]}function a(e,t,n){return o(e,t,n)}o.from&&o.alloc&&o.allocUnsafe&&o.allocUnsafeSlow?e.exports=r:(i(r,t),t.Buffer=a),i(o,a),a.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return o(e,t,n)},a.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=o(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},a.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return o(e)},a.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t,n){var r=n(46),o=n(349),i=n(218),a=Object.defineProperty;t.f=n(50)?Object.defineProperty:function(e,t,n){if(r(e),t=i(t,!0),r(n),o)try{return a(e,t,n)}catch(e){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(e[t]=n.value),e}},function(e,t,n){e.exports=!n(82)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(e,t,n){var r=n(366),o="object"==typeof self&&self&&self.Object===Object&&self,i=r||o||Function("return this")();e.exports=i},function(e,t){e.exports=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}},function(e,t,n){"use strict";e.exports={debugTool:null}},function(e,t,n){e.exports=n(573)},function(e,t,n){e.exports=n(770)},function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=45)}([function(e,t){e.exports=n(17)},function(e,t){e.exports=n(14)},function(e,t){e.exports=n(26)},function(e,t){e.exports=n(16)},function(e,t){e.exports=n(123)},function(e,t){e.exports=n(60)},function(e,t){e.exports=n(61)},function(e,t){e.exports=n(55)},function(e,t){e.exports=n(2)},function(e,t){e.exports=n(54)},function(e,t){e.exports=n(94)},function(e,t){e.exports=n(28)},function(e,t){e.exports=n(930)},function(e,t){e.exports=n(12)},function(e,t){e.exports=n(192)},function(e,t){e.exports=n(936)},function(e,t){e.exports=n(93)},function(e,t){e.exports=n(193)},function(e,t){e.exports=n(939)},function(e,t){e.exports=n(943)},function(e,t){e.exports=n(944)},function(e,t){e.exports=n(92)},function(e,t){e.exports=n(13)},function(e,t){e.exports=n(146)},function(e,t){e.exports=n(4)},function(e,t){e.exports=n(5)},function(e,t){e.exports=n(946)},function(e,t){e.exports=n(421)},function(e,t){e.exports=n(949)},function(e,t){e.exports=n(52)},function(e,t){e.exports=n(64)},function(e,t){e.exports=n(283)},function(e,t){e.exports=n(272)},function(e,t){e.exports=n(950)},function(e,t){e.exports=n(145)},function(e,t){e.exports=n(951)},function(e,t){e.exports=n(959)},function(e,t){e.exports=n(960)},function(e,t){e.exports=n(961)},function(e,t){e.exports=n(40)},function(e,t){e.exports=n(264)},function(e,t){e.exports=n(37)},function(e,t){e.exports=n(964)},function(e,t){e.exports=n(965)},function(e,t){e.exports=n(966)},function(e,t,n){e.exports=n(50)},function(e,t){e.exports=n(967)},function(e,t){e.exports=n(968)},function(e,t){e.exports=n(969)},function(e,t){e.exports=n(970)},function(e,t,n){"use strict";n.r(t);var r={};n.r(r),n.d(r,"path",function(){return mn}),n.d(r,"query",function(){return vn}),n.d(r,"header",function(){return yn}),n.d(r,"cookie",function(){return bn});var o=n(9),i=n.n(o),a=n(10),s=n.n(a),u=n(5),c=n.n(u),l=n(6),p=n.n(l),f=n(7),h=n.n(f),d=n(0),m=n.n(d),v=n(8),g=n.n(v),y=(n(46),n(15)),b=n.n(y),_=n(20),w=n.n(_),x=n(12),E=n.n(x),S=n(4),C=n.n(S),k=n(22),O=n.n(k),A=n(11),T=n.n(A),j=n(2),P=n.n(j),I=n(1),M=n.n(I),N=n(17),R=n.n(N),D=(n(47),n(26)),L=n.n(D),U=n(23),q=n.n(U),F=n(31),B=n.n(F),z={serializeRes:J,mergeInQueryOrForm:Z};function V(e){return H.apply(this,arguments)}function H(){return(H=R()(C.a.mark(function e(t){var n,r,o,i,a,s=arguments;return C.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:if(n=s.length>1&&void 0!==s[1]?s[1]:{},"object"===P()(t)&&(t=(n=t).url),n.headers=n.headers||{},z.mergeInQueryOrForm(n),n.headers&&m()(n.headers).forEach(function(e){var t=n.headers[e];"string"==typeof t&&(n.headers[e]=t.replace(/\n+/g," "))}),!n.requestInterceptor){e.next=12;break}return e.next=8,n.requestInterceptor(n);case 8:if(e.t0=e.sent,e.t0){e.next=11;break}e.t0=n;case 11:n=e.t0;case 12:return r=n.headers["content-type"]||n.headers["Content-Type"],/multipart\/form-data/i.test(r)&&(delete n.headers["content-type"],delete n.headers["Content-Type"]),e.prev=14,e.next=17,(n.userFetch||fetch)(n.url,n);case 17:return o=e.sent,e.next=20,z.serializeRes(o,t,n);case 20:if(o=e.sent,!n.responseInterceptor){e.next=28;break}return e.next=24,n.responseInterceptor(o);case 24:if(e.t1=e.sent,e.t1){e.next=27;break}e.t1=o;case 27:o=e.t1;case 28:e.next=38;break;case 30:if(e.prev=30,e.t2=e.catch(14),o){e.next=34;break}throw e.t2;case 34:throw(i=new Error(o.statusText)).statusCode=i.status=o.status,i.responseError=e.t2,i;case 38:if(o.ok){e.next=43;break}throw(a=new Error(o.statusText)).statusCode=a.status=o.status,a.response=o,a;case 43:return e.abrupt("return",o);case 44:case"end":return e.stop()}},e,null,[[14,30]])}))).apply(this,arguments)}var W=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";return/(json|xml|yaml|text)\b/.test(e)};function J(e,t){var n=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).loadSpec,r=void 0!==n&&n,o={ok:e.ok,url:e.url||t,status:e.status,statusText:e.statusText,headers:K(e.headers)},i=o.headers["content-type"],a=r||W(i);return(a?e.text:e.blob||e.buffer).call(e).then(function(e){if(o.text=e,o.data=e,a)try{var t=function(e,t){return t&&(0===t.indexOf("application/json")||t.indexOf("+json")>0)?JSON.parse(e):q.a.safeLoad(e)}(e,i);o.body=t,o.obj=t}catch(e){o.parseError=e}return o})}function K(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t={};return"function"==typeof e.forEach?(e.forEach(function(e,n){void 0!==t[n]?(t[n]=M()(t[n])?t[n]:[t[n]],t[n].push(e)):t[n]=e}),t):t}function Y(e,t){return t||"undefined"==typeof navigator||(t=navigator),t&&"ReactNative"===t.product?!(!e||"object"!==P()(e)||"string"!=typeof e.uri):"undefined"!=typeof File?e instanceof File:null!==e&&"object"===P()(e)&&"function"==typeof e.pipe}function $(e,t){var n=e.collectionFormat,r=e.allowEmptyValue,o="object"===P()(e)?e.value:e;if(void 0===o&&r)return"";if(Y(o)||"boolean"==typeof o)return o;var i=encodeURIComponent;return t&&(i=B()(o)?function(e){return e}:function(e){return T()(e)}),"object"!==P()(o)||M()(o)?M()(o)?M()(o)&&!n?o.map(i).join(","):"multi"===n?o.map(i):o.map(i).join({csv:",",ssv:"%20",tsv:"%09",pipes:"|"}[n]):i(o):""}function G(e){var t=m()(e).reduce(function(t,n){var r,o=e[n],i=!!o.skipEncoding,a=i?n:encodeURIComponent(n),s=(r=o)&&"object"===P()(r)&&!M()(o);return t[a]=$(s?o:{value:o},i),t},{});return L.a.stringify(t,{encode:!1,indices:!1})||""}function Z(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.url,r=void 0===t?"":t,o=e.query,i=e.form;if(i){var a=m()(i).some(function(e){return Y(i[e].value)}),s=e.headers["content-type"]||e.headers["Content-Type"];if(a||/multipart\/form-data/i.test(s)){var u=n(48);e.body=new u,m()(i).forEach(function(t){e.body.append(t,$(i[t],!0))})}else e.body=G(i);delete e.form}if(o){var c=r.split("?"),l=O()(c,2),p=l[0],f=l[1],h="";if(f){var d=L.a.parse(f);m()(o).forEach(function(e){return delete d[e]}),h=L.a.stringify(d,{encode:!0})}var v=function(){for(var e=arguments.length,t=new Array(e),n=0;n0){var o=t(e,n[n.length-1],n);o&&(r=r.concat(o))}if(M()(e)){var i=e.map(function(e,r){return Ce(e,t,n.concat(r))});i&&(r=r.concat(i))}else if(Te(e)){var a=m()(e).map(function(r){return Ce(e[r],t,n.concat(r))});a&&(r=r.concat(a))}return r=Oe(r)}function ke(e){return M()(e)?e:[e]}function Oe(e){var t;return(t=[]).concat.apply(t,he()(e.map(function(e){return M()(e)?Oe(e):e})))}function Ae(e){return e.filter(function(e){return void 0!==e})}function Te(e){return e&&"object"===P()(e)}function je(e){return e&&"function"==typeof e}function Pe(e){if(Ne(e)){var t=e.op;return"add"===t||"remove"===t||"replace"===t}return!1}function Ie(e){return Pe(e)||Ne(e)&&"mutation"===e.type}function Me(e){return Ie(e)&&("add"===e.op||"replace"===e.op||"merge"===e.op||"mergeDeep"===e.op)}function Ne(e){return e&&"object"===P()(e)}function Re(e,t){try{return me.a.getValueByPointer(e,t)}catch(e){return console.error(e),{}}}var De=n(35),Le=n.n(De),Ue=n(36),qe=n(28),Fe=n.n(qe);function Be(e,t){function n(){Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack;for(var e=arguments.length,n=new Array(e),r=0;r-1&&-1===We.indexOf(n)||Je.indexOf(r)>-1||Ke.some(function(e){return r.indexOf(e)>-1})}function $e(e,t){var n=e.split("#"),r=O()(n,2),o=r[0],i=r[1],a=E.a.resolve(o||"",t||"");return i?"".concat(a,"#").concat(i):a}var Ge="application/json, application/yaml",Ze=new RegExp("^([a-z]+://|//)","i"),Xe=Be("JSONRefError",function(e,t,n){this.originalError=n,ie()(this,t||{})}),Qe={},et=new Le.a,tt=[function(e){return"paths"===e[0]&&"responses"===e[3]&&"content"===e[5]&&"example"===e[7]},function(e){return"paths"===e[0]&&"requestBody"===e[3]&&"content"===e[4]&&"example"===e[6]}],nt={key:"$ref",plugin:function(e,t,n,r){var o=r.getInstance(),i=n.slice(0,-1);if(!Ye(i)&&(a=i,!tt.some(function(e){return e(a)}))){var a,s=r.getContext(n).baseDoc;if("string"!=typeof e)return new Xe("$ref: must be a string (JSON-Ref)",{$ref:e,baseDoc:s,fullPath:n});var u,c,l,p=st(e),f=p[0],h=p[1]||"";try{u=s||f?it(f,s):null}catch(t){return at(t,{pointer:h,$ref:e,basePath:u,fullPath:n})}if(function(e,t,n,r){var o=et.get(r);o||(o={},et.set(r,o));var i=function(e){if(0===e.length)return"";return"/".concat(e.map(ht).join("/"))}(n),a="".concat(t||"","#").concat(e),s=i.replace(/allOf\/\d+\/?/g,""),u=r.contextTree.get([]).baseDoc;if(t==u&&mt(s,e))return!0;var c="";if(n.some(function(e){return c="".concat(c,"/").concat(ht(e)),o[c]&&o[c].some(function(e){return mt(e,a)||mt(a,e)})}))return!0;o[s]=(o[s]||[]).concat(a)}(h,u,i,r)&&!o.useCircularStructures){var d=$e(e,u);return e===d?null:_e.replace(n,d)}if(null==u?(l=pt(h),void 0===(c=r.get(l))&&(c=new Xe("Could not resolve reference: ".concat(e),{pointer:h,$ref:e,baseDoc:s,fullPath:n}))):c=null!=(c=ut(u,h)).__value?c.__value:c.catch(function(t){throw at(t,{pointer:h,$ref:e,baseDoc:s,fullPath:n})}),c instanceof Error)return[_e.remove(n),c];var v=$e(e,u),g=_e.replace(i,c,{$$ref:v});if(u&&u!==s)return[g,_e.context(i,{baseDoc:u})];try{if(!function(e,t){var n=[e];return t.path.reduce(function(e,t){return n.push(e[t]),e[t]},e),function e(t){return _e.isObject(t)&&(n.indexOf(t)>=0||m()(t).some(function(n){return e(t[n])}))}(t.value)}(r.state,g)||o.useCircularStructures)return g}catch(e){return null}}}},rt=ie()(nt,{docCache:Qe,absoluteify:it,clearCache:function(e){void 0!==e?delete Qe[e]:m()(Qe).forEach(function(e){delete Qe[e]})},JSONRefError:Xe,wrapError:at,getDoc:ct,split:st,extractFromDoc:ut,fetchJSON:function(e){return Object(Ue.fetch)(e,{headers:{Accept:Ge},loadSpec:!0}).then(function(e){return e.text()}).then(function(e){return q.a.safeLoad(e)})},extract:lt,jsonPointerToArray:pt,unescapeJsonPointerToken:ft}),ot=rt;function it(e,t){if(!Ze.test(e)){if(!t)throw new Xe("Tried to resolve a relative URL, without having a basePath. path: '".concat(e,"' basePath: '").concat(t,"'"));return E.a.resolve(t,e)}return e}function at(e,t){var n;return n=e&&e.response&&e.response.body?"".concat(e.response.body.code," ").concat(e.response.body.message):e.message,new Xe("Could not resolve reference: ".concat(n),t,e)}function st(e){return(e+"").split("#")}function ut(e,t){var n=Qe[e];if(n&&!_e.isPromise(n))try{var r=lt(t,n);return ie()(Q.a.resolve(r),{__value:r})}catch(e){return Q.a.reject(e)}return ct(e).then(function(e){return lt(t,e)})}function ct(e){var t=Qe[e];return t?_e.isPromise(t)?t:Q.a.resolve(t):(Qe[e]=rt.fetchJSON(e).then(function(t){return Qe[e]=t,t}),Qe[e])}function lt(e,t){var n=pt(e);if(n.length<1)return t;var r=_e.getIn(t,n);if(void 0===r)throw new Xe("Could not resolve pointer: ".concat(e," does not exist in document"),{pointer:e});return r}function pt(e){if("string"!=typeof e)throw new TypeError("Expected a string, got a ".concat(P()(e)));return"/"===e[0]&&(e=e.substr(1)),""===e?[]:e.split("/").map(ft)}function ft(e){return"string"!=typeof e?e:Fe.a.unescape(e.replace(/~1/g,"/").replace(/~0/g,"~"))}function ht(e){return Fe.a.escape(e.replace(/~/g,"~0").replace(/\//g,"~1"))}var dt=function(e){return!e||"/"===e||"#"===e};function mt(e,t){if(dt(t))return!0;var n=e.charAt(t.length),r=t.slice(-1);return 0===e.indexOf(t)&&(!n||"/"===n||"#"===n)&&"#"!==r}var vt={key:"allOf",plugin:function(e,t,n,r,o){if(!o.meta||!o.meta.$$ref){var i=n.slice(0,-1);if(!Ye(i)){if(!M()(e)){var a=new TypeError("allOf must be an array");return a.fullPath=n,a}var s=!1,u=o.value;i.forEach(function(e){u&&(u=u[e])}),delete(u=ie()({},u)).allOf;var c=[];return c.push(r.replace(i,{})),e.forEach(function(e,t){if(!r.isObject(e)){if(s)return null;s=!0;var o=new TypeError("Elements in allOf must be objects");return o.fullPath=n,c.push(o)}c.push(r.mergeDeep(i,e));var a=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.specmap,o=n.getBaseUrlForNodePath,i=void 0===o?function(e){return r.getContext([].concat(he()(t),he()(e))).baseDoc}:o,a=n.targetKeys,s=void 0===a?["$ref","$$ref"]:a,u=[];return Ve()(e).forEach(function(){if(s.indexOf(this.key)>-1){var e=this.path,n=t.concat(this.path),o=$e(this.node,i(e));u.push(r.replace(n,o))}}),u}(e,n.slice(0,-1),{getBaseUrlForNodePath:function(e){return r.getContext([].concat(he()(n),[t],he()(e))).baseDoc},specmap:r});c.push.apply(c,he()(a))}),c.push(r.mergeDeep(i,u)),u.$$ref||c.push(r.remove([].concat(i,"$$ref"))),c}}}},gt={key:"parameters",plugin:function(e,t,n,r,o){if(M()(e)&&e.length){var i=ie()([],e),a=n.slice(0,-1),s=ie()({},_e.getIn(r.spec,a));return e.forEach(function(e,t){try{i[t].default=r.parameterMacro(s,e)}catch(e){var o=new Error(e);return o.fullPath=n,o}}),_e.replace(n,i)}return _e.replace(n,e)}},yt={key:"properties",plugin:function(e,t,n,r){var o=ie()({},e);for(var i in e)try{o[i].default=r.modelPropertyMacro(o[i])}catch(e){var a=new Error(e);return a.fullPath=n,a}return _e.replace(n,o)}};function bt(e,t){var n=m()(e);if(h.a){var r=h()(e);t&&(r=r.filter(function(t){return p()(e,t).enumerable})),n.push.apply(n,r)}return n}var _t=function(){function e(t){se()(this,e),this.root=wt(t||{})}return ce()(e,[{key:"set",value:function(e,t){var n=this.getParent(e,!0);if(n){var r=e[e.length-1],o=n.children;o[r]?xt(o[r],t,n):o[r]=wt(t,n)}else xt(this.root,t,null)}},{key:"get",value:function(e){if((e=e||[]).length<1)return this.root.value;for(var t,n,r=this.root,o=0;o1?n-1:0),o=1;o1?n-1:0),o=1;o0})}},{key:"nextPromisedPatch",value:function(){if(this.promisedPatches.length>0)return Q.a.race(this.promisedPatches.map(function(e){return e.value}))}},{key:"getPluginHistory",value:function(e){var t=this.getPluginName(e);return this.pluginHistory[t]||[]}},{key:"getPluginRunCount",value:function(e){return this.getPluginHistory(e).length}},{key:"getPluginHistoryTip",value:function(e){var t=this.getPluginHistory(e);return t&&t[t.length-1]||{}}},{key:"getPluginMutationIndex",value:function(e){var t=this.getPluginHistoryTip(e).mutationIndex;return"number"!=typeof t?-1:t}},{key:"getPluginName",value:function(e){return e.pluginName}},{key:"updatePluginHistory",value:function(e,t){var n=this.getPluginName(e);(this.pluginHistory[n]=this.pluginHistory[n]||[]).push(t)}},{key:"updatePatches",value:function(e,t){var n=this;_e.normalizeArray(e).forEach(function(e){if(e instanceof Error)n.errors.push(e);else try{if(!_e.isObject(e))return void n.debug("updatePatches","Got a non-object patch",e);if(n.showDebug&&n.allPatches.push(e),_e.isPromise(e.value))return n.promisedPatches.push(e),void n.promisedPatchThen(e);if(_e.isContextPatch(e))return void n.setContext(e.path,e.value);if(_e.isMutation(e))return void n.updateMutations(e)}catch(e){console.error(e),n.errors.push(e)}})}},{key:"updateMutations",value:function(e){"object"===P()(e.value)&&!M()(e.value)&&this.allowMetaPatches&&(e.value=ie()({},e.value));var t=_e.applyPatch(this.state,e,{allowMetaPatches:this.allowMetaPatches});t&&(this.mutations.push(e),this.state=t)}},{key:"removePromisedPatch",value:function(e){var t=this.promisedPatches.indexOf(e);t<0?this.debug("Tried to remove a promisedPatch that isn't there!"):this.promisedPatches.splice(t,1)}},{key:"promisedPatchThen",value:function(e){var t=this;return e.value=e.value.then(function(n){var r=ie()({},e,{value:n});t.removePromisedPatch(e),t.updatePatches(r)}).catch(function(n){t.removePromisedPatch(e),t.updatePatches(n)})}},{key:"getMutations",value:function(e,t){return e=e||0,"number"!=typeof t&&(t=this.mutations.length),this.mutations.slice(e,t)}},{key:"getCurrentMutations",value:function(){return this.getMutationsForPlugin(this.getCurrentPlugin())}},{key:"getMutationsForPlugin",value:function(e){var t=this.getPluginMutationIndex(e);return this.getMutations(t+1)}},{key:"getCurrentPlugin",value:function(){return this.currentPlugin}},{key:"getPatchesOfType",value:function(e,t){return e.filter(t)}},{key:"getLib",value:function(){return this.libMethods}},{key:"_get",value:function(e){return _e.getIn(this.state,e)}},{key:"_getContext",value:function(e){return this.contextTree.get(e)}},{key:"setContext",value:function(e,t){return this.contextTree.set(e,t)}},{key:"_hasRun",value:function(e){return this.getPluginRunCount(this.getCurrentPlugin())>(e||0)}},{key:"_clone",value:function(e){return JSON.parse(T()(e))}},{key:"dispatch",value:function(){var e=this,t=this,n=this.nextPlugin();if(!n){var r=this.nextPromisedPatch();if(r)return r.then(function(){return e.dispatch()}).catch(function(){return e.dispatch()});var o={spec:this.state,errors:this.errors};return this.showDebug&&(o.patches=this.allPatches),Q.a.resolve(o)}if(t.pluginCount=t.pluginCount||{},t.pluginCount[n]=(t.pluginCount[n]||0)+1,t.pluginCount[n]>100)return Q.a.resolve({spec:t.state,errors:t.errors.concat(new Error("We've reached a hard limit of ".concat(100," plugin runs")))});if(n!==this.currentPlugin&&this.promisedPatches.length){var i=this.promisedPatches.map(function(e){return e.value});return Q.a.all(i.map(function(e){return e.then(Function,Function)})).then(function(){return e.dispatch()})}return function(){t.currentPlugin=n;var e=t.getCurrentMutations(),r=t.mutations.length-1;try{if(n.isGenerator){var o=!0,i=!1,s=void 0;try{for(var u,c=te()(n(e,t.getLib()));!(o=(u=c.next()).done);o=!0){a(u.value)}}catch(e){i=!0,s=e}finally{try{o||null==c.return||c.return()}finally{if(i)throw s}}}else{a(n(e,t.getLib()))}}catch(e){console.error(e),a([ie()(re()(e),{plugin:n})])}finally{t.updatePluginHistory(n,{mutationIndex:r})}return t.dispatch()}();function a(e){e&&(e=_e.fullyNormalizeArray(e),t.updatePatches(e,n))}}}]),e}();var St={refs:ot,allOf:vt,parameters:gt,properties:yt},Ct=n(29),kt=n.n(Ct),Ot=function(e){return String.prototype.toLowerCase.call(e)},At=function(e){return e.replace(/[^\w]/gi,"_")};function Tt(e){var t=e.openapi;return!!t&&w()(t,"3")}function jt(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"",r=(arguments.length>3&&void 0!==arguments[3]?arguments[3]:{}).v2OperationIdCompatibilityMode;return e&&"object"===P()(e)?(e.operationId||"").replace(/\s/g,"").length?At(e.operationId):function(e,t){if((arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).v2OperationIdCompatibilityMode){var n="".concat(t.toLowerCase(),"_").concat(e).replace(/[\s!@#$%^&*()_+=[{\]};:<>|.\/?,\\'""-]/g,"_");return(n=n||"".concat(e.substring(1),"_").concat(t)).replace(/((_){2,})/g,"_").replace(/^(_)*/g,"").replace(/([_])*$/g,"")}return"".concat(Ot(t)).concat(At(e))}(t,n,{v2OperationIdCompatibilityMode:r}):null}function Pt(e,t){return"".concat(Ot(t),"-").concat(e)}function It(e,t){return e&&e.paths?function(e,t){return Mt(e,t,!0)||null}(e,function(e){var n=e.pathName,r=e.method,o=e.operation;if(!o||"object"!==P()(o))return!1;var i=o.operationId;return[jt(o,n,r),Pt(n,r),i].some(function(e){return e&&e===t})}):null}function Mt(e,t,n){if(!e||"object"!==P()(e)||!e.paths||"object"!==P()(e.paths))return null;var r=e.paths;for(var o in r)for(var i in r[o])if("PARAMETERS"!==i.toUpperCase()){var a=r[o][i];if(a&&"object"===P()(a)){var s={spec:e,pathName:o,method:i.toUpperCase(),operation:a},u=t(s);if(n&&u)return s}}}function Nt(e){var t=e.spec,n=t.paths,r={};if(!n||t.$$normalized)return e;for(var o in n){var i=n[o];if(kt()(i)){var a=i.parameters,s=function(e){var n=i[e];if(!kt()(n))return"continue";var s=jt(n,o,e);if(s){r[s]?r[s].push(n):r[s]=[n];var u=r[s];if(u.length>1)u.forEach(function(e,t){e.__originalOperationId=e.__originalOperationId||e.operationId,e.operationId="".concat(s).concat(t+1)});else if(void 0!==n.operationId){var c=u[0];c.__originalOperationId=c.__originalOperationId||n.operationId,c.operationId=s}}if("parameters"!==e){var l=[],p={};for(var f in t)"produces"!==f&&"consumes"!==f&&"security"!==f||(p[f]=t[f],l.push(p));if(a&&(p.parameters=a,l.push(p)),l.length)for(var h=0,d=l;h1&&void 0!==arguments[1]?arguments[1]:{},n=t.requestInterceptor,r=t.responseInterceptor,o=e.withCredentials?"include":"same-origin";return function(t){return e({url:t,loadSpec:!0,requestInterceptor:n,responseInterceptor:r,headers:{Accept:Ge},credentials:o}).then(function(e){return e.body})}}function Dt(e){var t=e.fetch,n=e.spec,r=e.url,o=e.mode,i=e.allowMetaPatches,a=void 0===i||i,s=e.pathDiscriminator,u=e.modelPropertyMacro,c=e.parameterMacro,l=e.requestInterceptor,p=e.responseInterceptor,f=e.skipNormalization,h=e.useCircularStructures,d=e.http,m=e.baseDoc;return m=m||r,d=t||d||V,n?v(n):Rt(d,{requestInterceptor:l,responseInterceptor:p})(m).then(v);function v(e){m&&(St.refs.docCache[m]=e),St.refs.fetchJSON=Rt(d,{requestInterceptor:l,responseInterceptor:p});var t,n=[St.refs];return"function"==typeof c&&n.push(St.parameters),"function"==typeof u&&n.push(St.properties),"strict"!==o&&n.push(St.allOf),(t={spec:e,context:{baseDoc:m},plugins:n,allowMetaPatches:a,pathDiscriminator:s,parameterMacro:c,modelPropertyMacro:u,useCircularStructures:h},new Et(t).dispatch()).then(f?function(){var e=R()(C.a.mark(function e(t){return C.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",t);case 1:case"end":return e.stop()}},e)}));return function(t){return e.apply(this,arguments)}}():Nt)}}var Lt=n(16),Ut=n.n(Lt);function qt(e,t){var n=m()(e);if(h.a){var r=h()(e);t&&(r=r.filter(function(t){return p()(e,t).enumerable})),n.push.apply(n,r)}return n}function Ft(e){for(var t=1;t2&&void 0!==m[2]?m[2]:{},o=r.returnEntireTree,i=r.baseDoc,a=r.requestInterceptor,s=r.responseInterceptor,u=r.parameterMacro,c=r.modelPropertyMacro,l=r.useCircularStructures,p={pathDiscriminator:n,baseDoc:i,requestInterceptor:a,responseInterceptor:s,parameterMacro:u,modelPropertyMacro:c,useCircularStructures:l},f=Nt({spec:t}),h=f.spec,e.next=6,Dt(Ft({},p,{spec:h,allowMetaPatches:!0,skipNormalization:!0}));case 6:return d=e.sent,!o&&M()(n)&&n.length&&(d.spec=Ut()(d.spec,n)||null),e.abrupt("return",d);case 9:case"end":return e.stop()}},e)}))).apply(this,arguments)}var zt=n(38),Vt=n.n(zt);function Ht(e,t){var n=m()(e);if(h.a){var r=h()(e);t&&(r=r.filter(function(t){return p()(e,t).enumerable})),n.push.apply(n,r)}return n}function Wt(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:{};return function(t){var n=t.pathName,r=t.method,o=t.operationId;return function(t){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.execute(Wt({spec:e.spec},Vt()(e,"requestInterceptor","responseInterceptor","userFetch"),{pathName:n,method:r,parameters:t,operationId:o},i))}}}};var $t=n(39),Gt=n.n($t),Zt=n(40),Xt=n.n(Zt),Qt=n(41),en=n.n(Qt),tn=n(19),nn=n.n(tn),rn=n(42),on=n.n(rn),an={body:function(e){var t=e.req,n=e.value;t.body=n},header:function(e){var t=e.req,n=e.parameter,r=e.value;t.headers=t.headers||{},void 0!==r&&(t.headers[n.name]=r)},query:function(e){var t=e.req,n=e.value,r=e.parameter;t.query=t.query||{},!1===n&&"boolean"===r.type&&(n="false");0===n&&["number","integer"].indexOf(r.type)>-1&&(n="0");if(n)t.query[r.name]={collectionFormat:r.collectionFormat,value:n};else if(r.allowEmptyValue&&void 0!==n){var o=r.name;t.query[o]=t.query[o]||{},t.query[o].allowEmptyValue=!0}},path:function(e){var t=e.req,n=e.value,r=e.parameter;t.url=t.url.split("{".concat(r.name,"}")).join(encodeURIComponent(n))},formData:function(e){var t=e.req,n=e.value,r=e.parameter;(n||r.allowEmptyValue)&&(t.form=t.form||{},t.form[r.name]={value:n,allowEmptyValue:r.allowEmptyValue,collectionFormat:r.collectionFormat})}};n(49);var sn=n(43),un=n.n(sn),cn=n(44),ln=function(e){return":/?#[]@!$&'()*+,;=".indexOf(e)>-1},pn=function(e){return/^[a-z0-9\-._~]+$/i.test(e)};function fn(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).escape,n=arguments.length>2?arguments[2]:void 0;return"number"==typeof e&&(e=e.toString()),"string"==typeof e&&e.length&&t?n?JSON.parse(e):Object(cn.stringToCharArray)(e).map(function(e){return pn(e)?e:ln(e)&&"unsafe"===t?e:(un()(e)||[]).map(function(e){return"0".concat(e.toString(16).toUpperCase()).slice(-2)}).map(function(e){return"%".concat(e)}).join("")}).join(""):e}function hn(e){var t=e.value;return M()(t)?function(e){var t=e.key,n=e.value,r=e.style,o=e.explode,i=e.escape,a=function(e){return fn(e,{escape:i})};if("simple"===r)return n.map(function(e){return a(e)}).join(",");if("label"===r)return".".concat(n.map(function(e){return a(e)}).join("."));if("matrix"===r)return n.map(function(e){return a(e)}).reduce(function(e,n){return!e||o?"".concat(e||"",";").concat(t,"=").concat(n):"".concat(e,",").concat(n)},"");if("form"===r){var s=o?"&".concat(t,"="):",";return n.map(function(e){return a(e)}).join(s)}if("spaceDelimited"===r){var u=o?"".concat(t,"="):"";return n.map(function(e){return a(e)}).join(" ".concat(u))}if("pipeDelimited"===r){var c=o?"".concat(t,"="):"";return n.map(function(e){return a(e)}).join("|".concat(c))}}(e):"object"===P()(t)?function(e){var t=e.key,n=e.value,r=e.style,o=e.explode,i=e.escape,a=function(e){return fn(e,{escape:i})},s=m()(n);if("simple"===r)return s.reduce(function(e,t){var r=a(n[t]),i=o?"=":",",s=e?"".concat(e,","):"";return"".concat(s).concat(t).concat(i).concat(r)},"");if("label"===r)return s.reduce(function(e,t){var r=a(n[t]),i=o?"=":".",s=e?"".concat(e,"."):".";return"".concat(s).concat(t).concat(i).concat(r)},"");if("matrix"===r&&o)return s.reduce(function(e,t){var r=a(n[t]),o=e?"".concat(e,";"):";";return"".concat(o).concat(t,"=").concat(r)},"");if("matrix"===r)return s.reduce(function(e,r){var o=a(n[r]),i=e?"".concat(e,","):";".concat(t,"=");return"".concat(i).concat(r,",").concat(o)},"");if("form"===r)return s.reduce(function(e,t){var r=a(n[t]),i=e?"".concat(e).concat(o?"&":","):"",s=o?"=":",";return"".concat(i).concat(t).concat(s).concat(r)},"")}(e):function(e){var t=e.key,n=e.value,r=e.style,o=e.escape,i=function(e){return fn(e,{escape:o})};if("simple"===r)return i(n);if("label"===r)return".".concat(i(n));if("matrix"===r)return";".concat(t,"=").concat(i(n));if("form"===r)return i(n);if("deepObject"===r)return i(n)}(e)}function dn(e,t){return t.includes("application/json")?"string"==typeof e?e:T()(e):e.toString()}function mn(e){var t=e.req,n=e.value,r=e.parameter,o=r.name,i=r.style,a=r.explode,s=r.content;if(s){var u=m()(s)[0];t.url=t.url.split("{".concat(o,"}")).join(fn(dn(n,u),{escape:!0}))}else{var c=hn({key:r.name,value:n,style:i||"simple",explode:a||!1,escape:!0});t.url=t.url.split("{".concat(o,"}")).join(c)}}function vn(e){var t=e.req,n=e.value,r=e.parameter;if(t.query=t.query||{},r.content){var o=m()(r.content)[0];t.query[r.name]=dn(n,o)}else if(!1===n&&(n="false"),0===n&&(n="0"),n){var i=P()(n);if("deepObject"===r.style)m()(n).forEach(function(e){var o=n[e];t.query["".concat(r.name,"[").concat(e,"]")]={value:hn({key:e,value:o,style:"deepObject",escape:r.allowReserved?"unsafe":"reserved"}),skipEncoding:!0}});else if("object"!==i||M()(n)||"form"!==r.style&&r.style||!r.explode&&void 0!==r.explode)t.query[r.name]={value:hn({key:r.name,value:n,style:r.style||"form",explode:void 0===r.explode||r.explode,escape:r.allowReserved?"unsafe":"reserved"}),skipEncoding:!0};else{m()(n).forEach(function(e){var o=n[e];t.query[e]={value:hn({key:e,value:o,style:r.style||"form",escape:r.allowReserved?"unsafe":"reserved"}),skipEncoding:!0}})}}else if(r.allowEmptyValue&&void 0!==n){var a=r.name;t.query[a]=t.query[a]||{},t.query[a].allowEmptyValue=!0}}var gn=["accept","authorization","content-type"];function yn(e){var t=e.req,n=e.parameter,r=e.value;if(t.headers=t.headers||{},!(gn.indexOf(n.name.toLowerCase())>-1))if(n.content){var o=m()(n.content)[0];t.headers[n.name]=dn(r,o)}else void 0!==r&&(t.headers[n.name]=hn({key:n.name,value:r,style:n.style||"simple",explode:void 0!==n.explode&&n.explode,escape:!1}))}function bn(e){var t=e.req,n=e.parameter,r=e.value;t.headers=t.headers||{};var o=P()(r);if(n.content){var i=m()(n.content)[0];t.headers.Cookie="".concat(n.name,"=").concat(dn(r,i))}else if("undefined"!==o){var a="object"===o&&!M()(r)&&n.explode?"":"".concat(n.name,"=");t.headers.Cookie=a+hn({key:n.name,value:r,escape:!1,style:n.style||"form",explode:void 0!==n.explode&&n.explode})}}var _n=n(30),wn=function(e,t){var n=e.operation,r=e.requestBody,o=e.securities,i=e.spec,a=e.attachContentTypeForEmptyPayload,s=e.requestContentType;t=function(e){var t=e.request,n=e.securities,r=void 0===n?{}:n,o=e.operation,i=void 0===o?{}:o,a=e.spec,s=b()({},t),u=r.authorized,c=void 0===u?{}:u,l=i.security||a.security||[],p=c&&!!m()(c).length,f=Ut()(a,["components","securitySchemes"])||{};if(s.headers=s.headers||{},s.query=s.query||{},!m()(r).length||!p||!l||M()(i.security)&&!i.security.length)return t;return l.forEach(function(e,t){for(var n in e){var r=c[n],o=f[n];if(r){var i=r.value||r,a=o.type;if(r)if("apiKey"===a)"query"===o.in&&(s.query[o.name]=i),"header"===o.in&&(s.headers[o.name]=i),"cookie"===o.in&&(s.cookies[o.name]=i);else if("http"===a){if("basic"===o.scheme){var u=i.username,l=i.password,p=nn()("".concat(u,":").concat(l));s.headers.Authorization="Basic ".concat(p)}"bearer"===o.scheme&&(s.headers.Authorization="Bearer ".concat(i))}else if("oauth2"===a){var h=r.token||{},d=h.access_token,m=h.token_type;m&&"bearer"!==m.toLowerCase()||(m="Bearer"),s.headers.Authorization="".concat(m," ").concat(d)}}}}),s}({request:t,securities:o,operation:n,spec:i});var u=n.requestBody||{},c=m()(u.content||{}),l=s&&c.indexOf(s)>-1;if(r||a){if(s&&l)t.headers["Content-Type"]=s;else if(!s){var p=c[0];p&&(t.headers["Content-Type"]=p,s=p)}}else s&&l&&(t.headers["Content-Type"]=s);return r&&(s?c.indexOf(s)>-1&&("application/x-www-form-urlencoded"===s||0===s.indexOf("multipart/")?"object"===P()(r)?(t.form={},m()(r).forEach(function(e){var n,o,i=r[e];"undefined"!=typeof File&&(o=i instanceof File),"undefined"!=typeof Blob&&(o=o||i instanceof Blob),void 0!==_n.Buffer&&(o=o||_n.Buffer.isBuffer(i)),n="object"!==P()(i)||o?i:M()(i)?i.toString():T()(i),t.form[e]={value:n}})):t.form=r:t.body=r):t.body=r),t};var xn=function(e,t){var n=e.spec,r=e.operation,o=e.securities,i=e.requestContentType,a=e.attachContentTypeForEmptyPayload;if((t=function(e){var t=e.request,n=e.securities,r=void 0===n?{}:n,o=e.operation,i=void 0===o?{}:o,a=e.spec,s=b()({},t),u=r.authorized,c=void 0===u?{}:u,l=r.specSecurity,p=void 0===l?[]:l,f=i.security||p,h=c&&!!m()(c).length,d=a.securityDefinitions;if(s.headers=s.headers||{},s.query=s.query||{},!m()(r).length||!h||!f||M()(i.security)&&!i.security.length)return t;return f.forEach(function(e,t){for(var n in e){var r=c[n];if(r){var o=r.token,i=r.value||r,a=d[n],u=a.type,l=a["x-tokenName"]||"access_token",p=o&&o[l],f=o&&o.token_type;if(r)if("apiKey"===u){var h="query"===a.in?"query":"headers";s[h]=s[h]||{},s[h][a.name]=i}else"basic"===u?i.header?s.headers.authorization=i.header:(i.base64=nn()("".concat(i.username,":").concat(i.password)),s.headers.authorization="Basic ".concat(i.base64)):"oauth2"===u&&p&&(f=f&&"bearer"!==f.toLowerCase()?f:"Bearer",s.headers.authorization="".concat(f," ").concat(p))}}}),s}({request:t,securities:o,operation:r,spec:n})).body||t.form||a)i?t.headers["Content-Type"]=i:M()(r.consumes)?t.headers["Content-Type"]=r.consumes[0]:M()(n.consumes)?t.headers["Content-Type"]=n.consumes[0]:r.parameters&&r.parameters.filter(function(e){return"file"===e.type}).length?t.headers["Content-Type"]="multipart/form-data":r.parameters&&r.parameters.filter(function(e){return"formData"===e.in}).length&&(t.headers["Content-Type"]="application/x-www-form-urlencoded");else if(i){var s=r.parameters&&r.parameters.filter(function(e){return"body"===e.in}).length>0,u=r.parameters&&r.parameters.filter(function(e){return"formData"===e.in}).length>0;(s||u)&&(t.headers["Content-Type"]=i)}return t};function En(e,t){var n=m()(e);if(h.a){var r=h()(e);t&&(r=r.filter(function(t){return p()(e,t).enumerable})),n.push.apply(n,r)}return n}function Sn(e){for(var t=1;t-1&&(c=o,l=u[p.indexOf(o)])}return!c&&u&&u.length&&(c=u[0].url,l=u[0]),c.indexOf("{")>-1&&function(e){for(var t,n=[],r=/{([^}]+)}/g;t=r.exec(e);)n.push(t[1]);return n}(c).forEach(function(e){if(l.variables&&l.variables[e]){var t=l.variables[e],n=s[e]||t.default,r=new RegExp("{".concat(e,"}"),"g");c=c.replace(r,n)}}),function(){var e,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",r=E.a.parse(t),o=E.a.parse(n),i=Pn(r.protocol)||Pn(o.protocol)||"",a=r.host||o.host,s=r.pathname||"";return"/"===(e=i&&a?"".concat(i,"://").concat(a+s):s)[e.length-1]?e.slice(0,-1):e}(c,i)}(b):function(e){var t,n=e.spec,r=e.scheme,o=e.contextUrl,i=void 0===o?"":o,a=E.a.parse(i),s=M()(n.schemes)?n.schemes[0]:null,u=r||s||Pn(a.protocol)||"http",c=n.host||a.host||"",l=n.basePath||"";return"/"===(t=u&&c?"".concat(u,"://").concat(c+l):l)[t.length-1]?t.slice(0,-1):t}(b),!n)return delete g.cookies,g;g.url+=S,g.method="".concat(x).toUpperCase(),h=h||{};var C=t.paths[S]||{};o&&(g.headers.accept=o);var k=An([].concat(Cn(w.parameters)).concat(Cn(C.parameters)));k.forEach(function(e){var n,r=d[e.in];if("body"===e.in&&e.schema&&e.schema.properties&&(n=h),void 0===(n=e&&e.name&&h[e.name])?n=e&&e.name&&h["".concat(e.in,".").concat(e.name)]:On(e.name,k).length>1&&console.warn("Parameter '".concat(e.name,"' is ambiguous because the defined spec has more than one parameter with the name: '").concat(e.name,"' and the passed-in parameter values did not define an 'in' value.")),null!==n){if(void 0!==e.default&&void 0===n&&(n=e.default),void 0===n&&e.required&&!e.allowEmptyValue)throw new Error("Required parameter ".concat(e.name," is not provided"));if(v&&e.schema&&"object"===e.schema.type&&"string"==typeof n)try{n=JSON.parse(n)}catch(e){throw new Error("Could not parse object parameter value string as JSON")}r&&r({req:g,parameter:e,value:n,operation:w,spec:t})}});var O=Sn({},e,{operation:w});if((g=v?wn(O,g):xn(O,g)).cookies&&m()(g.cookies).length){var A=m()(g.cookies).reduce(function(e,t){var n=g.cookies[t];return e+(e?"&":"")+on.a.serialize(t,n)},"");g.headers.Cookie=A}return g.cookies&&delete g.cookies,Z(g),g}var Pn=function(e){return e?e.replace(/\W/g,""):null};function In(e,t){var n=m()(e);if(h.a){var r=h()(e);t&&(r=r.filter(function(t){return p()(e,t).enumerable})),n.push.apply(n,r)}return n}function Mn(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if("string"==typeof e?n.url=e:n=e,!(this instanceof Mn))return new Mn(n);b()(this,n);var r=this.resolve().then(function(){return t.disableInterfaces||b()(t,Mn.makeApisTagOperation(t)),t});return r.client=this,r}Mn.http=V,Mn.makeHttp=function(e,t,n){return n=n||function(e){return e},t=t||function(e){return e},function(r){return"string"==typeof r&&(r={url:r}),z.mergeInQueryOrForm(r),r=t(r),n(e(r))}}.bind(null,Mn.http),Mn.resolve=Dt,Mn.resolveSubtree=function(e,t){return Bt.apply(this,arguments)},Mn.execute=function(e){var t=e.http,n=e.fetch,r=e.spec,o=e.operationId,i=e.pathName,a=e.method,s=e.parameters,u=e.securities,c=Gt()(e,["http","fetch","spec","operationId","pathName","method","parameters","securities"]),l=t||n||V;i&&a&&!o&&(o=Pt(i,a));var p=Tn.buildRequest(Sn({spec:r,operationId:o,parameters:s,securities:u,http:l},c));return p.body&&(Xt()(p.body)||en()(p.body))&&(p.body=T()(p.body)),l(p)},Mn.serializeRes=J,Mn.serializeHeaders=K,Mn.clearCache=function(){St.refs.clearCache()},Mn.makeApisTagOperation=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=Yt.makeExecute(e);return{apis:Yt.mapTagOperations({v2OperationIdCompatibilityMode:e.v2OperationIdCompatibilityMode,spec:e.spec,cb:t})}},Mn.buildRequest=jn,Mn.helpers={opId:jt},Mn.prototype={http:V,execute:function(e){return this.applyDefaults(),Mn.execute(function(e){for(var t=1;t + * @license MIT + */ +var r=n(569),o=n(570),i=n(355);function a(){return u.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function s(e,t){if(a()=a())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+a().toString(16)+" bytes");return 0|e}function d(e,t){if(u.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return B(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return z(e).length;default:if(r)return B(e).length;t=(""+t).toLowerCase(),r=!0}}function m(e,t,n){var r=!1;if((void 0===t||t<0)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return j(this,t,n);case"utf8":case"utf-8":return k(this,t,n);case"ascii":return A(this,t,n);case"latin1":case"binary":return T(this,t,n);case"base64":return C(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return P(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}function v(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function g(e,t,n,r,o){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=o?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(o)return-1;n=e.length-1}else if(n<0){if(!o)return-1;n=0}if("string"==typeof t&&(t=u.from(t,r)),u.isBuffer(t))return 0===t.length?-1:y(e,t,n,r,o);if("number"==typeof t)return t&=255,u.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?o?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):y(e,[t],n,r,o);throw new TypeError("val must be string, number or Buffer")}function y(e,t,n,r,o){var i,a=1,s=e.length,u=t.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;a=2,s/=2,u/=2,n/=2}function c(e,t){return 1===a?e[t]:e.readUInt16BE(t*a)}if(o){var l=-1;for(i=n;is&&(n=s-u),i=n;i>=0;i--){for(var p=!0,f=0;fo&&(r=o):r=o;var i=t.length;if(i%2!=0)throw new TypeError("Invalid hex string");r>i/2&&(r=i/2);for(var a=0;a>8,o=n%256,i.push(o),i.push(r);return i}(t,e.length-n),e,n,r)}function C(e,t,n){return 0===t&&n===e.length?r.fromByteArray(e):r.fromByteArray(e.slice(t,n))}function k(e,t,n){n=Math.min(e.length,n);for(var r=[],o=t;o239?4:c>223?3:c>191?2:1;if(o+p<=n)switch(p){case 1:c<128&&(l=c);break;case 2:128==(192&(i=e[o+1]))&&(u=(31&c)<<6|63&i)>127&&(l=u);break;case 3:i=e[o+1],a=e[o+2],128==(192&i)&&128==(192&a)&&(u=(15&c)<<12|(63&i)<<6|63&a)>2047&&(u<55296||u>57343)&&(l=u);break;case 4:i=e[o+1],a=e[o+2],s=e[o+3],128==(192&i)&&128==(192&a)&&128==(192&s)&&(u=(15&c)<<18|(63&i)<<12|(63&a)<<6|63&s)>65535&&u<1114112&&(l=u)}null===l?(l=65533,p=1):l>65535&&(l-=65536,r.push(l>>>10&1023|55296),l=56320|1023&l),r.push(l),o+=p}return function(e){var t=e.length;if(t<=O)return String.fromCharCode.apply(String,e);var n="",r=0;for(;r0&&(e=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(e+=" ... ")),""},u.prototype.compare=function(e,t,n,r,o){if(!u.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===o&&(o=this.length),t<0||n>e.length||r<0||o>this.length)throw new RangeError("out of range index");if(r>=o&&t>=n)return 0;if(r>=o)return-1;if(t>=n)return 1;if(this===e)return 0;for(var i=(o>>>=0)-(r>>>=0),a=(n>>>=0)-(t>>>=0),s=Math.min(i,a),c=this.slice(r,o),l=e.slice(t,n),p=0;po)&&(n=o),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var i=!1;;)switch(r){case"hex":return b(this,e,t,n);case"utf8":case"utf-8":return _(this,e,t,n);case"ascii":return w(this,e,t,n);case"latin1":case"binary":return x(this,e,t,n);case"base64":return E(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return S(this,e,t,n);default:if(i)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),i=!0}},u.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var O=4096;function A(e,t,n){var r="";n=Math.min(e.length,n);for(var o=t;or)&&(n=r);for(var o="",i=t;in)throw new RangeError("Trying to access beyond buffer length")}function M(e,t,n,r,o,i){if(!u.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>o||te.length)throw new RangeError("Index out of range")}function N(e,t,n,r){t<0&&(t=65535+t+1);for(var o=0,i=Math.min(e.length-n,2);o>>8*(r?o:1-o)}function R(e,t,n,r){t<0&&(t=4294967295+t+1);for(var o=0,i=Math.min(e.length-n,4);o>>8*(r?o:3-o)&255}function D(e,t,n,r,o,i){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function L(e,t,n,r,i){return i||D(e,0,n,4),o.write(e,t,n,r,23,4),n+4}function U(e,t,n,r,i){return i||D(e,0,n,8),o.write(e,t,n,r,52,8),n+8}u.prototype.slice=function(e,t){var n,r=this.length;if((e=~~e)<0?(e+=r)<0&&(e=0):e>r&&(e=r),(t=void 0===t?r:~~t)<0?(t+=r)<0&&(t=0):t>r&&(t=r),t0&&(o*=256);)r+=this[e+--t]*o;return r},u.prototype.readUInt8=function(e,t){return t||I(e,1,this.length),this[e]},u.prototype.readUInt16LE=function(e,t){return t||I(e,2,this.length),this[e]|this[e+1]<<8},u.prototype.readUInt16BE=function(e,t){return t||I(e,2,this.length),this[e]<<8|this[e+1]},u.prototype.readUInt32LE=function(e,t){return t||I(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},u.prototype.readUInt32BE=function(e,t){return t||I(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},u.prototype.readIntLE=function(e,t,n){e|=0,t|=0,n||I(e,t,this.length);for(var r=this[e],o=1,i=0;++i=(o*=128)&&(r-=Math.pow(2,8*t)),r},u.prototype.readIntBE=function(e,t,n){e|=0,t|=0,n||I(e,t,this.length);for(var r=t,o=1,i=this[e+--r];r>0&&(o*=256);)i+=this[e+--r]*o;return i>=(o*=128)&&(i-=Math.pow(2,8*t)),i},u.prototype.readInt8=function(e,t){return t||I(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},u.prototype.readInt16LE=function(e,t){t||I(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt16BE=function(e,t){t||I(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt32LE=function(e,t){return t||I(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},u.prototype.readInt32BE=function(e,t){return t||I(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},u.prototype.readFloatLE=function(e,t){return t||I(e,4,this.length),o.read(this,e,!0,23,4)},u.prototype.readFloatBE=function(e,t){return t||I(e,4,this.length),o.read(this,e,!1,23,4)},u.prototype.readDoubleLE=function(e,t){return t||I(e,8,this.length),o.read(this,e,!0,52,8)},u.prototype.readDoubleBE=function(e,t){return t||I(e,8,this.length),o.read(this,e,!1,52,8)},u.prototype.writeUIntLE=function(e,t,n,r){(e=+e,t|=0,n|=0,r)||M(this,e,t,n,Math.pow(2,8*n)-1,0);var o=1,i=0;for(this[t]=255&e;++i=0&&(i*=256);)this[t+o]=e/i&255;return t+n},u.prototype.writeUInt8=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,1,255,0),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},u.prototype.writeUInt16LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):N(this,e,t,!0),t+2},u.prototype.writeUInt16BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):N(this,e,t,!1),t+2},u.prototype.writeUInt32LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):R(this,e,t,!0),t+4},u.prototype.writeUInt32BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):R(this,e,t,!1),t+4},u.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);M(this,e,t,n,o-1,-o)}var i=0,a=1,s=0;for(this[t]=255&e;++i>0)-s&255;return t+n},u.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);M(this,e,t,n,o-1,-o)}var i=n-1,a=1,s=0;for(this[t+i]=255&e;--i>=0&&(a*=256);)e<0&&0===s&&0!==this[t+i+1]&&(s=1),this[t+i]=(e/a>>0)-s&255;return t+n},u.prototype.writeInt8=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,1,127,-128),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},u.prototype.writeInt16LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):N(this,e,t,!0),t+2},u.prototype.writeInt16BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):N(this,e,t,!1),t+2},u.prototype.writeInt32LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,2147483647,-2147483648),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):R(this,e,t,!0),t+4},u.prototype.writeInt32BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):R(this,e,t,!1),t+4},u.prototype.writeFloatLE=function(e,t,n){return L(this,e,t,!0,n)},u.prototype.writeFloatBE=function(e,t,n){return L(this,e,t,!1,n)},u.prototype.writeDoubleLE=function(e,t,n){return U(this,e,t,!0,n)},u.prototype.writeDoubleBE=function(e,t,n){return U(this,e,t,!1,n)},u.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t=0;--o)e[o+t]=this[o+n];else if(i<1e3||!u.TYPED_ARRAY_SUPPORT)for(o=0;o>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(i=t;i55295&&n<57344){if(!o){if(n>56319){(t-=3)>-1&&i.push(239,191,189);continue}if(a+1===r){(t-=3)>-1&&i.push(239,191,189);continue}o=n;continue}if(n<56320){(t-=3)>-1&&i.push(239,191,189),o=n;continue}n=65536+(o-55296<<10|n-56320)}else o&&(t-=3)>-1&&i.push(239,191,189);if(o=null,n<128){if((t-=1)<0)break;i.push(n)}else if(n<2048){if((t-=2)<0)break;i.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;i.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;i.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return i}function z(e){return r.toByteArray(function(e){if((e=function(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}(e).replace(q,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function V(e,t,n,r){for(var o=0;o=t.length||o>=e.length);++o)t[o+n]=e[o];return o}}).call(this,n(36))},function(e,t,n){"use strict";e.exports={current:null}},function(e,t){e.exports=function(e){return null!=e&&"object"==typeof e}},function(e,t){var n,r,o=e.exports={};function i(){throw new Error("setTimeout has not been defined")}function a(){throw new Error("clearTimeout has not been defined")}function s(e){if(n===setTimeout)return setTimeout(e,0);if((n===i||!n)&&setTimeout)return n=setTimeout,setTimeout(e,0);try{return n(e,0)}catch(t){try{return n.call(null,e,0)}catch(t){return n.call(this,e,0)}}}!function(){try{n="function"==typeof setTimeout?setTimeout:i}catch(e){n=i}try{r="function"==typeof clearTimeout?clearTimeout:a}catch(e){r=a}}();var u,c=[],l=!1,p=-1;function f(){l&&u&&(l=!1,u.length?c=u.concat(c):p=-1,c.length&&h())}function h(){if(!l){var e=s(f);l=!0;for(var t=c.length;t;){for(u=c,c=[];++p1)for(var n=1;n0&&"/"!==t[0]});function oe(e,t,n){return t=t||[],te.apply(void 0,[e].concat(u()(t))).get("parameters",Object(p.List)()).reduce(function(e,t){var r=n&&"body"===t.get("in")?t.get("value_xml"):t.get("value");return e.set(Object(l.B)(t,{allowHashes:!1}),r)},Object(p.fromJS)({}))}function ie(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(p.List.isList(e))return e.some(function(e){return p.Map.isMap(e)&&e.get("in")===t})}function ae(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(p.List.isList(e))return e.some(function(e){return p.Map.isMap(e)&&e.get("type")===t})}function se(e,t){t=t||[];var n=x(e).getIn(["paths"].concat(u()(t)),Object(p.fromJS)({})),r=e.getIn(["meta","paths"].concat(u()(t)),Object(p.fromJS)({})),o=ue(e,t),i=n.get("parameters")||new p.List,a=r.get("consumes_value")?r.get("consumes_value"):ae(i,"file")?"multipart/form-data":ae(i,"formData")?"application/x-www-form-urlencoded":void 0;return Object(p.fromJS)({requestContentType:a,responseContentType:o})}function ue(e,t){t=t||[];var n=x(e).getIn(["paths"].concat(u()(t)),null);if(null!==n){var r=e.getIn(["meta","paths"].concat(u()(t),["produces_value"]),null),o=n.getIn(["produces",0],null);return r||o||"application/json"}}function ce(e,t){t=t||[];var n=x(e),r=n.getIn(["paths"].concat(u()(t)),null);if(null!==r){var o=t,i=a()(o,1)[0],s=r.get("produces",null),c=n.getIn(["paths",i,"produces"],null),l=n.getIn(["produces"],null);return s||c||l}}function le(e,t){t=t||[];var n=x(e),r=n.getIn(["paths"].concat(u()(t)),null);if(null!==r){var o=t,i=a()(o,1)[0],s=r.get("consumes",null),c=n.getIn(["paths",i,"consumes"],null),l=n.getIn(["consumes"],null);return s||c||l}}var pe=function(e,t,n){var r=e.get("url").match(/^([a-z][a-z0-9+\-.]*):/),i=o()(r)?r[1]:null;return e.getIn(["scheme",t,n])||e.getIn(["scheme","_defaultScheme"])||i||""},fe=function(e,t,n){return["http","https"].indexOf(pe(e,t,n))>-1},he=function(e,t){t=t||[];var n=e.getIn(["meta","paths"].concat(u()(t),["parameters"]),Object(p.fromJS)([])),r=!0;return n.forEach(function(e){var t=e.get("errors");t&&t.count()&&(r=!1)}),r};function de(e){return p.Map.isMap(e)?e:new p.Map}},function(e,t,n){"use strict";n.r(t),n.d(t,"SHOW_AUTH_POPUP",function(){return d}),n.d(t,"AUTHORIZE",function(){return m}),n.d(t,"LOGOUT",function(){return v}),n.d(t,"PRE_AUTHORIZE_OAUTH2",function(){return g}),n.d(t,"AUTHORIZE_OAUTH2",function(){return y}),n.d(t,"VALIDATE",function(){return b}),n.d(t,"CONFIGURE_AUTH",function(){return _}),n.d(t,"showDefinitions",function(){return w}),n.d(t,"authorize",function(){return x}),n.d(t,"logout",function(){return E}),n.d(t,"preAuthorizeImplicit",function(){return S}),n.d(t,"authorizeOauth2",function(){return C}),n.d(t,"authorizePassword",function(){return k}),n.d(t,"authorizeApplication",function(){return O}),n.d(t,"authorizeAccessCodeWithFormParams",function(){return A}),n.d(t,"authorizeAccessCodeWithBasicAuthentication",function(){return T}),n.d(t,"authorizeRequest",function(){return j}),n.d(t,"configureAuth",function(){return P});var r=n(26),o=n.n(r),i=n(16),a=n.n(i),s=n(28),u=n.n(s),c=n(95),l=n.n(c),p=n(18),f=n.n(p),h=n(3),d="show_popup",m="authorize",v="logout",g="pre_authorize_oauth2",y="authorize_oauth2",b="validate",_="configure_auth";function w(e){return{type:d,payload:e}}function x(e){return{type:m,payload:e}}function E(e){return{type:v,payload:e}}var S=function(e){return function(t){var n=t.authActions,r=t.errActions,o=e.auth,i=e.token,a=e.isValid,s=o.schema,c=o.name,l=s.get("flow");delete f.a.swaggerUIRedirectOauth2,"accessCode"===l||a||r.newAuthErr({authId:c,source:"auth",level:"warning",message:"Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"}),i.error?r.newAuthErr({authId:c,source:"auth",level:"error",message:u()(i)}):n.authorizeOauth2({auth:o,token:i})}};function C(e){return{type:y,payload:e}}var k=function(e){return function(t){var n=t.authActions,r=e.schema,o=e.name,i=e.username,s=e.password,u=e.passwordType,c=e.clientId,l=e.clientSecret,p={grant_type:"password",scope:e.scopes.join(" "),username:i,password:s},f={};switch(u){case"request-body":!function(e,t,n){t&&a()(e,{client_id:t});n&&a()(e,{client_secret:n})}(p,c,l);break;case"basic":f.Authorization="Basic "+Object(h.a)(c+":"+l);break;default:console.warn("Warning: invalid passwordType ".concat(u," was passed, not including client id and secret"))}return n.authorizeRequest({body:Object(h.b)(p),url:r.get("tokenUrl"),name:o,headers:f,query:{},auth:e})}};var O=function(e){return function(t){var n=t.authActions,r=e.schema,o=e.scopes,i=e.name,a=e.clientId,s=e.clientSecret,u={Authorization:"Basic "+Object(h.a)(a+":"+s)},c={grant_type:"client_credentials",scope:o.join(" ")};return n.authorizeRequest({body:Object(h.b)(c),name:i,url:r.get("tokenUrl"),auth:e,headers:u})}},A=function(e){var t=e.auth,n=e.redirectUrl;return function(e){var r=e.authActions,o=t.schema,i=t.name,a=t.clientId,s=t.clientSecret,u=t.codeVerifier,c={grant_type:"authorization_code",code:t.code,client_id:a,client_secret:s,redirect_uri:n,code_verifier:u};return r.authorizeRequest({body:Object(h.b)(c),name:i,url:o.get("tokenUrl"),auth:t})}},T=function(e){var t=e.auth,n=e.redirectUrl;return function(e){var r=e.authActions,o=t.schema,i=t.name,a=t.clientId,s=t.clientSecret,u={Authorization:"Basic "+Object(h.a)(a+":"+s)},c={grant_type:"authorization_code",code:t.code,client_id:a,redirect_uri:n};return r.authorizeRequest({body:Object(h.b)(c),name:i,url:o.get("tokenUrl"),auth:t,headers:u})}},j=function(e){return function(t){var n,r=t.fn,i=t.getConfigs,s=t.authActions,c=t.errActions,p=t.oas3Selectors,f=t.specSelectors,h=t.authSelectors,d=e.body,m=e.query,v=void 0===m?{}:m,g=e.headers,y=void 0===g?{}:g,b=e.name,_=e.url,w=e.auth,x=(h.getConfigs()||{}).additionalQueryStringParams;n=f.isOAS3()?l()(_,p.selectedServer(),!0):l()(_,f.url(),!0),"object"===o()(x)&&(n.query=a()({},n.query,x));var E=n.toString(),S=a()({Accept:"application/json, text/plain, */*","Content-Type":"application/x-www-form-urlencoded","X-Requested-With":"XMLHttpRequest"},y);r.fetch({url:E,method:"post",headers:S,query:v,body:d,requestInterceptor:i().requestInterceptor,responseInterceptor:i().responseInterceptor}).then(function(e){var t=JSON.parse(e.data),n=t&&(t.error||""),r=t&&(t.parseError||"");e.ok?n||r?c.newAuthErr({authId:b,level:"error",source:"auth",message:u()(t)}):s.authorizeOauth2({auth:w,token:t}):c.newAuthErr({authId:b,level:"error",source:"auth",message:e.statusText})}).catch(function(e){var t=new Error(e).message;if(e.response&&e.response.data){var n=e.response.data;try{var r="string"==typeof n?JSON.parse(n):n;r.error&&(t+=", error: ".concat(r.error)),r.error_description&&(t+=", description: ".concat(r.error_description))}catch(e){}}c.newAuthErr({authId:b,level:"error",source:"auth",message:t})})}};function P(e){return{type:_,payload:e}}},function(e,t){var n=e.exports={version:"2.6.5"};"number"==typeof __e&&(__e=n)},function(e,t){e.exports=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e}},function(e,t,n){var r=n(127),o=Math.min;e.exports=function(e){return e>0?o(r(e),9007199254740991):0}},function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},function(e,t,n){var r=n(211),o=n(210);e.exports=function(e){return r(o(e))}},function(e,t,n){var r=n(49),o=n(133);e.exports=n(50)?function(e,t,n){return r.f(e,t,o(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t,n){"use strict";e.exports=function(e){if("function"!=typeof e)throw new TypeError(e+" is not a function");return e}},function(e,t,n){"use strict";n.r(t),n.d(t,"UPDATE_LAYOUT",function(){return o}),n.d(t,"UPDATE_FILTER",function(){return i}),n.d(t,"UPDATE_MODE",function(){return a}),n.d(t,"SHOW",function(){return s}),n.d(t,"updateLayout",function(){return u}),n.d(t,"updateFilter",function(){return c}),n.d(t,"show",function(){return l}),n.d(t,"changeMode",function(){return p});var r=n(3),o="layout_update_layout",i="layout_update_filter",a="layout_update_mode",s="layout_show";function u(e){return{type:o,payload:e}}function c(e){return{type:i,payload:e}}function l(e){var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];return e=Object(r.w)(e),{type:s,payload:{thing:e,shown:t}}}function p(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return e=Object(r.w)(e),{type:a,payload:{thing:e,mode:t}}}},function(e,t,n){"use strict";(function(t){ +/*! + * @description Recursive object extending + * @author Viacheslav Lotsmanov + * @license MIT + * + * The MIT License (MIT) + * + * Copyright (c) 2013-2018 Viacheslav Lotsmanov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +function n(e){return e instanceof t||e instanceof Date||e instanceof RegExp}function r(e){if(e instanceof t){var n=t.alloc?t.alloc(e.length):new t(e.length);return e.copy(n),n}if(e instanceof Date)return new Date(e.getTime());if(e instanceof RegExp)return new RegExp(e);throw new Error("Unexpected situation")}function o(e){var t=[];return e.forEach(function(e,i){"object"==typeof e&&null!==e?Array.isArray(e)?t[i]=o(e):n(e)?t[i]=r(e):t[i]=a({},e):t[i]=e}),t}function i(e,t){return"__proto__"===t?void 0:e[t]}var a=e.exports=function(){if(arguments.length<1||"object"!=typeof arguments[0])return!1;if(arguments.length<2)return arguments[0];var e,t,s=arguments[0],u=Array.prototype.slice.call(arguments,1);return u.forEach(function(u){"object"!=typeof u||null===u||Array.isArray(u)||Object.keys(u).forEach(function(c){return t=i(s,c),(e=i(u,c))===s?void 0:"object"!=typeof e||null===e?void(s[c]=e):Array.isArray(e)?void(s[c]=o(e)):n(e)?void(s[c]=r(e)):"object"!=typeof t||null===t||Array.isArray(t)?void(s[c]=a({},e)):void(s[c]=a(t,e))})}),s}}).call(this,n(64).Buffer)},function(e,t,n){var r=n(151),o=n(336);e.exports=n(126)?function(e,t,n){return r.f(e,t,o(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,t,n){var r=n(106),o=n(603),i=n(604),a="[object Null]",s="[object Undefined]",u=r?r.toStringTag:void 0;e.exports=function(e){return null==e?void 0===e?s:a:u&&u in Object(e)?o(e):i(e)}},function(e,t,n){var r=n(621),o=n(624);e.exports=function(e,t){var n=o(e,t);return r(n)?n:void 0}},function(e,t,n){var r=n(380),o=n(661),i=n(107);e.exports=function(e){return i(e)?r(e):o(e)}},function(e,t,n){"use strict";var r=n(178),o=Object.keys||function(e){var t=[];for(var n in e)t.push(n);return t};e.exports=p;var i=n(137);i.inherits=n(47);var a=n(390),s=n(240);i.inherits(p,a);for(var u=o(s.prototype),c=0;c=t.length?{value:void 0,done:!0}:(e=r(t,n),this._i+=e.length,{value:e,done:!1})})},function(e,t){e.exports={}},function(e,t,n){n(561);for(var r=n(32),o=n(77),i=n(102),a=n(34)("toStringTag"),s="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),u=0;u1){for(var d=Array(h),m=0;m1){for(var g=Array(v),y=0;y=this._finalSize&&(this._update(this._block),this._block.fill(0));var n=8*this._len;if(n<=4294967295)this._block.writeUInt32BE(n,this._blockSize-4);else{var r=(4294967295&n)>>>0,o=(n-r)/4294967296;this._block.writeUInt32BE(o,this._blockSize-8),this._block.writeUInt32BE(r,this._blockSize-4)}this._update(this._block);var i=this._hash();return e?i.toString(e):i},o.prototype._update=function(){throw new Error("_update must be implemented by subclass")},e.exports=o},function(e,t,n){var r=n(63),o=n(406),i=n(407),a=n(46),s=n(158),u=n(225),c={},l={};(t=e.exports=function(e,t,n,p,f){var h,d,m,v,g=f?function(){return e}:u(e),y=r(n,p,t?2:1),b=0;if("function"!=typeof g)throw TypeError(e+" is not iterable!");if(i(g)){for(h=s(e.length);h>b;b++)if((v=t?y(a(d=e[b])[0],d[1]):y(e[b]))===c||v===l)return v}else for(m=g.call(e);!(d=m.next()).done;)if((v=o(m,y,d.value,t))===c||v===l)return v}).BREAK=c,t.RETURN=l},function(e,t,n){"use strict";function r(e){return null==e}e.exports.isNothing=r,e.exports.isObject=function(e){return"object"==typeof e&&null!==e},e.exports.toArray=function(e){return Array.isArray(e)?e:r(e)?[]:[e]},e.exports.repeat=function(e,t){var n,r="";for(n=0;n1&&void 0!==arguments[1]?arguments[1]:{},r=Object(i.A)(t),a=r.type,s=r.example,u=r.properties,c=r.additionalProperties,l=r.items,p=n.includeReadOnly,f=n.includeWriteOnly;if(void 0!==s)return Object(i.e)(s,"$$ref",function(e){return"string"==typeof e&&e.indexOf("#")>-1});if(!a)if(u)a="object";else{if(!l)return;a="array"}if("object"===a){var d=Object(i.A)(u),m={};for(var v in d)d[v]&&d[v].deprecated||d[v]&&d[v].readOnly&&!p||d[v]&&d[v].writeOnly&&!f||(m[v]=e(d[v],n));if(!0===c)m.additionalProp1={};else if(c)for(var g=Object(i.A)(c),y=e(g,n),b=1;b<4;b++)m["additionalProp"+b]=y;return m}return"array"===a?o()(l.anyOf)?l.anyOf.map(function(t){return e(t,n)}):o()(l.oneOf)?l.oneOf.map(function(t){return e(t,n)}):[e(l,n)]:t.enum?t.default?t.default:Object(i.w)(t.enum)[0]:"file"!==a?h(t):void 0},m=function(e){return e.schema&&(e=e.schema),e.properties&&(e.type="object"),e},v=function e(t){var n,r,a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},s=p()({},Object(i.A)(t)),u=s.type,c=s.properties,l=s.additionalProperties,f=s.items,d=s.example,m=a.includeReadOnly,v=a.includeWriteOnly,g=s.default,y={},b={},_=t.xml,w=_.name,x=_.prefix,E=_.namespace,S=s.enum;if(!u)if(c||l)u="object";else{if(!f)return;u="array"}if(n=(x?x+":":"")+(w=w||"notagname"),E){var C=x?"xmlns:"+x:"xmlns";b[C]=E}if("array"===u&&f){if(f.xml=f.xml||_||{},f.xml.name=f.xml.name||_.name,_.wrapped)return y[n]=[],o()(d)?d.forEach(function(t){f.example=t,y[n].push(e(f,a))}):o()(g)?g.forEach(function(t){f.default=t,y[n].push(e(f,a))}):y[n]=[e(f,a)],b&&y[n].push({_attr:b}),y;var k=[];return o()(d)?(d.forEach(function(t){f.example=t,k.push(e(f,a))}),k):o()(g)?(g.forEach(function(t){f.default=t,k.push(e(f,a))}),k):e(f,a)}if("object"===u){var O=Object(i.A)(c);for(var A in y[n]=[],d=d||{},O)if(O.hasOwnProperty(A)&&(!O[A].readOnly||m)&&(!O[A].writeOnly||v))if(O[A].xml=O[A].xml||{},O[A].xml.attribute){var T=o()(O[A].enum)&&O[A].enum[0],j=O[A].example,P=O[A].default;b[O[A].xml.name||A]=void 0!==j&&j||void 0!==d[A]&&d[A]||void 0!==P&&P||T||h(O[A])}else{O[A].xml.name=O[A].xml.name||A,void 0===O[A].example&&void 0!==d[A]&&(O[A].example=d[A]);var I=e(O[A]);o()(I)?y[n]=y[n].concat(I):y[n].push(I)}return!0===l?y[n].push({additionalProp:"Anything can be here"}):l&&y[n].push({additionalProp:h(l)}),b&&y[n].push({_attr:b}),y}return r=void 0!==d?d:void 0!==g?g:o()(S)?S[0]:h(t),y[n]=b?[{_attr:b},r]:r,y};function g(e,t){var n=v(e,t);if(n)return s()(n,{declaration:!0,indent:"\t"})}var y=c()(g),b=c()(d)},function(e,t,n){"use strict";n.r(t),n.d(t,"UPDATE_CONFIGS",function(){return i}),n.d(t,"TOGGLE_CONFIGS",function(){return a}),n.d(t,"update",function(){return s}),n.d(t,"toggle",function(){return u}),n.d(t,"loaded",function(){return c});var r=n(2),o=n.n(r),i="configs_update",a="configs_toggle";function s(e,t){return{type:i,payload:o()({},e,t)}}function u(e){return{type:a,payload:e}}var c=function(){return function(){}}},function(e,t,n){"use strict";n.d(t,"a",function(){return a});var r=n(1),o=n.n(r),i=o.a.Set.of("type","format","items","default","maximum","exclusiveMaximum","minimum","exclusiveMinimum","maxLength","minLength","pattern","maxItems","minItems","uniqueItems","enum","multipleOf");function a(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).isOAS3;if(!o.a.Map.isMap(e))return{schema:o.a.Map(),parameterContentMediaType:null};if(!t)return"body"===e.get("in")?{schema:e.get("schema",o.a.Map()),parameterContentMediaType:null}:{schema:e.filter(function(e,t){return i.includes(t)}),parameterContentMediaType:null};if(e.get("content")){var n=e.get("content",o.a.Map({})).keySeq().first();return{schema:e.getIn(["content",n,"schema"],o.a.Map()),parameterContentMediaType:n}}return{schema:e.get("schema",o.a.Map()),parameterContentMediaType:null}}},function(e,t,n){e.exports=n(781)},function(e,t,n){"use strict";n.r(t);var r=n(469),o="object"==typeof self&&self&&self.Object===Object&&self,i=(r.a||o||Function("return this")()).Symbol,a=Object.prototype,s=a.hasOwnProperty,u=a.toString,c=i?i.toStringTag:void 0;var l=function(e){var t=s.call(e,c),n=e[c];try{e[c]=void 0;var r=!0}catch(e){}var o=u.call(e);return r&&(t?e[c]=n:delete e[c]),o},p=Object.prototype.toString;var f=function(e){return p.call(e)},h="[object Null]",d="[object Undefined]",m=i?i.toStringTag:void 0;var v=function(e){return null==e?void 0===e?d:h:m&&m in Object(e)?l(e):f(e)};var g=function(e,t){return function(n){return e(t(n))}}(Object.getPrototypeOf,Object);var y=function(e){return null!=e&&"object"==typeof e},b="[object Object]",_=Function.prototype,w=Object.prototype,x=_.toString,E=w.hasOwnProperty,S=x.call(Object);var C=function(e){if(!y(e)||v(e)!=b)return!1;var t=g(e);if(null===t)return!0;var n=E.call(t,"constructor")&&t.constructor;return"function"==typeof n&&n instanceof n&&x.call(n)==S},k=n(330),O={INIT:"@@redux/INIT"};function A(e,t,n){var r;if("function"==typeof t&&void 0===n&&(n=t,t=void 0),void 0!==n){if("function"!=typeof n)throw new Error("Expected the enhancer to be a function.");return n(A)(e,t)}if("function"!=typeof e)throw new Error("Expected the reducer to be a function.");var o=e,i=t,a=[],s=a,u=!1;function c(){s===a&&(s=a.slice())}function l(){return i}function p(e){if("function"!=typeof e)throw new Error("Expected listener to be a function.");var t=!0;return c(),s.push(e),function(){if(t){t=!1,c();var n=s.indexOf(e);s.splice(n,1)}}}function f(e){if(!C(e))throw new Error("Actions must be plain objects. Use custom middleware for async actions.");if(void 0===e.type)throw new Error('Actions may not have an undefined "type" property. Have you misspelled a constant?');if(u)throw new Error("Reducers may not dispatch actions.");try{u=!0,i=o(i,e)}finally{u=!1}for(var t=a=s,n=0;n0&&void 0!==arguments[0]?arguments[0]:{},t=arguments[1];if(a)throw a;for(var r=!1,o={},s=0;s0?r:n)(e)}},function(e,t){e.exports={}},function(e,t,n){var r=n(348),o=n(215);e.exports=Object.keys||function(e){return r(e,o)}},function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e,t){e.exports=!0},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},function(e,t,n){var r=n(49).f,o=n(75),i=n(34)("toStringTag");e.exports=function(e,t,n){e&&!o(e=n?e:e.prototype,i)&&r(e,i,{configurable:!0,value:t})}},function(e,t,n){var r=n(159)("meta"),o=n(43),i=n(75),a=n(49).f,s=0,u=Object.isExtensible||function(){return!0},c=!n(82)(function(){return u(Object.preventExtensions({}))}),l=function(e){a(e,r,{value:{i:"O"+ ++s,w:{}}})},p=e.exports={KEY:r,NEED:!1,fastKey:function(e,t){if(!o(e))return"symbol"==typeof e?e:("string"==typeof e?"S":"P")+e;if(!i(e,r)){if(!u(e))return"F";if(!t)return"E";l(e)}return e[r].i},getWeak:function(e,t){if(!i(e,r)){if(!u(e))return!0;if(!t)return!1;l(e)}return e[r].w},onFreeze:function(e){return c&&p.NEED&&u(e)&&!i(e,r)&&l(e),e}}},function(e,t,n){"use strict";e.exports=function(e){for(var t=arguments.length-1,n="Minified React error #"+e+"; visit http://facebook.github.io/react/docs/error-decoder.html?invariant="+e,r=0;r1&&void 0!==arguments[1]?arguments[1]:[],n={arrayBehaviour:(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).arrayBehaviour||"replace"},r=t.map(function(e){return e||{}}),i=e||{},c=0;c1?t-1:0),r=1;r")}),p=function(){var e=/(?:)/,t=e.exec;e.exec=function(){return t.apply(this,arguments)};var n="ab".split(e);return 2===n.length&&"a"===n[0]&&"b"===n[1]}();e.exports=function(e,t,n){var f=s(e),h=!i(function(){var t={};return t[f]=function(){return 7},7!=""[e](t)}),d=h?!i(function(){var t=!1,n=/a/;return n.exec=function(){return t=!0,null},"split"===e&&(n.constructor={},n.constructor[c]=function(){return n}),n[f](""),!t}):void 0;if(!h||!d||"replace"===e&&!l||"split"===e&&!p){var m=/./[f],v=n(a,f,""[e],function(e,t,n,r,o){return t.exec===u?h&&!o?{done:!0,value:m.call(t,n,r)}:{done:!0,value:e.call(n,t,r)}:{done:!1}}),g=v[0],y=v[1];r(String.prototype,e,g),o(RegExp.prototype,f,2==t?function(e,t){return y.call(e,this,t)}:function(e){return y.call(e,this)})}}},function(e,t,n){var r=n(212),o=Math.min;e.exports=function(e){return e>0?o(r(e),9007199254740991):0}},function(e,t){var n=0,r=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++n+r).toString(36))}},function(e,t,n){var r=n(46),o=n(350),i=n(215),a=n(213)("IE_PROTO"),s=function(){},u=function(){var e,t=n(217)("iframe"),r=i.length;for(t.style.display="none",n(351).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write(" + + + + diff --git a/swagger-ui/oauth2-redirect.html b/swagger-ui/oauth2-redirect.html new file mode 100644 index 00000000..a013fc82 --- /dev/null +++ b/swagger-ui/oauth2-redirect.html @@ -0,0 +1,68 @@ + + +Swagger UI: OAuth2 Redirect + + + + diff --git a/swagger-ui/swagger-ui-bundle.js b/swagger-ui/swagger-ui-bundle.js new file mode 100644 index 00000000..559df3ca --- /dev/null +++ b/swagger-ui/swagger-ui-bundle.js @@ -0,0 +1,134 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(function(){try{return require("esprima")}catch(e){}}()):"function"==typeof define&&define.amd?define(["esprima"],t):"object"==typeof exports?exports.SwaggerUIBundle=t(function(){try{return require("esprima")}catch(e){}}()):e.SwaggerUIBundle=t(e.esprima)}(window,function(e){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist",n(n.s=488)}([function(e,t,n){"use strict";e.exports=n(104)},function(e,t,n){e.exports=function(){"use strict";var e=Array.prototype.slice;function t(e,t){t&&(e.prototype=Object.create(t.prototype)),e.prototype.constructor=e}function n(e){return a(e)?e:J(e)}function r(e){return s(e)?e:K(e)}function o(e){return u(e)?e:Y(e)}function i(e){return a(e)&&!c(e)?e:$(e)}function a(e){return!(!e||!e[p])}function s(e){return!(!e||!e[f])}function u(e){return!(!e||!e[h])}function c(e){return s(e)||u(e)}function l(e){return!(!e||!e[d])}t(r,n),t(o,n),t(i,n),n.isIterable=a,n.isKeyed=s,n.isIndexed=u,n.isAssociative=c,n.isOrdered=l,n.Keyed=r,n.Indexed=o,n.Set=i;var p="@@__IMMUTABLE_ITERABLE__@@",f="@@__IMMUTABLE_KEYED__@@",h="@@__IMMUTABLE_INDEXED__@@",d="@@__IMMUTABLE_ORDERED__@@",m=5,v=1<>>0;if(""+n!==t||4294967295===n)return NaN;t=n}return t<0?C(e)+t:t}function O(){return!0}function A(e,t,n){return(0===e||void 0!==n&&e<=-n)&&(void 0===t||void 0!==n&&t>=n)}function T(e,t){return P(e,t,0)}function j(e,t){return P(e,t,t)}function P(e,t,n){return void 0===e?n:e<0?Math.max(0,t+e):void 0===t?e:Math.min(t,e)}var I=0,M=1,N=2,R="function"==typeof Symbol&&Symbol.iterator,D="@@iterator",L=R||D;function U(e){this.next=e}function q(e,t,n,r){var o=0===e?t:1===e?n:[t,n];return r?r.value=o:r={value:o,done:!1},r}function F(){return{value:void 0,done:!0}}function B(e){return!!H(e)}function z(e){return e&&"function"==typeof e.next}function V(e){var t=H(e);return t&&t.call(e)}function H(e){var t=e&&(R&&e[R]||e[D]);if("function"==typeof t)return t}function W(e){return e&&"number"==typeof e.length}function J(e){return null==e?ie():a(e)?e.toSeq():function(e){var t=ue(e)||"object"==typeof e&&new te(e);if(!t)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+e);return t}(e)}function K(e){return null==e?ie().toKeyedSeq():a(e)?s(e)?e.toSeq():e.fromEntrySeq():ae(e)}function Y(e){return null==e?ie():a(e)?s(e)?e.entrySeq():e.toIndexedSeq():se(e)}function $(e){return(null==e?ie():a(e)?s(e)?e.entrySeq():e:se(e)).toSetSeq()}U.prototype.toString=function(){return"[Iterator]"},U.KEYS=I,U.VALUES=M,U.ENTRIES=N,U.prototype.inspect=U.prototype.toSource=function(){return this.toString()},U.prototype[L]=function(){return this},t(J,n),J.of=function(){return J(arguments)},J.prototype.toSeq=function(){return this},J.prototype.toString=function(){return this.__toString("Seq {","}")},J.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},J.prototype.__iterate=function(e,t){return ce(this,e,t,!0)},J.prototype.__iterator=function(e,t){return le(this,e,t,!0)},t(K,J),K.prototype.toKeyedSeq=function(){return this},t(Y,J),Y.of=function(){return Y(arguments)},Y.prototype.toIndexedSeq=function(){return this},Y.prototype.toString=function(){return this.__toString("Seq [","]")},Y.prototype.__iterate=function(e,t){return ce(this,e,t,!1)},Y.prototype.__iterator=function(e,t){return le(this,e,t,!1)},t($,J),$.of=function(){return $(arguments)},$.prototype.toSetSeq=function(){return this},J.isSeq=oe,J.Keyed=K,J.Set=$,J.Indexed=Y;var G,Z,X,Q="@@__IMMUTABLE_SEQ__@@";function ee(e){this._array=e,this.size=e.length}function te(e){var t=Object.keys(e);this._object=e,this._keys=t,this.size=t.length}function ne(e){this._iterable=e,this.size=e.length||e.size}function re(e){this._iterator=e,this._iteratorCache=[]}function oe(e){return!(!e||!e[Q])}function ie(){return G||(G=new ee([]))}function ae(e){var t=Array.isArray(e)?new ee(e).fromEntrySeq():z(e)?new re(e).fromEntrySeq():B(e)?new ne(e).fromEntrySeq():"object"==typeof e?new te(e):void 0;if(!t)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+e);return t}function se(e){var t=ue(e);if(!t)throw new TypeError("Expected Array or iterable object of values: "+e);return t}function ue(e){return W(e)?new ee(e):z(e)?new re(e):B(e)?new ne(e):void 0}function ce(e,t,n,r){var o=e._cache;if(o){for(var i=o.length-1,a=0;a<=i;a++){var s=o[n?i-a:a];if(!1===t(s[1],r?s[0]:a,e))return a+1}return a}return e.__iterateUncached(t,n)}function le(e,t,n,r){var o=e._cache;if(o){var i=o.length-1,a=0;return new U(function(){var e=o[n?i-a:a];return a++>i?{value:void 0,done:!0}:q(t,r?e[0]:a-1,e[1])})}return e.__iteratorUncached(t,n)}function pe(e,t){return t?function e(t,n,r,o){return Array.isArray(n)?t.call(o,r,Y(n).map(function(r,o){return e(t,r,o,n)})):he(n)?t.call(o,r,K(n).map(function(r,o){return e(t,r,o,n)})):n}(t,e,"",{"":e}):fe(e)}function fe(e){return Array.isArray(e)?Y(e).map(fe).toList():he(e)?K(e).map(fe).toMap():e}function he(e){return e&&(e.constructor===Object||void 0===e.constructor)}function de(e,t){if(e===t||e!=e&&t!=t)return!0;if(!e||!t)return!1;if("function"==typeof e.valueOf&&"function"==typeof t.valueOf){if((e=e.valueOf())===(t=t.valueOf())||e!=e&&t!=t)return!0;if(!e||!t)return!1}return!("function"!=typeof e.equals||"function"!=typeof t.equals||!e.equals(t))}function me(e,t){if(e===t)return!0;if(!a(t)||void 0!==e.size&&void 0!==t.size&&e.size!==t.size||void 0!==e.__hash&&void 0!==t.__hash&&e.__hash!==t.__hash||s(e)!==s(t)||u(e)!==u(t)||l(e)!==l(t))return!1;if(0===e.size&&0===t.size)return!0;var n=!c(e);if(l(e)){var r=e.entries();return t.every(function(e,t){var o=r.next().value;return o&&de(o[1],e)&&(n||de(o[0],t))})&&r.next().done}var o=!1;if(void 0===e.size)if(void 0===t.size)"function"==typeof e.cacheResult&&e.cacheResult();else{o=!0;var i=e;e=t,t=i}var p=!0,f=t.__iterate(function(t,r){if(n?!e.has(t):o?!de(t,e.get(r,y)):!de(e.get(r,y),t))return p=!1,!1});return p&&e.size===f}function ve(e,t){if(!(this instanceof ve))return new ve(e,t);if(this._value=e,this.size=void 0===t?1/0:Math.max(0,t),0===this.size){if(Z)return Z;Z=this}}function ge(e,t){if(!e)throw new Error(t)}function ye(e,t,n){if(!(this instanceof ye))return new ye(e,t,n);if(ge(0!==n,"Cannot step a Range by 0"),e=e||0,void 0===t&&(t=1/0),n=void 0===n?1:Math.abs(n),tr?{value:void 0,done:!0}:q(e,o,n[t?r-o++:o++])})},t(te,K),te.prototype.get=function(e,t){return void 0===t||this.has(e)?this._object[e]:t},te.prototype.has=function(e){return this._object.hasOwnProperty(e)},te.prototype.__iterate=function(e,t){for(var n=this._object,r=this._keys,o=r.length-1,i=0;i<=o;i++){var a=r[t?o-i:i];if(!1===e(n[a],a,this))return i+1}return i},te.prototype.__iterator=function(e,t){var n=this._object,r=this._keys,o=r.length-1,i=0;return new U(function(){var a=r[t?o-i:i];return i++>o?{value:void 0,done:!0}:q(e,a,n[a])})},te.prototype[d]=!0,t(ne,Y),ne.prototype.__iterateUncached=function(e,t){if(t)return this.cacheResult().__iterate(e,t);var n=V(this._iterable),r=0;if(z(n))for(var o;!(o=n.next()).done&&!1!==e(o.value,r++,this););return r},ne.prototype.__iteratorUncached=function(e,t){if(t)return this.cacheResult().__iterator(e,t);var n=V(this._iterable);if(!z(n))return new U(F);var r=0;return new U(function(){var t=n.next();return t.done?t:q(e,r++,t.value)})},t(re,Y),re.prototype.__iterateUncached=function(e,t){if(t)return this.cacheResult().__iterate(e,t);for(var n,r=this._iterator,o=this._iteratorCache,i=0;i=r.length){var t=n.next();if(t.done)return t;r[o]=t.value}return q(e,o,r[o++])})},t(ve,Y),ve.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},ve.prototype.get=function(e,t){return this.has(e)?this._value:t},ve.prototype.includes=function(e){return de(this._value,e)},ve.prototype.slice=function(e,t){var n=this.size;return A(e,t,n)?this:new ve(this._value,j(t,n)-T(e,n))},ve.prototype.reverse=function(){return this},ve.prototype.indexOf=function(e){return de(this._value,e)?0:-1},ve.prototype.lastIndexOf=function(e){return de(this._value,e)?this.size:-1},ve.prototype.__iterate=function(e,t){for(var n=0;n=0&&t=0&&nn?{value:void 0,done:!0}:q(e,i++,a)})},ye.prototype.equals=function(e){return e instanceof ye?this._start===e._start&&this._end===e._end&&this._step===e._step:me(this,e)},t(be,n),t(_e,be),t(we,be),t(xe,be),be.Keyed=_e,be.Indexed=we,be.Set=xe;var Ee="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function(e,t){var n=65535&(e|=0),r=65535&(t|=0);return n*r+((e>>>16)*r+n*(t>>>16)<<16>>>0)|0};function Se(e){return e>>>1&1073741824|3221225471&e}function Ce(e){if(!1===e||null==e)return 0;if("function"==typeof e.valueOf&&(!1===(e=e.valueOf())||null==e))return 0;if(!0===e)return 1;var t=typeof e;if("number"===t){if(e!=e||e===1/0)return 0;var n=0|e;for(n!==e&&(n^=4294967295*e);e>4294967295;)n^=e/=4294967295;return Se(n)}if("string"===t)return e.length>Me?function(e){var t=De[e];return void 0===t&&(t=ke(e),Re===Ne&&(Re=0,De={}),Re++,De[e]=t),t}(e):ke(e);if("function"==typeof e.hashCode)return e.hashCode();if("object"===t)return function(e){var t;if(je&&void 0!==(t=Oe.get(e)))return t;if(void 0!==(t=e[Ie]))return t;if(!Te){if(void 0!==(t=e.propertyIsEnumerable&&e.propertyIsEnumerable[Ie]))return t;if(void 0!==(t=function(e){if(e&&e.nodeType>0)switch(e.nodeType){case 1:return e.uniqueID;case 9:return e.documentElement&&e.documentElement.uniqueID}}(e)))return t}if(t=++Pe,1073741824&Pe&&(Pe=0),je)Oe.set(e,t);else{if(void 0!==Ae&&!1===Ae(e))throw new Error("Non-extensible objects are not allowed as keys.");if(Te)Object.defineProperty(e,Ie,{enumerable:!1,configurable:!1,writable:!1,value:t});else if(void 0!==e.propertyIsEnumerable&&e.propertyIsEnumerable===e.constructor.prototype.propertyIsEnumerable)e.propertyIsEnumerable=function(){return this.constructor.prototype.propertyIsEnumerable.apply(this,arguments)},e.propertyIsEnumerable[Ie]=t;else{if(void 0===e.nodeType)throw new Error("Unable to set a non-enumerable property on object.");e[Ie]=t}}return t}(e);if("function"==typeof e.toString)return ke(e.toString());throw new Error("Value type "+t+" cannot be hashed.")}function ke(e){for(var t=0,n=0;n=t.length)throw new Error("Missing value for key: "+t[n]);e.set(t[n],t[n+1])}})},Ue.prototype.toString=function(){return this.__toString("Map {","}")},Ue.prototype.get=function(e,t){return this._root?this._root.get(0,void 0,e,t):t},Ue.prototype.set=function(e,t){return Qe(this,e,t)},Ue.prototype.setIn=function(e,t){return this.updateIn(e,y,function(){return t})},Ue.prototype.remove=function(e){return Qe(this,e,y)},Ue.prototype.deleteIn=function(e){return this.updateIn(e,function(){return y})},Ue.prototype.update=function(e,t,n){return 1===arguments.length?e(this):this.updateIn([e],t,n)},Ue.prototype.updateIn=function(e,t,n){n||(n=t,t=void 0);var r=function e(t,n,r,o){var i=t===y,a=n.next();if(a.done){var s=i?r:t,u=o(s);return u===s?t:u}ge(i||t&&t.set,"invalid keyPath");var c=a.value,l=i?y:t.get(c,y),p=e(l,n,r,o);return p===l?t:p===y?t.remove(c):(i?Xe():t).set(c,p)}(this,rn(e),t,n);return r===y?void 0:r},Ue.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):Xe()},Ue.prototype.merge=function(){return rt(this,void 0,arguments)},Ue.prototype.mergeWith=function(t){var n=e.call(arguments,1);return rt(this,t,n)},Ue.prototype.mergeIn=function(t){var n=e.call(arguments,1);return this.updateIn(t,Xe(),function(e){return"function"==typeof e.merge?e.merge.apply(e,n):n[n.length-1]})},Ue.prototype.mergeDeep=function(){return rt(this,ot,arguments)},Ue.prototype.mergeDeepWith=function(t){var n=e.call(arguments,1);return rt(this,it(t),n)},Ue.prototype.mergeDeepIn=function(t){var n=e.call(arguments,1);return this.updateIn(t,Xe(),function(e){return"function"==typeof e.mergeDeep?e.mergeDeep.apply(e,n):n[n.length-1]})},Ue.prototype.sort=function(e){return Tt(Jt(this,e))},Ue.prototype.sortBy=function(e,t){return Tt(Jt(this,t,e))},Ue.prototype.withMutations=function(e){var t=this.asMutable();return e(t),t.wasAltered()?t.__ensureOwner(this.__ownerID):this},Ue.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new E)},Ue.prototype.asImmutable=function(){return this.__ensureOwner()},Ue.prototype.wasAltered=function(){return this.__altered},Ue.prototype.__iterator=function(e,t){return new Ye(this,e,t)},Ue.prototype.__iterate=function(e,t){var n=this,r=0;return this._root&&this._root.iterate(function(t){return r++,e(t[1],t[0],n)},t),r},Ue.prototype.__ensureOwner=function(e){return e===this.__ownerID?this:e?Ze(this.size,this._root,e,this.__hash):(this.__ownerID=e,this.__altered=!1,this)},Ue.isMap=qe;var Fe,Be="@@__IMMUTABLE_MAP__@@",ze=Ue.prototype;function Ve(e,t){this.ownerID=e,this.entries=t}function He(e,t,n){this.ownerID=e,this.bitmap=t,this.nodes=n}function We(e,t,n){this.ownerID=e,this.count=t,this.nodes=n}function Je(e,t,n){this.ownerID=e,this.keyHash=t,this.entries=n}function Ke(e,t,n){this.ownerID=e,this.keyHash=t,this.entry=n}function Ye(e,t,n){this._type=t,this._reverse=n,this._stack=e._root&&Ge(e._root)}function $e(e,t){return q(e,t[0],t[1])}function Ge(e,t){return{node:e,index:0,__prev:t}}function Ze(e,t,n,r){var o=Object.create(ze);return o.size=e,o._root=t,o.__ownerID=n,o.__hash=r,o.__altered=!1,o}function Xe(){return Fe||(Fe=Ze(0))}function Qe(e,t,n){var r,o;if(e._root){var i=w(b),a=w(_);if(r=et(e._root,e.__ownerID,0,void 0,t,n,i,a),!a.value)return e;o=e.size+(i.value?n===y?-1:1:0)}else{if(n===y)return e;o=1,r=new Ve(e.__ownerID,[[t,n]])}return e.__ownerID?(e.size=o,e._root=r,e.__hash=void 0,e.__altered=!0,e):r?Ze(o,r):Xe()}function et(e,t,n,r,o,i,a,s){return e?e.update(t,n,r,o,i,a,s):i===y?e:(x(s),x(a),new Ke(t,r,[o,i]))}function tt(e){return e.constructor===Ke||e.constructor===Je}function nt(e,t,n,r,o){if(e.keyHash===r)return new Je(t,r,[e.entry,o]);var i,a=(0===n?e.keyHash:e.keyHash>>>n)&g,s=(0===n?r:r>>>n)&g;return new He(t,1<>1&1431655765))+(e>>2&858993459))+(e>>4)&252645135,e+=e>>8,127&(e+=e>>16)}function ut(e,t,n,r){var o=r?e:S(e);return o[t]=n,o}ze[Be]=!0,ze.delete=ze.remove,ze.removeIn=ze.deleteIn,Ve.prototype.get=function(e,t,n,r){for(var o=this.entries,i=0,a=o.length;i=ct)return function(e,t,n,r){e||(e=new E);for(var o=new Ke(e,Ce(n),[n,r]),i=0;i>>e)&g),i=this.bitmap;return 0==(i&o)?r:this.nodes[st(i&o-1)].get(e+m,t,n,r)},He.prototype.update=function(e,t,n,r,o,i,a){void 0===n&&(n=Ce(r));var s=(0===t?n:n>>>t)&g,u=1<=lt)return function(e,t,n,r,o){for(var i=0,a=new Array(v),s=0;0!==n;s++,n>>>=1)a[s]=1&n?t[i++]:void 0;return a[r]=o,new We(e,i+1,a)}(e,f,c,s,d);if(l&&!d&&2===f.length&&tt(f[1^p]))return f[1^p];if(l&&d&&1===f.length&&tt(d))return d;var b=e&&e===this.ownerID,_=l?d?c:c^u:c|u,w=l?d?ut(f,p,d,b):function(e,t,n){var r=e.length-1;if(n&&t===r)return e.pop(),e;for(var o=new Array(r),i=0,a=0;a>>e)&g,i=this.nodes[o];return i?i.get(e+m,t,n,r):r},We.prototype.update=function(e,t,n,r,o,i,a){void 0===n&&(n=Ce(r));var s=(0===t?n:n>>>t)&g,u=o===y,c=this.nodes,l=c[s];if(u&&!l)return this;var p=et(l,e,t+m,n,r,o,i,a);if(p===l)return this;var f=this.count;if(l){if(!p&&--f0&&r=0&&e=e.size||t<0)return e.withMutations(function(e){t<0?kt(e,t).set(0,n):kt(e,0,t+1).set(t,n)});t+=e._origin;var r=e._tail,o=e._root,i=w(_);return t>=At(e._capacity)?r=Et(r,e.__ownerID,0,t,n,i):o=Et(o,e.__ownerID,e._level,t,n,i),i.value?e.__ownerID?(e._root=o,e._tail=r,e.__hash=void 0,e.__altered=!0,e):wt(e._origin,e._capacity,e._level,o,r):e}(this,e,t)},ft.prototype.remove=function(e){return this.has(e)?0===e?this.shift():e===this.size-1?this.pop():this.splice(e,1):this},ft.prototype.insert=function(e,t){return this.splice(e,0,t)},ft.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=this._origin=this._capacity=0,this._level=m,this._root=this._tail=null,this.__hash=void 0,this.__altered=!0,this):xt()},ft.prototype.push=function(){var e=arguments,t=this.size;return this.withMutations(function(n){kt(n,0,t+e.length);for(var r=0;r>>t&g;if(r>=this.array.length)return new vt([],e);var o,i=0===r;if(t>0){var a=this.array[r];if((o=a&&a.removeBefore(e,t-m,n))===a&&i)return this}if(i&&!o)return this;var s=St(this,e);if(!i)for(var u=0;u>>t&g;if(o>=this.array.length)return this;if(t>0){var i=this.array[o];if((r=i&&i.removeAfter(e,t-m,n))===i&&o===this.array.length-1)return this}var a=St(this,e);return a.array.splice(o+1),r&&(a.array[o]=r),a};var gt,yt,bt={};function _t(e,t){var n=e._origin,r=e._capacity,o=At(r),i=e._tail;return a(e._root,e._level,0);function a(e,s,u){return 0===s?function(e,a){var s=a===o?i&&i.array:e&&e.array,u=a>n?0:n-a,c=r-a;return c>v&&(c=v),function(){if(u===c)return bt;var e=t?--c:u++;return s&&s[e]}}(e,u):function(e,o,i){var s,u=e&&e.array,c=i>n?0:n-i>>o,l=1+(r-i>>o);return l>v&&(l=v),function(){for(;;){if(s){var e=s();if(e!==bt)return e;s=null}if(c===l)return bt;var n=t?--l:c++;s=a(u&&u[n],o-m,i+(n<>>n&g,u=e&&s0){var c=e&&e.array[s],l=Et(c,t,n-m,r,o,i);return l===c?e:((a=St(e,t)).array[s]=l,a)}return u&&e.array[s]===o?e:(x(i),a=St(e,t),void 0===o&&s===a.array.length-1?a.array.pop():a.array[s]=o,a)}function St(e,t){return t&&e&&t===e.ownerID?e:new vt(e?e.array.slice():[],t)}function Ct(e,t){if(t>=At(e._capacity))return e._tail;if(t<1<0;)n=n.array[t>>>r&g],r-=m;return n}}function kt(e,t,n){void 0!==t&&(t|=0),void 0!==n&&(n|=0);var r=e.__ownerID||new E,o=e._origin,i=e._capacity,a=o+t,s=void 0===n?i:n<0?i+n:o+n;if(a===o&&s===i)return e;if(a>=s)return e.clear();for(var u=e._level,c=e._root,l=0;a+l<0;)c=new vt(c&&c.array.length?[void 0,c]:[],r),l+=1<<(u+=m);l&&(a+=l,o+=l,s+=l,i+=l);for(var p=At(i),f=At(s);f>=1<p?new vt([],r):h;if(h&&f>p&&am;y-=m){var b=p>>>y&g;v=v.array[b]=St(v.array[b],r)}v.array[p>>>m&g]=h}if(s=f)a-=f,s-=f,u=m,c=null,d=d&&d.removeBefore(r,0,a);else if(a>o||f>>u&g;if(_!==f>>>u&g)break;_&&(l+=(1<o&&(c=c.removeBefore(r,u,a-l)),c&&fi&&(i=c.size),a(u)||(c=c.map(function(e){return pe(e)})),r.push(c)}return i>e.size&&(e=e.setSize(i)),at(e,t,r)}function At(e){return e>>m<=v&&a.size>=2*i.size?(r=(o=a.filter(function(e,t){return void 0!==e&&s!==t})).toKeyedSeq().map(function(e){return e[0]}).flip().toMap(),e.__ownerID&&(r.__ownerID=o.__ownerID=e.__ownerID)):(r=i.remove(t),o=s===a.size-1?a.pop():a.set(s,void 0))}else if(u){if(n===a.get(s)[1])return e;r=i,o=a.set(s,[t,n])}else r=i.set(t,a.size),o=a.set(a.size,[t,n]);return e.__ownerID?(e.size=r.size,e._map=r,e._list=o,e.__hash=void 0,e):Pt(r,o)}function Nt(e,t){this._iter=e,this._useKeys=t,this.size=e.size}function Rt(e){this._iter=e,this.size=e.size}function Dt(e){this._iter=e,this.size=e.size}function Lt(e){this._iter=e,this.size=e.size}function Ut(e){var t=en(e);return t._iter=e,t.size=e.size,t.flip=function(){return e},t.reverse=function(){var t=e.reverse.apply(this);return t.flip=function(){return e.reverse()},t},t.has=function(t){return e.includes(t)},t.includes=function(t){return e.has(t)},t.cacheResult=tn,t.__iterateUncached=function(t,n){var r=this;return e.__iterate(function(e,n){return!1!==t(n,e,r)},n)},t.__iteratorUncached=function(t,n){if(t===N){var r=e.__iterator(t,n);return new U(function(){var e=r.next();if(!e.done){var t=e.value[0];e.value[0]=e.value[1],e.value[1]=t}return e})}return e.__iterator(t===M?I:M,n)},t}function qt(e,t,n){var r=en(e);return r.size=e.size,r.has=function(t){return e.has(t)},r.get=function(r,o){var i=e.get(r,y);return i===y?o:t.call(n,i,r,e)},r.__iterateUncached=function(r,o){var i=this;return e.__iterate(function(e,o,a){return!1!==r(t.call(n,e,o,a),o,i)},o)},r.__iteratorUncached=function(r,o){var i=e.__iterator(N,o);return new U(function(){var o=i.next();if(o.done)return o;var a=o.value,s=a[0];return q(r,s,t.call(n,a[1],s,e),o)})},r}function Ft(e,t){var n=en(e);return n._iter=e,n.size=e.size,n.reverse=function(){return e},e.flip&&(n.flip=function(){var t=Ut(e);return t.reverse=function(){return e.flip()},t}),n.get=function(n,r){return e.get(t?n:-1-n,r)},n.has=function(n){return e.has(t?n:-1-n)},n.includes=function(t){return e.includes(t)},n.cacheResult=tn,n.__iterate=function(t,n){var r=this;return e.__iterate(function(e,n){return t(e,n,r)},!n)},n.__iterator=function(t,n){return e.__iterator(t,!n)},n}function Bt(e,t,n,r){var o=en(e);return r&&(o.has=function(r){var o=e.get(r,y);return o!==y&&!!t.call(n,o,r,e)},o.get=function(r,o){var i=e.get(r,y);return i!==y&&t.call(n,i,r,e)?i:o}),o.__iterateUncached=function(o,i){var a=this,s=0;return e.__iterate(function(e,i,u){if(t.call(n,e,i,u))return s++,o(e,r?i:s-1,a)},i),s},o.__iteratorUncached=function(o,i){var a=e.__iterator(N,i),s=0;return new U(function(){for(;;){var i=a.next();if(i.done)return i;var u=i.value,c=u[0],l=u[1];if(t.call(n,l,c,e))return q(o,r?c:s++,l,i)}})},o}function zt(e,t,n,r){var o=e.size;if(void 0!==t&&(t|=0),void 0!==n&&(n===1/0?n=o:n|=0),A(t,n,o))return e;var i=T(t,o),a=j(n,o);if(i!=i||a!=a)return zt(e.toSeq().cacheResult(),t,n,r);var s,u=a-i;u==u&&(s=u<0?0:u);var c=en(e);return c.size=0===s?s:e.size&&s||void 0,!r&&oe(e)&&s>=0&&(c.get=function(t,n){return(t=k(this,t))>=0&&ts)return{value:void 0,done:!0};var e=o.next();return r||t===M?e:q(t,u-1,t===I?void 0:e.value[1],e)})},c}function Vt(e,t,n,r){var o=en(e);return o.__iterateUncached=function(o,i){var a=this;if(i)return this.cacheResult().__iterate(o,i);var s=!0,u=0;return e.__iterate(function(e,i,c){if(!s||!(s=t.call(n,e,i,c)))return u++,o(e,r?i:u-1,a)}),u},o.__iteratorUncached=function(o,i){var a=this;if(i)return this.cacheResult().__iterator(o,i);var s=e.__iterator(N,i),u=!0,c=0;return new U(function(){var e,i,l;do{if((e=s.next()).done)return r||o===M?e:q(o,c++,o===I?void 0:e.value[1],e);var p=e.value;i=p[0],l=p[1],u&&(u=t.call(n,l,i,a))}while(u);return o===N?e:q(o,i,l,e)})},o}function Ht(e,t){var n=s(e),o=[e].concat(t).map(function(e){return a(e)?n&&(e=r(e)):e=n?ae(e):se(Array.isArray(e)?e:[e]),e}).filter(function(e){return 0!==e.size});if(0===o.length)return e;if(1===o.length){var i=o[0];if(i===e||n&&s(i)||u(e)&&u(i))return i}var c=new ee(o);return n?c=c.toKeyedSeq():u(e)||(c=c.toSetSeq()),(c=c.flatten(!0)).size=o.reduce(function(e,t){if(void 0!==e){var n=t.size;if(void 0!==n)return e+n}},0),c}function Wt(e,t,n){var r=en(e);return r.__iterateUncached=function(r,o){var i=0,s=!1;return function e(u,c){var l=this;u.__iterate(function(o,u){return(!t||c0}function $t(e,t,r){var o=en(e);return o.size=new ee(r).map(function(e){return e.size}).min(),o.__iterate=function(e,t){for(var n,r=this.__iterator(M,t),o=0;!(n=r.next()).done&&!1!==e(n.value,o++,this););return o},o.__iteratorUncached=function(e,o){var i=r.map(function(e){return e=n(e),V(o?e.reverse():e)}),a=0,s=!1;return new U(function(){var n;return s||(n=i.map(function(e){return e.next()}),s=n.some(function(e){return e.done})),s?{value:void 0,done:!0}:q(e,a++,t.apply(null,n.map(function(e){return e.value})))})},o}function Gt(e,t){return oe(e)?t:e.constructor(t)}function Zt(e){if(e!==Object(e))throw new TypeError("Expected [K, V] tuple: "+e)}function Xt(e){return Le(e.size),C(e)}function Qt(e){return s(e)?r:u(e)?o:i}function en(e){return Object.create((s(e)?K:u(e)?Y:$).prototype)}function tn(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):J.prototype.cacheResult.call(this)}function nn(e,t){return e>t?1:e=0;n--)t={value:arguments[n],next:t};return this.__ownerID?(this.size=e,this._head=t,this.__hash=void 0,this.__altered=!0,this):An(e,t)},En.prototype.pushAll=function(e){if(0===(e=o(e)).size)return this;Le(e.size);var t=this.size,n=this._head;return e.reverse().forEach(function(e){t++,n={value:e,next:n}}),this.__ownerID?(this.size=t,this._head=n,this.__hash=void 0,this.__altered=!0,this):An(t,n)},En.prototype.pop=function(){return this.slice(1)},En.prototype.unshift=function(){return this.push.apply(this,arguments)},En.prototype.unshiftAll=function(e){return this.pushAll(e)},En.prototype.shift=function(){return this.pop.apply(this,arguments)},En.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):Tn()},En.prototype.slice=function(e,t){if(A(e,t,this.size))return this;var n=T(e,this.size);if(j(t,this.size)!==this.size)return we.prototype.slice.call(this,e,t);for(var r=this.size-n,o=this._head;n--;)o=o.next;return this.__ownerID?(this.size=r,this._head=o,this.__hash=void 0,this.__altered=!0,this):An(r,o)},En.prototype.__ensureOwner=function(e){return e===this.__ownerID?this:e?An(this.size,this._head,e,this.__hash):(this.__ownerID=e,this.__altered=!1,this)},En.prototype.__iterate=function(e,t){if(t)return this.reverse().__iterate(e);for(var n=0,r=this._head;r&&!1!==e(r.value,n++,this);)r=r.next;return n},En.prototype.__iterator=function(e,t){if(t)return this.reverse().__iterator(e);var n=0,r=this._head;return new U(function(){if(r){var t=r.value;return r=r.next,q(e,n++,t)}return{value:void 0,done:!0}})},En.isStack=Sn;var Cn,kn="@@__IMMUTABLE_STACK__@@",On=En.prototype;function An(e,t,n,r){var o=Object.create(On);return o.size=e,o._head=t,o.__ownerID=n,o.__hash=r,o.__altered=!1,o}function Tn(){return Cn||(Cn=An(0))}function jn(e,t){var n=function(n){e.prototype[n]=t[n]};return Object.keys(t).forEach(n),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(t).forEach(n),e}On[kn]=!0,On.withMutations=ze.withMutations,On.asMutable=ze.asMutable,On.asImmutable=ze.asImmutable,On.wasAltered=ze.wasAltered,n.Iterator=U,jn(n,{toArray:function(){Le(this.size);var e=new Array(this.size||0);return this.valueSeq().__iterate(function(t,n){e[n]=t}),e},toIndexedSeq:function(){return new Rt(this)},toJS:function(){return this.toSeq().map(function(e){return e&&"function"==typeof e.toJS?e.toJS():e}).__toJS()},toJSON:function(){return this.toSeq().map(function(e){return e&&"function"==typeof e.toJSON?e.toJSON():e}).__toJS()},toKeyedSeq:function(){return new Nt(this,!0)},toMap:function(){return Ue(this.toKeyedSeq())},toObject:function(){Le(this.size);var e={};return this.__iterate(function(t,n){e[n]=t}),e},toOrderedMap:function(){return Tt(this.toKeyedSeq())},toOrderedSet:function(){return gn(s(this)?this.valueSeq():this)},toSet:function(){return cn(s(this)?this.valueSeq():this)},toSetSeq:function(){return new Dt(this)},toSeq:function(){return u(this)?this.toIndexedSeq():s(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return En(s(this)?this.valueSeq():this)},toList:function(){return ft(s(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(e,t){return 0===this.size?e+t:e+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+t},concat:function(){var t=e.call(arguments,0);return Gt(this,Ht(this,t))},includes:function(e){return this.some(function(t){return de(t,e)})},entries:function(){return this.__iterator(N)},every:function(e,t){Le(this.size);var n=!0;return this.__iterate(function(r,o,i){if(!e.call(t,r,o,i))return n=!1,!1}),n},filter:function(e,t){return Gt(this,Bt(this,e,t,!0))},find:function(e,t,n){var r=this.findEntry(e,t);return r?r[1]:n},forEach:function(e,t){return Le(this.size),this.__iterate(t?e.bind(t):e)},join:function(e){Le(this.size),e=void 0!==e?""+e:",";var t="",n=!0;return this.__iterate(function(r){n?n=!1:t+=e,t+=null!=r?r.toString():""}),t},keys:function(){return this.__iterator(I)},map:function(e,t){return Gt(this,qt(this,e,t))},reduce:function(e,t,n){var r,o;return Le(this.size),arguments.length<2?o=!0:r=t,this.__iterate(function(t,i,a){o?(o=!1,r=t):r=e.call(n,r,t,i,a)}),r},reduceRight:function(e,t,n){var r=this.toKeyedSeq().reverse();return r.reduce.apply(r,arguments)},reverse:function(){return Gt(this,Ft(this,!0))},slice:function(e,t){return Gt(this,zt(this,e,t,!0))},some:function(e,t){return!this.every(Rn(e),t)},sort:function(e){return Gt(this,Jt(this,e))},values:function(){return this.__iterator(M)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some(function(){return!0})},count:function(e,t){return C(e?this.toSeq().filter(e,t):this)},countBy:function(e,t){return function(e,t,n){var r=Ue().asMutable();return e.__iterate(function(o,i){r.update(t.call(n,o,i,e),0,function(e){return e+1})}),r.asImmutable()}(this,e,t)},equals:function(e){return me(this,e)},entrySeq:function(){var e=this;if(e._cache)return new ee(e._cache);var t=e.toSeq().map(Nn).toIndexedSeq();return t.fromEntrySeq=function(){return e.toSeq()},t},filterNot:function(e,t){return this.filter(Rn(e),t)},findEntry:function(e,t,n){var r=n;return this.__iterate(function(n,o,i){if(e.call(t,n,o,i))return r=[o,n],!1}),r},findKey:function(e,t){var n=this.findEntry(e,t);return n&&n[0]},findLast:function(e,t,n){return this.toKeyedSeq().reverse().find(e,t,n)},findLastEntry:function(e,t,n){return this.toKeyedSeq().reverse().findEntry(e,t,n)},findLastKey:function(e,t){return this.toKeyedSeq().reverse().findKey(e,t)},first:function(){return this.find(O)},flatMap:function(e,t){return Gt(this,function(e,t,n){var r=Qt(e);return e.toSeq().map(function(o,i){return r(t.call(n,o,i,e))}).flatten(!0)}(this,e,t))},flatten:function(e){return Gt(this,Wt(this,e,!0))},fromEntrySeq:function(){return new Lt(this)},get:function(e,t){return this.find(function(t,n){return de(n,e)},void 0,t)},getIn:function(e,t){for(var n,r=this,o=rn(e);!(n=o.next()).done;){var i=n.value;if((r=r&&r.get?r.get(i,y):y)===y)return t}return r},groupBy:function(e,t){return function(e,t,n){var r=s(e),o=(l(e)?Tt():Ue()).asMutable();e.__iterate(function(i,a){o.update(t.call(n,i,a,e),function(e){return(e=e||[]).push(r?[a,i]:i),e})});var i=Qt(e);return o.map(function(t){return Gt(e,i(t))})}(this,e,t)},has:function(e){return this.get(e,y)!==y},hasIn:function(e){return this.getIn(e,y)!==y},isSubset:function(e){return e="function"==typeof e.includes?e:n(e),this.every(function(t){return e.includes(t)})},isSuperset:function(e){return(e="function"==typeof e.isSubset?e:n(e)).isSubset(this)},keyOf:function(e){return this.findKey(function(t){return de(t,e)})},keySeq:function(){return this.toSeq().map(Mn).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(e){return this.toKeyedSeq().reverse().keyOf(e)},max:function(e){return Kt(this,e)},maxBy:function(e,t){return Kt(this,t,e)},min:function(e){return Kt(this,e?Dn(e):qn)},minBy:function(e,t){return Kt(this,t?Dn(t):qn,e)},rest:function(){return this.slice(1)},skip:function(e){return this.slice(Math.max(0,e))},skipLast:function(e){return Gt(this,this.toSeq().reverse().skip(e).reverse())},skipWhile:function(e,t){return Gt(this,Vt(this,e,t,!0))},skipUntil:function(e,t){return this.skipWhile(Rn(e),t)},sortBy:function(e,t){return Gt(this,Jt(this,t,e))},take:function(e){return this.slice(0,Math.max(0,e))},takeLast:function(e){return Gt(this,this.toSeq().reverse().take(e).reverse())},takeWhile:function(e,t){return Gt(this,function(e,t,n){var r=en(e);return r.__iterateUncached=function(r,o){var i=this;if(o)return this.cacheResult().__iterate(r,o);var a=0;return e.__iterate(function(e,o,s){return t.call(n,e,o,s)&&++a&&r(e,o,i)}),a},r.__iteratorUncached=function(r,o){var i=this;if(o)return this.cacheResult().__iterator(r,o);var a=e.__iterator(N,o),s=!0;return new U(function(){if(!s)return{value:void 0,done:!0};var e=a.next();if(e.done)return e;var o=e.value,u=o[0],c=o[1];return t.call(n,c,u,i)?r===N?e:q(r,u,c,e):(s=!1,{value:void 0,done:!0})})},r}(this,e,t))},takeUntil:function(e,t){return this.takeWhile(Rn(e),t)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=function(e){if(e.size===1/0)return 0;var t=l(e),n=s(e),r=t?1:0;return function(e,t){return t=Ee(t,3432918353),t=Ee(t<<15|t>>>-15,461845907),t=Ee(t<<13|t>>>-13,5),t=Ee((t=(t+3864292196|0)^e)^t>>>16,2246822507),t=Se((t=Ee(t^t>>>13,3266489909))^t>>>16)}(e.__iterate(n?t?function(e,t){r=31*r+Fn(Ce(e),Ce(t))|0}:function(e,t){r=r+Fn(Ce(e),Ce(t))|0}:t?function(e){r=31*r+Ce(e)|0}:function(e){r=r+Ce(e)|0}),r)}(this))}});var Pn=n.prototype;Pn[p]=!0,Pn[L]=Pn.values,Pn.__toJS=Pn.toArray,Pn.__toStringMapper=Ln,Pn.inspect=Pn.toSource=function(){return this.toString()},Pn.chain=Pn.flatMap,Pn.contains=Pn.includes,jn(r,{flip:function(){return Gt(this,Ut(this))},mapEntries:function(e,t){var n=this,r=0;return Gt(this,this.toSeq().map(function(o,i){return e.call(t,[i,o],r++,n)}).fromEntrySeq())},mapKeys:function(e,t){var n=this;return Gt(this,this.toSeq().flip().map(function(r,o){return e.call(t,r,o,n)}).flip())}});var In=r.prototype;function Mn(e,t){return t}function Nn(e,t){return[t,e]}function Rn(e){return function(){return!e.apply(this,arguments)}}function Dn(e){return function(){return-e.apply(this,arguments)}}function Ln(e){return"string"==typeof e?JSON.stringify(e):String(e)}function Un(){return S(arguments)}function qn(e,t){return et?-1:0}function Fn(e,t){return e^t+2654435769+(e<<6)+(e>>2)|0}return In[f]=!0,In[L]=Pn.entries,In.__toJS=Pn.toObject,In.__toStringMapper=function(e,t){return JSON.stringify(t)+": "+Ln(e)},jn(o,{toKeyedSeq:function(){return new Nt(this,!1)},filter:function(e,t){return Gt(this,Bt(this,e,t,!1))},findIndex:function(e,t){var n=this.findEntry(e,t);return n?n[0]:-1},indexOf:function(e){var t=this.keyOf(e);return void 0===t?-1:t},lastIndexOf:function(e){var t=this.lastKeyOf(e);return void 0===t?-1:t},reverse:function(){return Gt(this,Ft(this,!1))},slice:function(e,t){return Gt(this,zt(this,e,t,!1))},splice:function(e,t){var n=arguments.length;if(t=Math.max(0|t,0),0===n||2===n&&!t)return this;e=T(e,e<0?this.count():this.size);var r=this.slice(0,e);return Gt(this,1===n?r:r.concat(S(arguments,2),this.slice(e+t)))},findLastIndex:function(e,t){var n=this.findLastEntry(e,t);return n?n[0]:-1},first:function(){return this.get(0)},flatten:function(e){return Gt(this,Wt(this,e,!1))},get:function(e,t){return(e=k(this,e))<0||this.size===1/0||void 0!==this.size&&e>this.size?t:this.find(function(t,n){return n===e},void 0,t)},has:function(e){return(e=k(this,e))>=0&&(void 0!==this.size?this.size===1/0||e5e3)return e.textContent;return function(e){for(var n,r,o,i,a,s=e.textContent,u=0,c=s[0],l=1,p=e.innerHTML="",f=0;r=n,n=f<7&&"\\"==n?1:l;){if(l=c,c=s[++u],i=p.length>1,!l||f>8&&"\n"==l||[/\S/.test(l),1,1,!/[$\w]/.test(l),("/"==n||"\n"==n)&&i,'"'==n&&i,"'"==n&&i,s[u-4]+r+n=="--\x3e",r+n=="*/"][f])for(p&&(e.appendChild(a=t.createElement("span")).setAttribute("style",["color: #555; font-weight: bold;","","","color: #555;",""][f?f<3?2:f>6?4:f>3?3:+/^(a(bstract|lias|nd|rguments|rray|s(m|sert)?|uto)|b(ase|egin|ool(ean)?|reak|yte)|c(ase|atch|har|hecked|lass|lone|ompl|onst|ontinue)|de(bugger|cimal|clare|f(ault|er)?|init|l(egate|ete)?)|do|double|e(cho|ls?if|lse(if)?|nd|nsure|num|vent|x(cept|ec|p(licit|ort)|te(nds|nsion|rn)))|f(allthrough|alse|inal(ly)?|ixed|loat|or(each)?|riend|rom|unc(tion)?)|global|goto|guard|i(f|mp(lements|licit|ort)|n(it|clude(_once)?|line|out|stanceof|t(erface|ernal)?)?|s)|l(ambda|et|ock|ong)|m(icrolight|odule|utable)|NaN|n(amespace|ative|ext|ew|il|ot|ull)|o(bject|perator|r|ut|verride)|p(ackage|arams|rivate|rotected|rotocol|ublic)|r(aise|e(adonly|do|f|gister|peat|quire(_once)?|scue|strict|try|turn))|s(byte|ealed|elf|hort|igned|izeof|tatic|tring|truct|ubscript|uper|ynchronized|witch)|t(emplate|hen|his|hrows?|ransient|rue|ry|ype(alias|def|id|name|of))|u(n(checked|def(ined)?|ion|less|signed|til)|se|sing)|v(ar|irtual|oid|olatile)|w(char_t|hen|here|hile|ith)|xor|yield)$/.test(p):0]),a.appendChild(t.createTextNode(p))),o=f&&f<7?f:o,p="",f=11;![1,/[\/{}[(\-+*=<>:;|\\.,?!&@~]/.test(l),/[\])]/.test(l),/[$\w]/.test(l),"/"==l&&o<2&&"<"!=n,'"'==l,"'"==l,l+c+s[u+1]+s[u+2]=="\x3c!--",l+c=="/*",l+c=="//","#"==l][--f];);p+=l}}(e)}function Q(e){var t;if([/filename\*=[^']+'\w*'"([^"]+)";?/i,/filename\*=[^']+'\w*'([^;]+);?/i,/filename="([^;]*);?"/i,/filename=([^;]*);?/i].some(function(n){return null!==(t=n.exec(e))}),null!==t&&t.length>1)try{return decodeURIComponent(t[1])}catch(e){console.error(e)}return null}function ee(e){return t=e.replace(/\.[^.\/]*$/,""),b()(g()(t));var t}var te=function(e,t){if(e>t)return"Value must be less than Maximum"},ne=function(e,t){if(et)return"Value must be less than MaxLength"},pe=function(e,t){if(e.length2&&void 0!==arguments[2]?arguments[2]:{},r=n.isOAS3,o=void 0!==r&&r,i=n.bypassRequiredCheck,a=void 0!==i&&i,s=[],u=e.get("required"),c=Object(P.a)(e,{isOAS3:o}),p=c.schema,h=c.parameterContentMediaType;if(!p)return s;var m=p.get("required"),v=p.get("maximum"),g=p.get("minimum"),y=p.get("type"),b=p.get("format"),_=p.get("maxLength"),w=p.get("minLength"),x=p.get("pattern");if(y&&(u||m||t)){var E="string"===y&&t,S="array"===y&&l()(t)&&t.length,C="array"===y&&d.a.List.isList(t)&&t.count(),k="array"===y&&"string"==typeof t&&t,O="file"===y&&t instanceof A.a.File,T="boolean"===y&&(t||!1===t),j="number"===y&&(t||0===t),I="integer"===y&&(t||0===t),M="object"===y&&"object"===f()(t)&&null!==t,N="object"===y&&"string"==typeof t&&t,R=[E,S,C,k,O,T,j,I,M,N],D=R.some(function(e){return!!e});if((u||m)&&!D&&!a)return s.push("Required field is not provided"),s;if("object"===y&&"string"==typeof t&&(null===h||"application/json"===h))try{JSON.parse(t)}catch(e){return s.push("Parameter string value must be valid JSON"),s}if(x){var L=fe(t,x);L&&s.push(L)}if(_||0===_){var U=le(t,_);U&&s.push(U)}if(w){var q=pe(t,w);q&&s.push(q)}if(v||0===v){var F=te(t,v);F&&s.push(F)}if(g||0===g){var B=ne(t,g);B&&s.push(B)}if("string"===y){var z;if(!(z="date-time"===b?ue(t):"uuid"===b?ce(t):se(t)))return s;s.push(z)}else if("boolean"===y){var V=ae(t);if(!V)return s;s.push(V)}else if("number"===y){var H=re(t);if(!H)return s;s.push(H)}else if("integer"===y){var W=oe(t);if(!W)return s;s.push(W)}else if("array"===y){var J;if(!C||!t.count())return s;J=p.getIn(["items","type"]),t.forEach(function(e,t){var n;"number"===J?n=re(e):"integer"===J?n=oe(e):"string"===J&&(n=se(e)),n&&s.push({index:t,error:n})})}else if("file"===y){var K=ie(t);if(!K)return s;s.push(K)}}return s},de=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(/xml/.test(t)){if(!e.xml||!e.xml.name){if(e.xml=e.xml||{},!e.$$ref)return e.type||e.items||e.properties||e.additionalProperties?'\n\x3c!-- XML example cannot be generated; root element name is undefined --\x3e':null;var r=e.$$ref.match(/\S*\/(\S+)$/);e.xml.name=r[1]}return Object(k.memoizedCreateXMLExample)(e,n)}var i=Object(k.memoizedSampleFromSchema)(e,n);return"object"===f()(i)?o()(i,null,2):i},me=function(){var e={},t=A.a.location.search;if(!t)return{};if(""!=t){var n=t.substr(1).split("&");for(var r in n)n.hasOwnProperty(r)&&(r=n[r].split("="),e[decodeURIComponent(r[0])]=r[1]&&decodeURIComponent(r[1])||"")}return e},ve=function(t){return(t instanceof e?t:new e(t.toString(),"utf-8")).toString("base64")},ge={operationsSorter:{alpha:function(e,t){return e.get("path").localeCompare(t.get("path"))},method:function(e,t){return e.get("method").localeCompare(t.get("method"))}},tagsSorter:{alpha:function(e,t){return e.localeCompare(t)}}},ye=function(e){var t=[];for(var n in e){var r=e[n];void 0!==r&&""!==r&&t.push([n,"=",encodeURIComponent(r).replace(/%20/g,"+")].join(""))}return t.join("&")},be=function(e,t,n){return!!E()(n,function(n){return C()(e[n],t[n])})};function _e(e){return"string"!=typeof e||""===e?"":Object(m.sanitizeUrl)(e)}function we(e){if(!d.a.OrderedMap.isOrderedMap(e))return null;if(!e.size)return null;var t=e.find(function(e,t){return t.startsWith("2")&&u()(e.get("content")||{}).length>0}),n=e.get("default")||d.a.OrderedMap(),r=(n.get("content")||d.a.OrderedMap()).keySeq().toJS().length?n:null;return t||r}var xe=function(e){return"string"==typeof e||e instanceof String?e.trim().replace(/\s/g,"%20"):""},Ee=function(e){return j()(xe(e).replace(/%20/g,"_"))},Se=function(e){return e.filter(function(e,t){return/^x-/.test(t)})},Ce=function(e){return e.filter(function(e,t){return/^pattern|maxLength|minLength|maximum|minimum/.test(t)})};function ke(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){return!0};if("object"!==f()(e)||l()(e)||null===e||!t)return e;var r=a()({},e);return u()(r).forEach(function(e){e===t&&n(r[e],e)?delete r[e]:r[e]=ke(r[e],t,n)}),r}function Oe(e){if("string"==typeof e)return e;if(e&&e.toJS&&(e=e.toJS()),"object"===f()(e)&&null!==e)try{return o()(e,null,2)}catch(t){return String(e)}return null==e?"":e.toString()}function Ae(e){return"number"==typeof e?e.toString():e}function Te(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.returnAll,r=void 0!==n&&n,o=t.allowHashes,i=void 0===o||o;if(!d.a.Map.isMap(e))throw new Error("paramToIdentifier: received a non-Im.Map parameter as input");var a=e.get("name"),s=e.get("in"),u=[];return e&&e.hashCode&&s&&a&&i&&u.push("".concat(s,".").concat(a,".hash-").concat(e.hashCode())),s&&a&&u.push("".concat(s,".").concat(a)),u.push(a),r?u:u[0]||""}function je(e,t){return Te(e,{returnAll:!0}).map(function(e){return t[e]}).filter(function(e){return void 0!==e})[0]}function Pe(){return Me(M()(32).toString("base64"))}function Ie(e){return Me(R()("sha256").update(e).digest("base64"))}function Me(e){return e.replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}}).call(this,n(64).Buffer)},function(e,t){e.exports=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}},function(e,t,n){var r=n(54);function o(e,t){for(var n=0;n1?t-1:0),o=1;o2?n-2:0),i=2;i>",i={listOf:function(e){return c(e,"List",r.List.isList)},mapOf:function(e,t){return l(e,t,"Map",r.Map.isMap)},orderedMapOf:function(e,t){return l(e,t,"OrderedMap",r.OrderedMap.isOrderedMap)},setOf:function(e){return c(e,"Set",r.Set.isSet)},orderedSetOf:function(e){return c(e,"OrderedSet",r.OrderedSet.isOrderedSet)},stackOf:function(e){return c(e,"Stack",r.Stack.isStack)},iterableOf:function(e){return c(e,"Iterable",r.Iterable.isIterable)},recordOf:function(e){return s(function(t,n,o,i,s){for(var u=arguments.length,c=Array(u>5?u-5:0),l=5;l6?u-6:0),l=6;l5?c-5:0),p=5;p5?i-5:0),s=5;s key("+l[p]+")"].concat(a));if(h instanceof Error)return h}})).apply(void 0,i);var u})}function p(e){var t=void 0===arguments[1]?"Iterable":arguments[1],n=void 0===arguments[2]?r.Iterable.isIterable:arguments[2];return s(function(r,o,i,s,u){for(var c=arguments.length,l=Array(c>5?c-5:0),p=5;p4)}function u(e){var t=e.get("swagger");return"string"==typeof t&&t.startsWith("2.0")}function c(e){return function(t,n){return function(r){return n&&n.specSelectors&&n.specSelectors.specJson?s(n.specSelectors.specJson())?a.a.createElement(e,o()({},r,n,{Ori:t})):a.a.createElement(t,r):(console.warn("OAS3 wrapper: couldn't get spec"),null)}}}},function(e,t,n){"use strict"; +/* +object-assign +(c) Sindre Sorhus +@license MIT +*/var r=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable;function a(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map(function(e){return t[e]}).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach(function(e){r[e]=e}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,t){for(var n,s,u=a(e),c=1;c0){var o=n.map(function(e){return console.error(e),e.line=e.fullPath?g(y,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",A()(e,"message",{enumerable:!0,value:e.message}),e});i.newThrownErrBatch(o)}return r.updateResolved(t)})}},_e=[],we=V()(k()(S.a.mark(function e(){var t,n,r,o,i,a,s,u,c,l,p,f,h,d,m,v,g;return S.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:if(t=_e.system){e.next=4;break}return console.error("debResolveSubtrees: don't have a system to operate on, aborting."),e.abrupt("return");case 4:if(n=t.errActions,r=t.errSelectors,o=t.fn,i=o.resolveSubtree,a=o.AST,s=void 0===a?{}:a,u=t.specSelectors,c=t.specActions,i){e.next=8;break}return console.error("Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing."),e.abrupt("return");case 8:return l=s.getLineNumberForPath?s.getLineNumberForPath:function(){},p=u.specStr(),f=t.getConfigs(),h=f.modelPropertyMacro,d=f.parameterMacro,m=f.requestInterceptor,v=f.responseInterceptor,e.prev=11,e.next=14,_e.reduce(function(){var e=k()(S.a.mark(function e(t,o){var a,s,c,f,g,y,b;return S.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,t;case 2:return a=e.sent,s=a.resultMap,c=a.specWithCurrentSubtrees,e.next=7,i(c,o,{baseDoc:u.url(),modelPropertyMacro:h,parameterMacro:d,requestInterceptor:m,responseInterceptor:v});case 7:return f=e.sent,g=f.errors,y=f.spec,r.allErrors().size&&n.clearBy(function(e){return"thrown"!==e.get("type")||"resolver"!==e.get("source")||!e.get("fullPath").every(function(e,t){return e===o[t]||void 0===o[t]})}),j()(g)&&g.length>0&&(b=g.map(function(e){return e.line=e.fullPath?l(p,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",A()(e,"message",{enumerable:!0,value:e.message}),e}),n.newThrownErrBatch(b)),W()(s,o,y),W()(c,o,y),e.abrupt("return",{resultMap:s,specWithCurrentSubtrees:c});case 15:case"end":return e.stop()}},e)}));return function(t,n){return e.apply(this,arguments)}}(),x.a.resolve({resultMap:(u.specResolvedSubtree([])||Object(R.Map)()).toJS(),specWithCurrentSubtrees:u.specJson().toJS()}));case 14:g=e.sent,delete _e.system,_e=[],e.next=22;break;case 19:e.prev=19,e.t0=e.catch(11),console.error(e.t0);case 22:c.updateResolvedSubtree([],g.resultMap);case 23:case"end":return e.stop()}},e,null,[[11,19]])})),35),xe=function(e){return function(t){_e.map(function(e){return e.join("@@")}).indexOf(e.join("@@"))>-1||(_e.push(e),_e.system=t,we())}};function Ee(e,t,n,r,o){return{type:X,payload:{path:e,value:r,paramName:t,paramIn:n,isXml:o}}}function Se(e,t,n,r){return{type:X,payload:{path:e,param:t,value:n,isXml:r}}}var Ce=function(e,t){return{type:le,payload:{path:e,value:t}}},ke=function(){return{type:le,payload:{path:[],value:Object(R.Map)()}}},Oe=function(e,t){return{type:ee,payload:{pathMethod:e,isOAS3:t}}},Ae=function(e,t,n,r){return{type:Q,payload:{pathMethod:e,paramName:t,paramIn:n,includeEmptyValue:r}}};function Te(e){return{type:se,payload:{pathMethod:e}}}function je(e,t){return{type:ue,payload:{path:e,value:t,key:"consumes_value"}}}function Pe(e,t){return{type:ue,payload:{path:e,value:t,key:"produces_value"}}}var Ie=function(e,t,n){return{payload:{path:e,method:t,res:n},type:te}},Me=function(e,t,n){return{payload:{path:e,method:t,req:n},type:ne}},Ne=function(e,t,n){return{payload:{path:e,method:t,req:n},type:re}},Re=function(e){return{payload:e,type:oe}},De=function(e){return function(t){var n=t.fn,r=t.specActions,o=t.specSelectors,i=t.getConfigs,a=t.oas3Selectors,s=e.pathName,u=e.method,c=e.operation,l=i(),p=l.requestInterceptor,f=l.responseInterceptor,h=c.toJS();if(c&&c.get("parameters")&&c.get("parameters").filter(function(e){return e&&!0===e.get("allowEmptyValue")}).forEach(function(t){if(o.parameterInclusionSettingFor([s,u],t.get("name"),t.get("in"))){e.parameters=e.parameters||{};var n=Object(J.C)(t,e.parameters);(!n||n&&0===n.size)&&(e.parameters[t.get("name")]="")}}),e.contextUrl=L()(o.url()).toString(),h&&h.operationId?e.operationId=h.operationId:h&&s&&u&&(e.operationId=n.opId(h,s,u)),o.isOAS3()){var d="".concat(s,":").concat(u);e.server=a.selectedServer(d)||a.selectedServer();var m=a.serverVariables({server:e.server,namespace:d}).toJS(),g=a.serverVariables({server:e.server}).toJS();e.serverVariables=_()(m).length?m:g,e.requestContentType=a.requestContentType(s,u),e.responseContentType=a.responseContentType(s,u)||"*/*";var b=a.requestBodyValue(s,u);Object(J.t)(b)?e.requestBody=JSON.parse(b):b&&b.toJS?e.requestBody=b.toJS():e.requestBody=b}var w=y()({},e);w=n.buildRequest(w),r.setRequest(e.pathName,e.method,w);e.requestInterceptor=function(t){var n=p.apply(this,[t]),o=y()({},n);return r.setMutatedRequest(e.pathName,e.method,o),n},e.responseInterceptor=f;var x=v()();return n.execute(e).then(function(t){t.duration=v()()-x,r.setResponse(e.pathName,e.method,t)}).catch(function(t){console.error(t),r.setResponse(e.pathName,e.method,{error:!0,err:q()(t)})})}},Le=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.path,n=e.method,r=d()(e,["path","method"]);return function(e){var o=e.fn.fetch,i=e.specSelectors,a=e.specActions,s=i.specJsonWithResolvedSubtrees().toJS(),u=i.operationScheme(t,n),c=i.contentTypeValues([t,n]).toJS(),l=c.requestContentType,p=c.responseContentType,f=/xml/i.test(l),h=i.parameterValues([t,n],f).toJS();return a.executeRequest(Y({},r,{fetch:o,spec:s,pathName:t,method:n,parameters:h,requestContentType:l,scheme:u,responseContentType:p}))}};function Ue(e,t){return{type:ie,payload:{path:e,method:t}}}function qe(e,t){return{type:ae,payload:{path:e,method:t}}}function Fe(e,t,n){return{type:pe,payload:{scheme:e,path:t,method:n}}}},function(e,t,n){var r=n(32),o=n(22),i=n(63),a=n(77),s=n(75),u=function(e,t,n){var c,l,p,f=e&u.F,h=e&u.G,d=e&u.S,m=e&u.P,v=e&u.B,g=e&u.W,y=h?o:o[t]||(o[t]={}),b=y.prototype,_=h?r:d?r[t]:(r[t]||{}).prototype;for(c in h&&(n=t),n)(l=!f&&_&&void 0!==_[c])&&s(y,c)||(p=l?_[c]:n[c],y[c]=h&&"function"!=typeof _[c]?n[c]:v&&l?i(p,r):g&&_[c]==p?function(e){var t=function(t,n,r){if(this instanceof e){switch(arguments.length){case 0:return new e;case 1:return new e(t);case 2:return new e(t,n)}return new e(t,n,r)}return e.apply(this,arguments)};return t.prototype=e.prototype,t}(p):m&&"function"==typeof p?i(Function.call,p):p,m&&((y.virtual||(y.virtual={}))[c]=p,e&u.R&&b&&!b[c]&&a(b,c,p)))};u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,e.exports=u},function(e,t,n){"use strict";var r=n(138),o=["kind","resolve","construct","instanceOf","predicate","represent","defaultStyle","styleAliases"],i=["scalar","sequence","mapping"];e.exports=function(e,t){var n,a;if(t=t||{},Object.keys(t).forEach(function(t){if(-1===o.indexOf(t))throw new r('Unknown option "'+t+'" is met in definition of "'+e+'" YAML type.')}),this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(e){return e},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.defaultStyle=t.defaultStyle||null,this.styleAliases=(n=t.styleAliases||null,a={},null!==n&&Object.keys(n).forEach(function(e){n[e].forEach(function(t){a[String(t)]=e})}),a),-1===i.indexOf(this.kind))throw new r('Unknown kind "'+this.kind+'" is specified for "'+e+'" YAML type.')}},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t,n){var r=n(197)("wks"),o=n(199),i=n(41).Symbol,a="function"==typeof i;(e.exports=function(e){return r[e]||(r[e]=a&&i[e]||(a?i:o)("Symbol."+e))}).store=r},function(e,t,n){var r=n(214)("wks"),o=n(159),i=n(32).Symbol,a="function"==typeof i;(e.exports=function(e){return r[e]||(r[e]=a&&i[e]||(a?i:o)("Symbol."+e))}).store=r},function(e,t,n){var r=n(41),o=n(72),i=n(81),a=n(97),s=n(153),u=function(e,t,n){var c,l,p,f,h=e&u.F,d=e&u.G,m=e&u.S,v=e&u.P,g=e&u.B,y=d?r:m?r[t]||(r[t]={}):(r[t]||{}).prototype,b=d?o:o[t]||(o[t]={}),_=b.prototype||(b.prototype={});for(c in d&&(n=t),n)p=((l=!h&&y&&void 0!==y[c])?y:n)[c],f=g&&l?s(p,r):v&&"function"==typeof p?s(Function.call,p):p,y&&a(y,c,p,e&u.U),b[c]!=p&&i(b,c,f),v&&_[c]!=p&&(_[c]=p)};r.core=o,u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,e.exports=u},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t){var n=Array.isArray;e.exports=n},function(e,t,n){"use strict";var r=!("undefined"==typeof window||!window.document||!window.document.createElement),o={canUseDOM:r,canUseWorkers:"undefined"!=typeof Worker,canUseEventListeners:r&&!(!window.addEventListener&&!window.attachEvent),canUseViewport:r&&!!window.screen,isInWorker:!r};e.exports=o},function(e,t,n){"use strict";var r=Object.prototype.hasOwnProperty;function o(e,t){return!!e&&r.call(e,t)}var i=/\\([\\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g;function a(e){return!(e>=55296&&e<=57343)&&(!(e>=64976&&e<=65007)&&(65535!=(65535&e)&&65534!=(65535&e)&&(!(e>=0&&e<=8)&&(11!==e&&(!(e>=14&&e<=31)&&(!(e>=127&&e<=159)&&!(e>1114111)))))))}function s(e){if(e>65535){var t=55296+((e-=65536)>>10),n=56320+(1023&e);return String.fromCharCode(t,n)}return String.fromCharCode(e)}var u=/&([a-z#][a-z0-9]{1,31});/gi,c=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))/i,l=n(463);function p(e,t){var n=0;return o(l,t)?l[t]:35===t.charCodeAt(0)&&c.test(t)&&a(n="x"===t[1].toLowerCase()?parseInt(t.slice(2),16):parseInt(t.slice(1),10))?s(n):e}var f=/[&<>"]/,h=/[&<>"]/g,d={"&":"&","<":"<",">":">",'"':"""};function m(e){return d[e]}t.assign=function(e){return[].slice.call(arguments,1).forEach(function(t){if(t){if("object"!=typeof t)throw new TypeError(t+"must be object");Object.keys(t).forEach(function(n){e[n]=t[n]})}}),e},t.isString=function(e){return"[object String]"===function(e){return Object.prototype.toString.call(e)}(e)},t.has=o,t.unescapeMd=function(e){return e.indexOf("\\")<0?e:e.replace(i,"$1")},t.isValidEntityCode=a,t.fromCodePoint=s,t.replaceEntities=function(e){return e.indexOf("&")<0?e:e.replace(u,p)},t.escapeHtml=function(e){return f.test(e)?e.replace(h,m):e}},function(e,t,n){var r=n(55),o=n(771);e.exports=function(e,t){if(null==e)return{};var n,i,a=o(e,t);if(r){var s=r(e);for(i=0;i=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t,n){var r=n(35),o=n(99),i=n(73),a=/"/g,s=function(e,t,n,r){var o=String(i(e)),s="<"+t;return""!==n&&(s+=" "+n+'="'+String(r).replace(a,""")+'"'),s+">"+o+""};e.exports=function(e,t){var n={};n[e]=t(s),r(r.P+r.F*o(function(){var t=""[e]('"');return t!==t.toLowerCase()||t.split('"').length>3}),"String",n)}},function(e,t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,t,n){"use strict";n.r(t),n.d(t,"NEW_THROWN_ERR",function(){return i}),n.d(t,"NEW_THROWN_ERR_BATCH",function(){return a}),n.d(t,"NEW_SPEC_ERR",function(){return s}),n.d(t,"NEW_SPEC_ERR_BATCH",function(){return u}),n.d(t,"NEW_AUTH_ERR",function(){return c}),n.d(t,"CLEAR",function(){return l}),n.d(t,"CLEAR_BY",function(){return p}),n.d(t,"newThrownErr",function(){return f}),n.d(t,"newThrownErrBatch",function(){return h}),n.d(t,"newSpecErr",function(){return d}),n.d(t,"newSpecErrBatch",function(){return m}),n.d(t,"newAuthErr",function(){return v}),n.d(t,"clear",function(){return g}),n.d(t,"clearBy",function(){return y});var r=n(119),o=n.n(r),i="err_new_thrown_err",a="err_new_thrown_err_batch",s="err_new_spec_err",u="err_new_spec_err_batch",c="err_new_auth_err",l="err_clear",p="err_clear_by";function f(e){return{type:i,payload:o()(e)}}function h(e){return{type:a,payload:e}}function d(e){return{type:s,payload:e}}function m(e){return{type:u,payload:e}}function v(e){return{type:c,payload:e}}function g(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return{type:l,payload:e}}function y(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!0};return{type:p,payload:e}}},function(e,t,n){var r=n(98);e.exports=function(e){if(!r(e))throw TypeError(e+" is not an object!");return e}},function(e,t,n){var r=n(43);e.exports=function(e){if(!r(e))throw TypeError(e+" is not an object!");return e}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t,n){var r=n(64),o=r.Buffer;function i(e,t){for(var n in e)t[n]=e[n]}function a(e,t,n){return o(e,t,n)}o.from&&o.alloc&&o.allocUnsafe&&o.allocUnsafeSlow?e.exports=r:(i(r,t),t.Buffer=a),i(o,a),a.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return o(e,t,n)},a.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=o(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},a.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return o(e)},a.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t,n){var r=n(46),o=n(349),i=n(218),a=Object.defineProperty;t.f=n(50)?Object.defineProperty:function(e,t,n){if(r(e),t=i(t,!0),r(n),o)try{return a(e,t,n)}catch(e){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(e[t]=n.value),e}},function(e,t,n){e.exports=!n(82)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(e,t,n){var r=n(366),o="object"==typeof self&&self&&self.Object===Object&&self,i=r||o||Function("return this")();e.exports=i},function(e,t){e.exports=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}},function(e,t,n){"use strict";e.exports={debugTool:null}},function(e,t,n){e.exports=n(573)},function(e,t,n){e.exports=n(770)},function(e,t,n){e.exports=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=45)}([function(e,t){e.exports=n(17)},function(e,t){e.exports=n(14)},function(e,t){e.exports=n(26)},function(e,t){e.exports=n(16)},function(e,t){e.exports=n(123)},function(e,t){e.exports=n(60)},function(e,t){e.exports=n(61)},function(e,t){e.exports=n(55)},function(e,t){e.exports=n(2)},function(e,t){e.exports=n(54)},function(e,t){e.exports=n(94)},function(e,t){e.exports=n(28)},function(e,t){e.exports=n(930)},function(e,t){e.exports=n(12)},function(e,t){e.exports=n(192)},function(e,t){e.exports=n(936)},function(e,t){e.exports=n(93)},function(e,t){e.exports=n(193)},function(e,t){e.exports=n(939)},function(e,t){e.exports=n(943)},function(e,t){e.exports=n(944)},function(e,t){e.exports=n(92)},function(e,t){e.exports=n(13)},function(e,t){e.exports=n(146)},function(e,t){e.exports=n(4)},function(e,t){e.exports=n(5)},function(e,t){e.exports=n(946)},function(e,t){e.exports=n(421)},function(e,t){e.exports=n(949)},function(e,t){e.exports=n(52)},function(e,t){e.exports=n(64)},function(e,t){e.exports=n(283)},function(e,t){e.exports=n(272)},function(e,t){e.exports=n(950)},function(e,t){e.exports=n(145)},function(e,t){e.exports=n(951)},function(e,t){e.exports=n(959)},function(e,t){e.exports=n(960)},function(e,t){e.exports=n(961)},function(e,t){e.exports=n(40)},function(e,t){e.exports=n(264)},function(e,t){e.exports=n(37)},function(e,t){e.exports=n(964)},function(e,t){e.exports=n(965)},function(e,t){e.exports=n(966)},function(e,t,n){e.exports=n(50)},function(e,t){e.exports=n(967)},function(e,t){e.exports=n(968)},function(e,t){e.exports=n(969)},function(e,t){e.exports=n(970)},function(e,t,n){"use strict";n.r(t);var r={};n.r(r),n.d(r,"path",function(){return mn}),n.d(r,"query",function(){return vn}),n.d(r,"header",function(){return yn}),n.d(r,"cookie",function(){return bn});var o=n(9),i=n.n(o),a=n(10),s=n.n(a),u=n(5),c=n.n(u),l=n(6),p=n.n(l),f=n(7),h=n.n(f),d=n(0),m=n.n(d),v=n(8),g=n.n(v),y=(n(46),n(15)),b=n.n(y),_=n(20),w=n.n(_),x=n(12),E=n.n(x),S=n(4),C=n.n(S),k=n(22),O=n.n(k),A=n(11),T=n.n(A),j=n(2),P=n.n(j),I=n(1),M=n.n(I),N=n(17),R=n.n(N),D=(n(47),n(26)),L=n.n(D),U=n(23),q=n.n(U),F=n(31),B=n.n(F),z={serializeRes:J,mergeInQueryOrForm:Z};function V(e){return H.apply(this,arguments)}function H(){return(H=R()(C.a.mark(function e(t){var n,r,o,i,a,s=arguments;return C.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:if(n=s.length>1&&void 0!==s[1]?s[1]:{},"object"===P()(t)&&(t=(n=t).url),n.headers=n.headers||{},z.mergeInQueryOrForm(n),n.headers&&m()(n.headers).forEach(function(e){var t=n.headers[e];"string"==typeof t&&(n.headers[e]=t.replace(/\n+/g," "))}),!n.requestInterceptor){e.next=12;break}return e.next=8,n.requestInterceptor(n);case 8:if(e.t0=e.sent,e.t0){e.next=11;break}e.t0=n;case 11:n=e.t0;case 12:return r=n.headers["content-type"]||n.headers["Content-Type"],/multipart\/form-data/i.test(r)&&(delete n.headers["content-type"],delete n.headers["Content-Type"]),e.prev=14,e.next=17,(n.userFetch||fetch)(n.url,n);case 17:return o=e.sent,e.next=20,z.serializeRes(o,t,n);case 20:if(o=e.sent,!n.responseInterceptor){e.next=28;break}return e.next=24,n.responseInterceptor(o);case 24:if(e.t1=e.sent,e.t1){e.next=27;break}e.t1=o;case 27:o=e.t1;case 28:e.next=38;break;case 30:if(e.prev=30,e.t2=e.catch(14),o){e.next=34;break}throw e.t2;case 34:throw(i=new Error(o.statusText)).statusCode=i.status=o.status,i.responseError=e.t2,i;case 38:if(o.ok){e.next=43;break}throw(a=new Error(o.statusText)).statusCode=a.status=o.status,a.response=o,a;case 43:return e.abrupt("return",o);case 44:case"end":return e.stop()}},e,null,[[14,30]])}))).apply(this,arguments)}var W=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";return/(json|xml|yaml|text)\b/.test(e)};function J(e,t){var n=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).loadSpec,r=void 0!==n&&n,o={ok:e.ok,url:e.url||t,status:e.status,statusText:e.statusText,headers:K(e.headers)},i=o.headers["content-type"],a=r||W(i);return(a?e.text:e.blob||e.buffer).call(e).then(function(e){if(o.text=e,o.data=e,a)try{var t=function(e,t){return t&&(0===t.indexOf("application/json")||t.indexOf("+json")>0)?JSON.parse(e):q.a.safeLoad(e)}(e,i);o.body=t,o.obj=t}catch(e){o.parseError=e}return o})}function K(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t={};return"function"==typeof e.forEach?(e.forEach(function(e,n){void 0!==t[n]?(t[n]=M()(t[n])?t[n]:[t[n]],t[n].push(e)):t[n]=e}),t):t}function Y(e,t){return t||"undefined"==typeof navigator||(t=navigator),t&&"ReactNative"===t.product?!(!e||"object"!==P()(e)||"string"!=typeof e.uri):"undefined"!=typeof File?e instanceof File:null!==e&&"object"===P()(e)&&"function"==typeof e.pipe}function $(e,t){var n=e.collectionFormat,r=e.allowEmptyValue,o="object"===P()(e)?e.value:e;if(void 0===o&&r)return"";if(Y(o)||"boolean"==typeof o)return o;var i=encodeURIComponent;return t&&(i=B()(o)?function(e){return e}:function(e){return T()(e)}),"object"!==P()(o)||M()(o)?M()(o)?M()(o)&&!n?o.map(i).join(","):"multi"===n?o.map(i):o.map(i).join({csv:",",ssv:"%20",tsv:"%09",pipes:"|"}[n]):i(o):""}function G(e){var t=m()(e).reduce(function(t,n){var r,o=e[n],i=!!o.skipEncoding,a=i?n:encodeURIComponent(n),s=(r=o)&&"object"===P()(r)&&!M()(o);return t[a]=$(s?o:{value:o},i),t},{});return L.a.stringify(t,{encode:!1,indices:!1})||""}function Z(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.url,r=void 0===t?"":t,o=e.query,i=e.form;if(i){var a=m()(i).some(function(e){return Y(i[e].value)}),s=e.headers["content-type"]||e.headers["Content-Type"];if(a||/multipart\/form-data/i.test(s)){var u=n(48);e.body=new u,m()(i).forEach(function(t){e.body.append(t,$(i[t],!0))})}else e.body=G(i);delete e.form}if(o){var c=r.split("?"),l=O()(c,2),p=l[0],f=l[1],h="";if(f){var d=L.a.parse(f);m()(o).forEach(function(e){return delete d[e]}),h=L.a.stringify(d,{encode:!0})}var v=function(){for(var e=arguments.length,t=new Array(e),n=0;n0){var o=t(e,n[n.length-1],n);o&&(r=r.concat(o))}if(M()(e)){var i=e.map(function(e,r){return Ce(e,t,n.concat(r))});i&&(r=r.concat(i))}else if(Te(e)){var a=m()(e).map(function(r){return Ce(e[r],t,n.concat(r))});a&&(r=r.concat(a))}return r=Oe(r)}function ke(e){return M()(e)?e:[e]}function Oe(e){var t;return(t=[]).concat.apply(t,he()(e.map(function(e){return M()(e)?Oe(e):e})))}function Ae(e){return e.filter(function(e){return void 0!==e})}function Te(e){return e&&"object"===P()(e)}function je(e){return e&&"function"==typeof e}function Pe(e){if(Ne(e)){var t=e.op;return"add"===t||"remove"===t||"replace"===t}return!1}function Ie(e){return Pe(e)||Ne(e)&&"mutation"===e.type}function Me(e){return Ie(e)&&("add"===e.op||"replace"===e.op||"merge"===e.op||"mergeDeep"===e.op)}function Ne(e){return e&&"object"===P()(e)}function Re(e,t){try{return me.a.getValueByPointer(e,t)}catch(e){return console.error(e),{}}}var De=n(35),Le=n.n(De),Ue=n(36),qe=n(28),Fe=n.n(qe);function Be(e,t){function n(){Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack;for(var e=arguments.length,n=new Array(e),r=0;r-1&&-1===We.indexOf(n)||Je.indexOf(r)>-1||Ke.some(function(e){return r.indexOf(e)>-1})}function $e(e,t){var n=e.split("#"),r=O()(n,2),o=r[0],i=r[1],a=E.a.resolve(o||"",t||"");return i?"".concat(a,"#").concat(i):a}var Ge="application/json, application/yaml",Ze=new RegExp("^([a-z]+://|//)","i"),Xe=Be("JSONRefError",function(e,t,n){this.originalError=n,ie()(this,t||{})}),Qe={},et=new Le.a,tt=[function(e){return"paths"===e[0]&&"responses"===e[3]&&"content"===e[5]&&"example"===e[7]},function(e){return"paths"===e[0]&&"requestBody"===e[3]&&"content"===e[4]&&"example"===e[6]}],nt={key:"$ref",plugin:function(e,t,n,r){var o=r.getInstance(),i=n.slice(0,-1);if(!Ye(i)&&(a=i,!tt.some(function(e){return e(a)}))){var a,s=r.getContext(n).baseDoc;if("string"!=typeof e)return new Xe("$ref: must be a string (JSON-Ref)",{$ref:e,baseDoc:s,fullPath:n});var u,c,l,p=st(e),f=p[0],h=p[1]||"";try{u=s||f?it(f,s):null}catch(t){return at(t,{pointer:h,$ref:e,basePath:u,fullPath:n})}if(function(e,t,n,r){var o=et.get(r);o||(o={},et.set(r,o));var i=function(e){if(0===e.length)return"";return"/".concat(e.map(ht).join("/"))}(n),a="".concat(t||"","#").concat(e),s=i.replace(/allOf\/\d+\/?/g,""),u=r.contextTree.get([]).baseDoc;if(t==u&&mt(s,e))return!0;var c="";if(n.some(function(e){return c="".concat(c,"/").concat(ht(e)),o[c]&&o[c].some(function(e){return mt(e,a)||mt(a,e)})}))return!0;o[s]=(o[s]||[]).concat(a)}(h,u,i,r)&&!o.useCircularStructures){var d=$e(e,u);return e===d?null:_e.replace(n,d)}if(null==u?(l=pt(h),void 0===(c=r.get(l))&&(c=new Xe("Could not resolve reference: ".concat(e),{pointer:h,$ref:e,baseDoc:s,fullPath:n}))):c=null!=(c=ut(u,h)).__value?c.__value:c.catch(function(t){throw at(t,{pointer:h,$ref:e,baseDoc:s,fullPath:n})}),c instanceof Error)return[_e.remove(n),c];var v=$e(e,u),g=_e.replace(i,c,{$$ref:v});if(u&&u!==s)return[g,_e.context(i,{baseDoc:u})];try{if(!function(e,t){var n=[e];return t.path.reduce(function(e,t){return n.push(e[t]),e[t]},e),function e(t){return _e.isObject(t)&&(n.indexOf(t)>=0||m()(t).some(function(n){return e(t[n])}))}(t.value)}(r.state,g)||o.useCircularStructures)return g}catch(e){return null}}}},rt=ie()(nt,{docCache:Qe,absoluteify:it,clearCache:function(e){void 0!==e?delete Qe[e]:m()(Qe).forEach(function(e){delete Qe[e]})},JSONRefError:Xe,wrapError:at,getDoc:ct,split:st,extractFromDoc:ut,fetchJSON:function(e){return Object(Ue.fetch)(e,{headers:{Accept:Ge},loadSpec:!0}).then(function(e){return e.text()}).then(function(e){return q.a.safeLoad(e)})},extract:lt,jsonPointerToArray:pt,unescapeJsonPointerToken:ft}),ot=rt;function it(e,t){if(!Ze.test(e)){if(!t)throw new Xe("Tried to resolve a relative URL, without having a basePath. path: '".concat(e,"' basePath: '").concat(t,"'"));return E.a.resolve(t,e)}return e}function at(e,t){var n;return n=e&&e.response&&e.response.body?"".concat(e.response.body.code," ").concat(e.response.body.message):e.message,new Xe("Could not resolve reference: ".concat(n),t,e)}function st(e){return(e+"").split("#")}function ut(e,t){var n=Qe[e];if(n&&!_e.isPromise(n))try{var r=lt(t,n);return ie()(Q.a.resolve(r),{__value:r})}catch(e){return Q.a.reject(e)}return ct(e).then(function(e){return lt(t,e)})}function ct(e){var t=Qe[e];return t?_e.isPromise(t)?t:Q.a.resolve(t):(Qe[e]=rt.fetchJSON(e).then(function(t){return Qe[e]=t,t}),Qe[e])}function lt(e,t){var n=pt(e);if(n.length<1)return t;var r=_e.getIn(t,n);if(void 0===r)throw new Xe("Could not resolve pointer: ".concat(e," does not exist in document"),{pointer:e});return r}function pt(e){if("string"!=typeof e)throw new TypeError("Expected a string, got a ".concat(P()(e)));return"/"===e[0]&&(e=e.substr(1)),""===e?[]:e.split("/").map(ft)}function ft(e){return"string"!=typeof e?e:Fe.a.unescape(e.replace(/~1/g,"/").replace(/~0/g,"~"))}function ht(e){return Fe.a.escape(e.replace(/~/g,"~0").replace(/\//g,"~1"))}var dt=function(e){return!e||"/"===e||"#"===e};function mt(e,t){if(dt(t))return!0;var n=e.charAt(t.length),r=t.slice(-1);return 0===e.indexOf(t)&&(!n||"/"===n||"#"===n)&&"#"!==r}var vt={key:"allOf",plugin:function(e,t,n,r,o){if(!o.meta||!o.meta.$$ref){var i=n.slice(0,-1);if(!Ye(i)){if(!M()(e)){var a=new TypeError("allOf must be an array");return a.fullPath=n,a}var s=!1,u=o.value;i.forEach(function(e){u&&(u=u[e])}),delete(u=ie()({},u)).allOf;var c=[];return c.push(r.replace(i,{})),e.forEach(function(e,t){if(!r.isObject(e)){if(s)return null;s=!0;var o=new TypeError("Elements in allOf must be objects");return o.fullPath=n,c.push(o)}c.push(r.mergeDeep(i,e));var a=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.specmap,o=n.getBaseUrlForNodePath,i=void 0===o?function(e){return r.getContext([].concat(he()(t),he()(e))).baseDoc}:o,a=n.targetKeys,s=void 0===a?["$ref","$$ref"]:a,u=[];return Ve()(e).forEach(function(){if(s.indexOf(this.key)>-1){var e=this.path,n=t.concat(this.path),o=$e(this.node,i(e));u.push(r.replace(n,o))}}),u}(e,n.slice(0,-1),{getBaseUrlForNodePath:function(e){return r.getContext([].concat(he()(n),[t],he()(e))).baseDoc},specmap:r});c.push.apply(c,he()(a))}),c.push(r.mergeDeep(i,u)),u.$$ref||c.push(r.remove([].concat(i,"$$ref"))),c}}}},gt={key:"parameters",plugin:function(e,t,n,r,o){if(M()(e)&&e.length){var i=ie()([],e),a=n.slice(0,-1),s=ie()({},_e.getIn(r.spec,a));return e.forEach(function(e,t){try{i[t].default=r.parameterMacro(s,e)}catch(e){var o=new Error(e);return o.fullPath=n,o}}),_e.replace(n,i)}return _e.replace(n,e)}},yt={key:"properties",plugin:function(e,t,n,r){var o=ie()({},e);for(var i in e)try{o[i].default=r.modelPropertyMacro(o[i])}catch(e){var a=new Error(e);return a.fullPath=n,a}return _e.replace(n,o)}};function bt(e,t){var n=m()(e);if(h.a){var r=h()(e);t&&(r=r.filter(function(t){return p()(e,t).enumerable})),n.push.apply(n,r)}return n}var _t=function(){function e(t){se()(this,e),this.root=wt(t||{})}return ce()(e,[{key:"set",value:function(e,t){var n=this.getParent(e,!0);if(n){var r=e[e.length-1],o=n.children;o[r]?xt(o[r],t,n):o[r]=wt(t,n)}else xt(this.root,t,null)}},{key:"get",value:function(e){if((e=e||[]).length<1)return this.root.value;for(var t,n,r=this.root,o=0;o1?n-1:0),o=1;o1?n-1:0),o=1;o0})}},{key:"nextPromisedPatch",value:function(){if(this.promisedPatches.length>0)return Q.a.race(this.promisedPatches.map(function(e){return e.value}))}},{key:"getPluginHistory",value:function(e){var t=this.getPluginName(e);return this.pluginHistory[t]||[]}},{key:"getPluginRunCount",value:function(e){return this.getPluginHistory(e).length}},{key:"getPluginHistoryTip",value:function(e){var t=this.getPluginHistory(e);return t&&t[t.length-1]||{}}},{key:"getPluginMutationIndex",value:function(e){var t=this.getPluginHistoryTip(e).mutationIndex;return"number"!=typeof t?-1:t}},{key:"getPluginName",value:function(e){return e.pluginName}},{key:"updatePluginHistory",value:function(e,t){var n=this.getPluginName(e);(this.pluginHistory[n]=this.pluginHistory[n]||[]).push(t)}},{key:"updatePatches",value:function(e,t){var n=this;_e.normalizeArray(e).forEach(function(e){if(e instanceof Error)n.errors.push(e);else try{if(!_e.isObject(e))return void n.debug("updatePatches","Got a non-object patch",e);if(n.showDebug&&n.allPatches.push(e),_e.isPromise(e.value))return n.promisedPatches.push(e),void n.promisedPatchThen(e);if(_e.isContextPatch(e))return void n.setContext(e.path,e.value);if(_e.isMutation(e))return void n.updateMutations(e)}catch(e){console.error(e),n.errors.push(e)}})}},{key:"updateMutations",value:function(e){"object"===P()(e.value)&&!M()(e.value)&&this.allowMetaPatches&&(e.value=ie()({},e.value));var t=_e.applyPatch(this.state,e,{allowMetaPatches:this.allowMetaPatches});t&&(this.mutations.push(e),this.state=t)}},{key:"removePromisedPatch",value:function(e){var t=this.promisedPatches.indexOf(e);t<0?this.debug("Tried to remove a promisedPatch that isn't there!"):this.promisedPatches.splice(t,1)}},{key:"promisedPatchThen",value:function(e){var t=this;return e.value=e.value.then(function(n){var r=ie()({},e,{value:n});t.removePromisedPatch(e),t.updatePatches(r)}).catch(function(n){t.removePromisedPatch(e),t.updatePatches(n)})}},{key:"getMutations",value:function(e,t){return e=e||0,"number"!=typeof t&&(t=this.mutations.length),this.mutations.slice(e,t)}},{key:"getCurrentMutations",value:function(){return this.getMutationsForPlugin(this.getCurrentPlugin())}},{key:"getMutationsForPlugin",value:function(e){var t=this.getPluginMutationIndex(e);return this.getMutations(t+1)}},{key:"getCurrentPlugin",value:function(){return this.currentPlugin}},{key:"getPatchesOfType",value:function(e,t){return e.filter(t)}},{key:"getLib",value:function(){return this.libMethods}},{key:"_get",value:function(e){return _e.getIn(this.state,e)}},{key:"_getContext",value:function(e){return this.contextTree.get(e)}},{key:"setContext",value:function(e,t){return this.contextTree.set(e,t)}},{key:"_hasRun",value:function(e){return this.getPluginRunCount(this.getCurrentPlugin())>(e||0)}},{key:"_clone",value:function(e){return JSON.parse(T()(e))}},{key:"dispatch",value:function(){var e=this,t=this,n=this.nextPlugin();if(!n){var r=this.nextPromisedPatch();if(r)return r.then(function(){return e.dispatch()}).catch(function(){return e.dispatch()});var o={spec:this.state,errors:this.errors};return this.showDebug&&(o.patches=this.allPatches),Q.a.resolve(o)}if(t.pluginCount=t.pluginCount||{},t.pluginCount[n]=(t.pluginCount[n]||0)+1,t.pluginCount[n]>100)return Q.a.resolve({spec:t.state,errors:t.errors.concat(new Error("We've reached a hard limit of ".concat(100," plugin runs")))});if(n!==this.currentPlugin&&this.promisedPatches.length){var i=this.promisedPatches.map(function(e){return e.value});return Q.a.all(i.map(function(e){return e.then(Function,Function)})).then(function(){return e.dispatch()})}return function(){t.currentPlugin=n;var e=t.getCurrentMutations(),r=t.mutations.length-1;try{if(n.isGenerator){var o=!0,i=!1,s=void 0;try{for(var u,c=te()(n(e,t.getLib()));!(o=(u=c.next()).done);o=!0){a(u.value)}}catch(e){i=!0,s=e}finally{try{o||null==c.return||c.return()}finally{if(i)throw s}}}else{a(n(e,t.getLib()))}}catch(e){console.error(e),a([ie()(re()(e),{plugin:n})])}finally{t.updatePluginHistory(n,{mutationIndex:r})}return t.dispatch()}();function a(e){e&&(e=_e.fullyNormalizeArray(e),t.updatePatches(e,n))}}}]),e}();var St={refs:ot,allOf:vt,parameters:gt,properties:yt},Ct=n(29),kt=n.n(Ct),Ot=function(e){return String.prototype.toLowerCase.call(e)},At=function(e){return e.replace(/[^\w]/gi,"_")};function Tt(e){var t=e.openapi;return!!t&&w()(t,"3")}function jt(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"",r=(arguments.length>3&&void 0!==arguments[3]?arguments[3]:{}).v2OperationIdCompatibilityMode;return e&&"object"===P()(e)?(e.operationId||"").replace(/\s/g,"").length?At(e.operationId):function(e,t){if((arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).v2OperationIdCompatibilityMode){var n="".concat(t.toLowerCase(),"_").concat(e).replace(/[\s!@#$%^&*()_+=[{\]};:<>|.\/?,\\'""-]/g,"_");return(n=n||"".concat(e.substring(1),"_").concat(t)).replace(/((_){2,})/g,"_").replace(/^(_)*/g,"").replace(/([_])*$/g,"")}return"".concat(Ot(t)).concat(At(e))}(t,n,{v2OperationIdCompatibilityMode:r}):null}function Pt(e,t){return"".concat(Ot(t),"-").concat(e)}function It(e,t){return e&&e.paths?function(e,t){return Mt(e,t,!0)||null}(e,function(e){var n=e.pathName,r=e.method,o=e.operation;if(!o||"object"!==P()(o))return!1;var i=o.operationId;return[jt(o,n,r),Pt(n,r),i].some(function(e){return e&&e===t})}):null}function Mt(e,t,n){if(!e||"object"!==P()(e)||!e.paths||"object"!==P()(e.paths))return null;var r=e.paths;for(var o in r)for(var i in r[o])if("PARAMETERS"!==i.toUpperCase()){var a=r[o][i];if(a&&"object"===P()(a)){var s={spec:e,pathName:o,method:i.toUpperCase(),operation:a},u=t(s);if(n&&u)return s}}}function Nt(e){var t=e.spec,n=t.paths,r={};if(!n||t.$$normalized)return e;for(var o in n){var i=n[o];if(kt()(i)){var a=i.parameters,s=function(e){var n=i[e];if(!kt()(n))return"continue";var s=jt(n,o,e);if(s){r[s]?r[s].push(n):r[s]=[n];var u=r[s];if(u.length>1)u.forEach(function(e,t){e.__originalOperationId=e.__originalOperationId||e.operationId,e.operationId="".concat(s).concat(t+1)});else if(void 0!==n.operationId){var c=u[0];c.__originalOperationId=c.__originalOperationId||n.operationId,c.operationId=s}}if("parameters"!==e){var l=[],p={};for(var f in t)"produces"!==f&&"consumes"!==f&&"security"!==f||(p[f]=t[f],l.push(p));if(a&&(p.parameters=a,l.push(p)),l.length)for(var h=0,d=l;h1&&void 0!==arguments[1]?arguments[1]:{},n=t.requestInterceptor,r=t.responseInterceptor,o=e.withCredentials?"include":"same-origin";return function(t){return e({url:t,loadSpec:!0,requestInterceptor:n,responseInterceptor:r,headers:{Accept:Ge},credentials:o}).then(function(e){return e.body})}}function Dt(e){var t=e.fetch,n=e.spec,r=e.url,o=e.mode,i=e.allowMetaPatches,a=void 0===i||i,s=e.pathDiscriminator,u=e.modelPropertyMacro,c=e.parameterMacro,l=e.requestInterceptor,p=e.responseInterceptor,f=e.skipNormalization,h=e.useCircularStructures,d=e.http,m=e.baseDoc;return m=m||r,d=t||d||V,n?v(n):Rt(d,{requestInterceptor:l,responseInterceptor:p})(m).then(v);function v(e){m&&(St.refs.docCache[m]=e),St.refs.fetchJSON=Rt(d,{requestInterceptor:l,responseInterceptor:p});var t,n=[St.refs];return"function"==typeof c&&n.push(St.parameters),"function"==typeof u&&n.push(St.properties),"strict"!==o&&n.push(St.allOf),(t={spec:e,context:{baseDoc:m},plugins:n,allowMetaPatches:a,pathDiscriminator:s,parameterMacro:c,modelPropertyMacro:u,useCircularStructures:h},new Et(t).dispatch()).then(f?function(){var e=R()(C.a.mark(function e(t){return C.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",t);case 1:case"end":return e.stop()}},e)}));return function(t){return e.apply(this,arguments)}}():Nt)}}var Lt=n(16),Ut=n.n(Lt);function qt(e,t){var n=m()(e);if(h.a){var r=h()(e);t&&(r=r.filter(function(t){return p()(e,t).enumerable})),n.push.apply(n,r)}return n}function Ft(e){for(var t=1;t2&&void 0!==m[2]?m[2]:{},o=r.returnEntireTree,i=r.baseDoc,a=r.requestInterceptor,s=r.responseInterceptor,u=r.parameterMacro,c=r.modelPropertyMacro,l=r.useCircularStructures,p={pathDiscriminator:n,baseDoc:i,requestInterceptor:a,responseInterceptor:s,parameterMacro:u,modelPropertyMacro:c,useCircularStructures:l},f=Nt({spec:t}),h=f.spec,e.next=6,Dt(Ft({},p,{spec:h,allowMetaPatches:!0,skipNormalization:!0}));case 6:return d=e.sent,!o&&M()(n)&&n.length&&(d.spec=Ut()(d.spec,n)||null),e.abrupt("return",d);case 9:case"end":return e.stop()}},e)}))).apply(this,arguments)}var zt=n(38),Vt=n.n(zt);function Ht(e,t){var n=m()(e);if(h.a){var r=h()(e);t&&(r=r.filter(function(t){return p()(e,t).enumerable})),n.push.apply(n,r)}return n}function Wt(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:{};return function(t){var n=t.pathName,r=t.method,o=t.operationId;return function(t){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.execute(Wt({spec:e.spec},Vt()(e,"requestInterceptor","responseInterceptor","userFetch"),{pathName:n,method:r,parameters:t,operationId:o},i))}}}};var $t=n(39),Gt=n.n($t),Zt=n(40),Xt=n.n(Zt),Qt=n(41),en=n.n(Qt),tn=n(19),nn=n.n(tn),rn=n(42),on=n.n(rn),an={body:function(e){var t=e.req,n=e.value;t.body=n},header:function(e){var t=e.req,n=e.parameter,r=e.value;t.headers=t.headers||{},void 0!==r&&(t.headers[n.name]=r)},query:function(e){var t=e.req,n=e.value,r=e.parameter;t.query=t.query||{},!1===n&&"boolean"===r.type&&(n="false");0===n&&["number","integer"].indexOf(r.type)>-1&&(n="0");if(n)t.query[r.name]={collectionFormat:r.collectionFormat,value:n};else if(r.allowEmptyValue&&void 0!==n){var o=r.name;t.query[o]=t.query[o]||{},t.query[o].allowEmptyValue=!0}},path:function(e){var t=e.req,n=e.value,r=e.parameter;t.url=t.url.split("{".concat(r.name,"}")).join(encodeURIComponent(n))},formData:function(e){var t=e.req,n=e.value,r=e.parameter;(n||r.allowEmptyValue)&&(t.form=t.form||{},t.form[r.name]={value:n,allowEmptyValue:r.allowEmptyValue,collectionFormat:r.collectionFormat})}};n(49);var sn=n(43),un=n.n(sn),cn=n(44),ln=function(e){return":/?#[]@!$&'()*+,;=".indexOf(e)>-1},pn=function(e){return/^[a-z0-9\-._~]+$/i.test(e)};function fn(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).escape,n=arguments.length>2?arguments[2]:void 0;return"number"==typeof e&&(e=e.toString()),"string"==typeof e&&e.length&&t?n?JSON.parse(e):Object(cn.stringToCharArray)(e).map(function(e){return pn(e)?e:ln(e)&&"unsafe"===t?e:(un()(e)||[]).map(function(e){return"0".concat(e.toString(16).toUpperCase()).slice(-2)}).map(function(e){return"%".concat(e)}).join("")}).join(""):e}function hn(e){var t=e.value;return M()(t)?function(e){var t=e.key,n=e.value,r=e.style,o=e.explode,i=e.escape,a=function(e){return fn(e,{escape:i})};if("simple"===r)return n.map(function(e){return a(e)}).join(",");if("label"===r)return".".concat(n.map(function(e){return a(e)}).join("."));if("matrix"===r)return n.map(function(e){return a(e)}).reduce(function(e,n){return!e||o?"".concat(e||"",";").concat(t,"=").concat(n):"".concat(e,",").concat(n)},"");if("form"===r){var s=o?"&".concat(t,"="):",";return n.map(function(e){return a(e)}).join(s)}if("spaceDelimited"===r){var u=o?"".concat(t,"="):"";return n.map(function(e){return a(e)}).join(" ".concat(u))}if("pipeDelimited"===r){var c=o?"".concat(t,"="):"";return n.map(function(e){return a(e)}).join("|".concat(c))}}(e):"object"===P()(t)?function(e){var t=e.key,n=e.value,r=e.style,o=e.explode,i=e.escape,a=function(e){return fn(e,{escape:i})},s=m()(n);if("simple"===r)return s.reduce(function(e,t){var r=a(n[t]),i=o?"=":",",s=e?"".concat(e,","):"";return"".concat(s).concat(t).concat(i).concat(r)},"");if("label"===r)return s.reduce(function(e,t){var r=a(n[t]),i=o?"=":".",s=e?"".concat(e,"."):".";return"".concat(s).concat(t).concat(i).concat(r)},"");if("matrix"===r&&o)return s.reduce(function(e,t){var r=a(n[t]),o=e?"".concat(e,";"):";";return"".concat(o).concat(t,"=").concat(r)},"");if("matrix"===r)return s.reduce(function(e,r){var o=a(n[r]),i=e?"".concat(e,","):";".concat(t,"=");return"".concat(i).concat(r,",").concat(o)},"");if("form"===r)return s.reduce(function(e,t){var r=a(n[t]),i=e?"".concat(e).concat(o?"&":","):"",s=o?"=":",";return"".concat(i).concat(t).concat(s).concat(r)},"")}(e):function(e){var t=e.key,n=e.value,r=e.style,o=e.escape,i=function(e){return fn(e,{escape:o})};if("simple"===r)return i(n);if("label"===r)return".".concat(i(n));if("matrix"===r)return";".concat(t,"=").concat(i(n));if("form"===r)return i(n);if("deepObject"===r)return i(n)}(e)}function dn(e,t){return t.includes("application/json")?"string"==typeof e?e:T()(e):e.toString()}function mn(e){var t=e.req,n=e.value,r=e.parameter,o=r.name,i=r.style,a=r.explode,s=r.content;if(s){var u=m()(s)[0];t.url=t.url.split("{".concat(o,"}")).join(fn(dn(n,u),{escape:!0}))}else{var c=hn({key:r.name,value:n,style:i||"simple",explode:a||!1,escape:!0});t.url=t.url.split("{".concat(o,"}")).join(c)}}function vn(e){var t=e.req,n=e.value,r=e.parameter;if(t.query=t.query||{},r.content){var o=m()(r.content)[0];t.query[r.name]=dn(n,o)}else if(!1===n&&(n="false"),0===n&&(n="0"),n){var i=P()(n);if("deepObject"===r.style)m()(n).forEach(function(e){var o=n[e];t.query["".concat(r.name,"[").concat(e,"]")]={value:hn({key:e,value:o,style:"deepObject",escape:r.allowReserved?"unsafe":"reserved"}),skipEncoding:!0}});else if("object"!==i||M()(n)||"form"!==r.style&&r.style||!r.explode&&void 0!==r.explode)t.query[r.name]={value:hn({key:r.name,value:n,style:r.style||"form",explode:void 0===r.explode||r.explode,escape:r.allowReserved?"unsafe":"reserved"}),skipEncoding:!0};else{m()(n).forEach(function(e){var o=n[e];t.query[e]={value:hn({key:e,value:o,style:r.style||"form",escape:r.allowReserved?"unsafe":"reserved"}),skipEncoding:!0}})}}else if(r.allowEmptyValue&&void 0!==n){var a=r.name;t.query[a]=t.query[a]||{},t.query[a].allowEmptyValue=!0}}var gn=["accept","authorization","content-type"];function yn(e){var t=e.req,n=e.parameter,r=e.value;if(t.headers=t.headers||{},!(gn.indexOf(n.name.toLowerCase())>-1))if(n.content){var o=m()(n.content)[0];t.headers[n.name]=dn(r,o)}else void 0!==r&&(t.headers[n.name]=hn({key:n.name,value:r,style:n.style||"simple",explode:void 0!==n.explode&&n.explode,escape:!1}))}function bn(e){var t=e.req,n=e.parameter,r=e.value;t.headers=t.headers||{};var o=P()(r);if(n.content){var i=m()(n.content)[0];t.headers.Cookie="".concat(n.name,"=").concat(dn(r,i))}else if("undefined"!==o){var a="object"===o&&!M()(r)&&n.explode?"":"".concat(n.name,"=");t.headers.Cookie=a+hn({key:n.name,value:r,escape:!1,style:n.style||"form",explode:void 0!==n.explode&&n.explode})}}var _n=n(30),wn=function(e,t){var n=e.operation,r=e.requestBody,o=e.securities,i=e.spec,a=e.attachContentTypeForEmptyPayload,s=e.requestContentType;t=function(e){var t=e.request,n=e.securities,r=void 0===n?{}:n,o=e.operation,i=void 0===o?{}:o,a=e.spec,s=b()({},t),u=r.authorized,c=void 0===u?{}:u,l=i.security||a.security||[],p=c&&!!m()(c).length,f=Ut()(a,["components","securitySchemes"])||{};if(s.headers=s.headers||{},s.query=s.query||{},!m()(r).length||!p||!l||M()(i.security)&&!i.security.length)return t;return l.forEach(function(e,t){for(var n in e){var r=c[n],o=f[n];if(r){var i=r.value||r,a=o.type;if(r)if("apiKey"===a)"query"===o.in&&(s.query[o.name]=i),"header"===o.in&&(s.headers[o.name]=i),"cookie"===o.in&&(s.cookies[o.name]=i);else if("http"===a){if("basic"===o.scheme){var u=i.username,l=i.password,p=nn()("".concat(u,":").concat(l));s.headers.Authorization="Basic ".concat(p)}"bearer"===o.scheme&&(s.headers.Authorization="Bearer ".concat(i))}else if("oauth2"===a){var h=r.token||{},d=h.access_token,m=h.token_type;m&&"bearer"!==m.toLowerCase()||(m="Bearer"),s.headers.Authorization="".concat(m," ").concat(d)}}}}),s}({request:t,securities:o,operation:n,spec:i});var u=n.requestBody||{},c=m()(u.content||{}),l=s&&c.indexOf(s)>-1;if(r||a){if(s&&l)t.headers["Content-Type"]=s;else if(!s){var p=c[0];p&&(t.headers["Content-Type"]=p,s=p)}}else s&&l&&(t.headers["Content-Type"]=s);return r&&(s?c.indexOf(s)>-1&&("application/x-www-form-urlencoded"===s||0===s.indexOf("multipart/")?"object"===P()(r)?(t.form={},m()(r).forEach(function(e){var n,o,i=r[e];"undefined"!=typeof File&&(o=i instanceof File),"undefined"!=typeof Blob&&(o=o||i instanceof Blob),void 0!==_n.Buffer&&(o=o||_n.Buffer.isBuffer(i)),n="object"!==P()(i)||o?i:M()(i)?i.toString():T()(i),t.form[e]={value:n}})):t.form=r:t.body=r):t.body=r),t};var xn=function(e,t){var n=e.spec,r=e.operation,o=e.securities,i=e.requestContentType,a=e.attachContentTypeForEmptyPayload;if((t=function(e){var t=e.request,n=e.securities,r=void 0===n?{}:n,o=e.operation,i=void 0===o?{}:o,a=e.spec,s=b()({},t),u=r.authorized,c=void 0===u?{}:u,l=r.specSecurity,p=void 0===l?[]:l,f=i.security||p,h=c&&!!m()(c).length,d=a.securityDefinitions;if(s.headers=s.headers||{},s.query=s.query||{},!m()(r).length||!h||!f||M()(i.security)&&!i.security.length)return t;return f.forEach(function(e,t){for(var n in e){var r=c[n];if(r){var o=r.token,i=r.value||r,a=d[n],u=a.type,l=a["x-tokenName"]||"access_token",p=o&&o[l],f=o&&o.token_type;if(r)if("apiKey"===u){var h="query"===a.in?"query":"headers";s[h]=s[h]||{},s[h][a.name]=i}else"basic"===u?i.header?s.headers.authorization=i.header:(i.base64=nn()("".concat(i.username,":").concat(i.password)),s.headers.authorization="Basic ".concat(i.base64)):"oauth2"===u&&p&&(f=f&&"bearer"!==f.toLowerCase()?f:"Bearer",s.headers.authorization="".concat(f," ").concat(p))}}}),s}({request:t,securities:o,operation:r,spec:n})).body||t.form||a)i?t.headers["Content-Type"]=i:M()(r.consumes)?t.headers["Content-Type"]=r.consumes[0]:M()(n.consumes)?t.headers["Content-Type"]=n.consumes[0]:r.parameters&&r.parameters.filter(function(e){return"file"===e.type}).length?t.headers["Content-Type"]="multipart/form-data":r.parameters&&r.parameters.filter(function(e){return"formData"===e.in}).length&&(t.headers["Content-Type"]="application/x-www-form-urlencoded");else if(i){var s=r.parameters&&r.parameters.filter(function(e){return"body"===e.in}).length>0,u=r.parameters&&r.parameters.filter(function(e){return"formData"===e.in}).length>0;(s||u)&&(t.headers["Content-Type"]=i)}return t};function En(e,t){var n=m()(e);if(h.a){var r=h()(e);t&&(r=r.filter(function(t){return p()(e,t).enumerable})),n.push.apply(n,r)}return n}function Sn(e){for(var t=1;t-1&&(c=o,l=u[p.indexOf(o)])}return!c&&u&&u.length&&(c=u[0].url,l=u[0]),c.indexOf("{")>-1&&function(e){for(var t,n=[],r=/{([^}]+)}/g;t=r.exec(e);)n.push(t[1]);return n}(c).forEach(function(e){if(l.variables&&l.variables[e]){var t=l.variables[e],n=s[e]||t.default,r=new RegExp("{".concat(e,"}"),"g");c=c.replace(r,n)}}),function(){var e,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",r=E.a.parse(t),o=E.a.parse(n),i=Pn(r.protocol)||Pn(o.protocol)||"",a=r.host||o.host,s=r.pathname||"";return"/"===(e=i&&a?"".concat(i,"://").concat(a+s):s)[e.length-1]?e.slice(0,-1):e}(c,i)}(b):function(e){var t,n=e.spec,r=e.scheme,o=e.contextUrl,i=void 0===o?"":o,a=E.a.parse(i),s=M()(n.schemes)?n.schemes[0]:null,u=r||s||Pn(a.protocol)||"http",c=n.host||a.host||"",l=n.basePath||"";return"/"===(t=u&&c?"".concat(u,"://").concat(c+l):l)[t.length-1]?t.slice(0,-1):t}(b),!n)return delete g.cookies,g;g.url+=S,g.method="".concat(x).toUpperCase(),h=h||{};var C=t.paths[S]||{};o&&(g.headers.accept=o);var k=An([].concat(Cn(w.parameters)).concat(Cn(C.parameters)));k.forEach(function(e){var n,r=d[e.in];if("body"===e.in&&e.schema&&e.schema.properties&&(n=h),void 0===(n=e&&e.name&&h[e.name])?n=e&&e.name&&h["".concat(e.in,".").concat(e.name)]:On(e.name,k).length>1&&console.warn("Parameter '".concat(e.name,"' is ambiguous because the defined spec has more than one parameter with the name: '").concat(e.name,"' and the passed-in parameter values did not define an 'in' value.")),null!==n){if(void 0!==e.default&&void 0===n&&(n=e.default),void 0===n&&e.required&&!e.allowEmptyValue)throw new Error("Required parameter ".concat(e.name," is not provided"));if(v&&e.schema&&"object"===e.schema.type&&"string"==typeof n)try{n=JSON.parse(n)}catch(e){throw new Error("Could not parse object parameter value string as JSON")}r&&r({req:g,parameter:e,value:n,operation:w,spec:t})}});var O=Sn({},e,{operation:w});if((g=v?wn(O,g):xn(O,g)).cookies&&m()(g.cookies).length){var A=m()(g.cookies).reduce(function(e,t){var n=g.cookies[t];return e+(e?"&":"")+on.a.serialize(t,n)},"");g.headers.Cookie=A}return g.cookies&&delete g.cookies,Z(g),g}var Pn=function(e){return e?e.replace(/\W/g,""):null};function In(e,t){var n=m()(e);if(h.a){var r=h()(e);t&&(r=r.filter(function(t){return p()(e,t).enumerable})),n.push.apply(n,r)}return n}function Mn(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if("string"==typeof e?n.url=e:n=e,!(this instanceof Mn))return new Mn(n);b()(this,n);var r=this.resolve().then(function(){return t.disableInterfaces||b()(t,Mn.makeApisTagOperation(t)),t});return r.client=this,r}Mn.http=V,Mn.makeHttp=function(e,t,n){return n=n||function(e){return e},t=t||function(e){return e},function(r){return"string"==typeof r&&(r={url:r}),z.mergeInQueryOrForm(r),r=t(r),n(e(r))}}.bind(null,Mn.http),Mn.resolve=Dt,Mn.resolveSubtree=function(e,t){return Bt.apply(this,arguments)},Mn.execute=function(e){var t=e.http,n=e.fetch,r=e.spec,o=e.operationId,i=e.pathName,a=e.method,s=e.parameters,u=e.securities,c=Gt()(e,["http","fetch","spec","operationId","pathName","method","parameters","securities"]),l=t||n||V;i&&a&&!o&&(o=Pt(i,a));var p=Tn.buildRequest(Sn({spec:r,operationId:o,parameters:s,securities:u,http:l},c));return p.body&&(Xt()(p.body)||en()(p.body))&&(p.body=T()(p.body)),l(p)},Mn.serializeRes=J,Mn.serializeHeaders=K,Mn.clearCache=function(){St.refs.clearCache()},Mn.makeApisTagOperation=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=Yt.makeExecute(e);return{apis:Yt.mapTagOperations({v2OperationIdCompatibilityMode:e.v2OperationIdCompatibilityMode,spec:e.spec,cb:t})}},Mn.buildRequest=jn,Mn.helpers={opId:jt},Mn.prototype={http:V,execute:function(e){return this.applyDefaults(),Mn.execute(function(e){for(var t=1;t + * @license MIT + */ +var r=n(569),o=n(570),i=n(355);function a(){return u.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function s(e,t){if(a()=a())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+a().toString(16)+" bytes");return 0|e}function d(e,t){if(u.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return B(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return z(e).length;default:if(r)return B(e).length;t=(""+t).toLowerCase(),r=!0}}function m(e,t,n){var r=!1;if((void 0===t||t<0)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return j(this,t,n);case"utf8":case"utf-8":return k(this,t,n);case"ascii":return A(this,t,n);case"latin1":case"binary":return T(this,t,n);case"base64":return C(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return P(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}function v(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function g(e,t,n,r,o){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=o?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(o)return-1;n=e.length-1}else if(n<0){if(!o)return-1;n=0}if("string"==typeof t&&(t=u.from(t,r)),u.isBuffer(t))return 0===t.length?-1:y(e,t,n,r,o);if("number"==typeof t)return t&=255,u.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?o?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):y(e,[t],n,r,o);throw new TypeError("val must be string, number or Buffer")}function y(e,t,n,r,o){var i,a=1,s=e.length,u=t.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;a=2,s/=2,u/=2,n/=2}function c(e,t){return 1===a?e[t]:e.readUInt16BE(t*a)}if(o){var l=-1;for(i=n;is&&(n=s-u),i=n;i>=0;i--){for(var p=!0,f=0;fo&&(r=o):r=o;var i=t.length;if(i%2!=0)throw new TypeError("Invalid hex string");r>i/2&&(r=i/2);for(var a=0;a>8,o=n%256,i.push(o),i.push(r);return i}(t,e.length-n),e,n,r)}function C(e,t,n){return 0===t&&n===e.length?r.fromByteArray(e):r.fromByteArray(e.slice(t,n))}function k(e,t,n){n=Math.min(e.length,n);for(var r=[],o=t;o239?4:c>223?3:c>191?2:1;if(o+p<=n)switch(p){case 1:c<128&&(l=c);break;case 2:128==(192&(i=e[o+1]))&&(u=(31&c)<<6|63&i)>127&&(l=u);break;case 3:i=e[o+1],a=e[o+2],128==(192&i)&&128==(192&a)&&(u=(15&c)<<12|(63&i)<<6|63&a)>2047&&(u<55296||u>57343)&&(l=u);break;case 4:i=e[o+1],a=e[o+2],s=e[o+3],128==(192&i)&&128==(192&a)&&128==(192&s)&&(u=(15&c)<<18|(63&i)<<12|(63&a)<<6|63&s)>65535&&u<1114112&&(l=u)}null===l?(l=65533,p=1):l>65535&&(l-=65536,r.push(l>>>10&1023|55296),l=56320|1023&l),r.push(l),o+=p}return function(e){var t=e.length;if(t<=O)return String.fromCharCode.apply(String,e);var n="",r=0;for(;r0&&(e=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(e+=" ... ")),""},u.prototype.compare=function(e,t,n,r,o){if(!u.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===o&&(o=this.length),t<0||n>e.length||r<0||o>this.length)throw new RangeError("out of range index");if(r>=o&&t>=n)return 0;if(r>=o)return-1;if(t>=n)return 1;if(this===e)return 0;for(var i=(o>>>=0)-(r>>>=0),a=(n>>>=0)-(t>>>=0),s=Math.min(i,a),c=this.slice(r,o),l=e.slice(t,n),p=0;po)&&(n=o),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var i=!1;;)switch(r){case"hex":return b(this,e,t,n);case"utf8":case"utf-8":return _(this,e,t,n);case"ascii":return w(this,e,t,n);case"latin1":case"binary":return x(this,e,t,n);case"base64":return E(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return S(this,e,t,n);default:if(i)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),i=!0}},u.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var O=4096;function A(e,t,n){var r="";n=Math.min(e.length,n);for(var o=t;or)&&(n=r);for(var o="",i=t;in)throw new RangeError("Trying to access beyond buffer length")}function M(e,t,n,r,o,i){if(!u.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>o||te.length)throw new RangeError("Index out of range")}function N(e,t,n,r){t<0&&(t=65535+t+1);for(var o=0,i=Math.min(e.length-n,2);o>>8*(r?o:1-o)}function R(e,t,n,r){t<0&&(t=4294967295+t+1);for(var o=0,i=Math.min(e.length-n,4);o>>8*(r?o:3-o)&255}function D(e,t,n,r,o,i){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function L(e,t,n,r,i){return i||D(e,0,n,4),o.write(e,t,n,r,23,4),n+4}function U(e,t,n,r,i){return i||D(e,0,n,8),o.write(e,t,n,r,52,8),n+8}u.prototype.slice=function(e,t){var n,r=this.length;if((e=~~e)<0?(e+=r)<0&&(e=0):e>r&&(e=r),(t=void 0===t?r:~~t)<0?(t+=r)<0&&(t=0):t>r&&(t=r),t0&&(o*=256);)r+=this[e+--t]*o;return r},u.prototype.readUInt8=function(e,t){return t||I(e,1,this.length),this[e]},u.prototype.readUInt16LE=function(e,t){return t||I(e,2,this.length),this[e]|this[e+1]<<8},u.prototype.readUInt16BE=function(e,t){return t||I(e,2,this.length),this[e]<<8|this[e+1]},u.prototype.readUInt32LE=function(e,t){return t||I(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},u.prototype.readUInt32BE=function(e,t){return t||I(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},u.prototype.readIntLE=function(e,t,n){e|=0,t|=0,n||I(e,t,this.length);for(var r=this[e],o=1,i=0;++i=(o*=128)&&(r-=Math.pow(2,8*t)),r},u.prototype.readIntBE=function(e,t,n){e|=0,t|=0,n||I(e,t,this.length);for(var r=t,o=1,i=this[e+--r];r>0&&(o*=256);)i+=this[e+--r]*o;return i>=(o*=128)&&(i-=Math.pow(2,8*t)),i},u.prototype.readInt8=function(e,t){return t||I(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},u.prototype.readInt16LE=function(e,t){t||I(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt16BE=function(e,t){t||I(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt32LE=function(e,t){return t||I(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},u.prototype.readInt32BE=function(e,t){return t||I(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},u.prototype.readFloatLE=function(e,t){return t||I(e,4,this.length),o.read(this,e,!0,23,4)},u.prototype.readFloatBE=function(e,t){return t||I(e,4,this.length),o.read(this,e,!1,23,4)},u.prototype.readDoubleLE=function(e,t){return t||I(e,8,this.length),o.read(this,e,!0,52,8)},u.prototype.readDoubleBE=function(e,t){return t||I(e,8,this.length),o.read(this,e,!1,52,8)},u.prototype.writeUIntLE=function(e,t,n,r){(e=+e,t|=0,n|=0,r)||M(this,e,t,n,Math.pow(2,8*n)-1,0);var o=1,i=0;for(this[t]=255&e;++i=0&&(i*=256);)this[t+o]=e/i&255;return t+n},u.prototype.writeUInt8=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,1,255,0),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},u.prototype.writeUInt16LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):N(this,e,t,!0),t+2},u.prototype.writeUInt16BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):N(this,e,t,!1),t+2},u.prototype.writeUInt32LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):R(this,e,t,!0),t+4},u.prototype.writeUInt32BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):R(this,e,t,!1),t+4},u.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);M(this,e,t,n,o-1,-o)}var i=0,a=1,s=0;for(this[t]=255&e;++i>0)-s&255;return t+n},u.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);M(this,e,t,n,o-1,-o)}var i=n-1,a=1,s=0;for(this[t+i]=255&e;--i>=0&&(a*=256);)e<0&&0===s&&0!==this[t+i+1]&&(s=1),this[t+i]=(e/a>>0)-s&255;return t+n},u.prototype.writeInt8=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,1,127,-128),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},u.prototype.writeInt16LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):N(this,e,t,!0),t+2},u.prototype.writeInt16BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):N(this,e,t,!1),t+2},u.prototype.writeInt32LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,2147483647,-2147483648),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):R(this,e,t,!0),t+4},u.prototype.writeInt32BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):R(this,e,t,!1),t+4},u.prototype.writeFloatLE=function(e,t,n){return L(this,e,t,!0,n)},u.prototype.writeFloatBE=function(e,t,n){return L(this,e,t,!1,n)},u.prototype.writeDoubleLE=function(e,t,n){return U(this,e,t,!0,n)},u.prototype.writeDoubleBE=function(e,t,n){return U(this,e,t,!1,n)},u.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t=0;--o)e[o+t]=this[o+n];else if(i<1e3||!u.TYPED_ARRAY_SUPPORT)for(o=0;o>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(i=t;i55295&&n<57344){if(!o){if(n>56319){(t-=3)>-1&&i.push(239,191,189);continue}if(a+1===r){(t-=3)>-1&&i.push(239,191,189);continue}o=n;continue}if(n<56320){(t-=3)>-1&&i.push(239,191,189),o=n;continue}n=65536+(o-55296<<10|n-56320)}else o&&(t-=3)>-1&&i.push(239,191,189);if(o=null,n<128){if((t-=1)<0)break;i.push(n)}else if(n<2048){if((t-=2)<0)break;i.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;i.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;i.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return i}function z(e){return r.toByteArray(function(e){if((e=function(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}(e).replace(q,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function V(e,t,n,r){for(var o=0;o=t.length||o>=e.length);++o)t[o+n]=e[o];return o}}).call(this,n(36))},function(e,t,n){"use strict";e.exports={current:null}},function(e,t){e.exports=function(e){return null!=e&&"object"==typeof e}},function(e,t){var n,r,o=e.exports={};function i(){throw new Error("setTimeout has not been defined")}function a(){throw new Error("clearTimeout has not been defined")}function s(e){if(n===setTimeout)return setTimeout(e,0);if((n===i||!n)&&setTimeout)return n=setTimeout,setTimeout(e,0);try{return n(e,0)}catch(t){try{return n.call(null,e,0)}catch(t){return n.call(this,e,0)}}}!function(){try{n="function"==typeof setTimeout?setTimeout:i}catch(e){n=i}try{r="function"==typeof clearTimeout?clearTimeout:a}catch(e){r=a}}();var u,c=[],l=!1,p=-1;function f(){l&&u&&(l=!1,u.length?c=u.concat(c):p=-1,c.length&&h())}function h(){if(!l){var e=s(f);l=!0;for(var t=c.length;t;){for(u=c,c=[];++p1)for(var n=1;n0&&"/"!==t[0]});function oe(e,t,n){return t=t||[],te.apply(void 0,[e].concat(u()(t))).get("parameters",Object(p.List)()).reduce(function(e,t){var r=n&&"body"===t.get("in")?t.get("value_xml"):t.get("value");return e.set(Object(l.B)(t,{allowHashes:!1}),r)},Object(p.fromJS)({}))}function ie(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(p.List.isList(e))return e.some(function(e){return p.Map.isMap(e)&&e.get("in")===t})}function ae(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(p.List.isList(e))return e.some(function(e){return p.Map.isMap(e)&&e.get("type")===t})}function se(e,t){t=t||[];var n=x(e).getIn(["paths"].concat(u()(t)),Object(p.fromJS)({})),r=e.getIn(["meta","paths"].concat(u()(t)),Object(p.fromJS)({})),o=ue(e,t),i=n.get("parameters")||new p.List,a=r.get("consumes_value")?r.get("consumes_value"):ae(i,"file")?"multipart/form-data":ae(i,"formData")?"application/x-www-form-urlencoded":void 0;return Object(p.fromJS)({requestContentType:a,responseContentType:o})}function ue(e,t){t=t||[];var n=x(e).getIn(["paths"].concat(u()(t)),null);if(null!==n){var r=e.getIn(["meta","paths"].concat(u()(t),["produces_value"]),null),o=n.getIn(["produces",0],null);return r||o||"application/json"}}function ce(e,t){t=t||[];var n=x(e),r=n.getIn(["paths"].concat(u()(t)),null);if(null!==r){var o=t,i=a()(o,1)[0],s=r.get("produces",null),c=n.getIn(["paths",i,"produces"],null),l=n.getIn(["produces"],null);return s||c||l}}function le(e,t){t=t||[];var n=x(e),r=n.getIn(["paths"].concat(u()(t)),null);if(null!==r){var o=t,i=a()(o,1)[0],s=r.get("consumes",null),c=n.getIn(["paths",i,"consumes"],null),l=n.getIn(["consumes"],null);return s||c||l}}var pe=function(e,t,n){var r=e.get("url").match(/^([a-z][a-z0-9+\-.]*):/),i=o()(r)?r[1]:null;return e.getIn(["scheme",t,n])||e.getIn(["scheme","_defaultScheme"])||i||""},fe=function(e,t,n){return["http","https"].indexOf(pe(e,t,n))>-1},he=function(e,t){t=t||[];var n=e.getIn(["meta","paths"].concat(u()(t),["parameters"]),Object(p.fromJS)([])),r=!0;return n.forEach(function(e){var t=e.get("errors");t&&t.count()&&(r=!1)}),r};function de(e){return p.Map.isMap(e)?e:new p.Map}},function(e,t,n){"use strict";n.r(t),n.d(t,"SHOW_AUTH_POPUP",function(){return d}),n.d(t,"AUTHORIZE",function(){return m}),n.d(t,"LOGOUT",function(){return v}),n.d(t,"PRE_AUTHORIZE_OAUTH2",function(){return g}),n.d(t,"AUTHORIZE_OAUTH2",function(){return y}),n.d(t,"VALIDATE",function(){return b}),n.d(t,"CONFIGURE_AUTH",function(){return _}),n.d(t,"showDefinitions",function(){return w}),n.d(t,"authorize",function(){return x}),n.d(t,"logout",function(){return E}),n.d(t,"preAuthorizeImplicit",function(){return S}),n.d(t,"authorizeOauth2",function(){return C}),n.d(t,"authorizePassword",function(){return k}),n.d(t,"authorizeApplication",function(){return O}),n.d(t,"authorizeAccessCodeWithFormParams",function(){return A}),n.d(t,"authorizeAccessCodeWithBasicAuthentication",function(){return T}),n.d(t,"authorizeRequest",function(){return j}),n.d(t,"configureAuth",function(){return P});var r=n(26),o=n.n(r),i=n(16),a=n.n(i),s=n(28),u=n.n(s),c=n(95),l=n.n(c),p=n(18),f=n.n(p),h=n(3),d="show_popup",m="authorize",v="logout",g="pre_authorize_oauth2",y="authorize_oauth2",b="validate",_="configure_auth";function w(e){return{type:d,payload:e}}function x(e){return{type:m,payload:e}}function E(e){return{type:v,payload:e}}var S=function(e){return function(t){var n=t.authActions,r=t.errActions,o=e.auth,i=e.token,a=e.isValid,s=o.schema,c=o.name,l=s.get("flow");delete f.a.swaggerUIRedirectOauth2,"accessCode"===l||a||r.newAuthErr({authId:c,source:"auth",level:"warning",message:"Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"}),i.error?r.newAuthErr({authId:c,source:"auth",level:"error",message:u()(i)}):n.authorizeOauth2({auth:o,token:i})}};function C(e){return{type:y,payload:e}}var k=function(e){return function(t){var n=t.authActions,r=e.schema,o=e.name,i=e.username,s=e.password,u=e.passwordType,c=e.clientId,l=e.clientSecret,p={grant_type:"password",scope:e.scopes.join(" "),username:i,password:s},f={};switch(u){case"request-body":!function(e,t,n){t&&a()(e,{client_id:t});n&&a()(e,{client_secret:n})}(p,c,l);break;case"basic":f.Authorization="Basic "+Object(h.a)(c+":"+l);break;default:console.warn("Warning: invalid passwordType ".concat(u," was passed, not including client id and secret"))}return n.authorizeRequest({body:Object(h.b)(p),url:r.get("tokenUrl"),name:o,headers:f,query:{},auth:e})}};var O=function(e){return function(t){var n=t.authActions,r=e.schema,o=e.scopes,i=e.name,a=e.clientId,s=e.clientSecret,u={Authorization:"Basic "+Object(h.a)(a+":"+s)},c={grant_type:"client_credentials",scope:o.join(" ")};return n.authorizeRequest({body:Object(h.b)(c),name:i,url:r.get("tokenUrl"),auth:e,headers:u})}},A=function(e){var t=e.auth,n=e.redirectUrl;return function(e){var r=e.authActions,o=t.schema,i=t.name,a=t.clientId,s=t.clientSecret,u=t.codeVerifier,c={grant_type:"authorization_code",code:t.code,client_id:a,client_secret:s,redirect_uri:n,code_verifier:u};return r.authorizeRequest({body:Object(h.b)(c),name:i,url:o.get("tokenUrl"),auth:t})}},T=function(e){var t=e.auth,n=e.redirectUrl;return function(e){var r=e.authActions,o=t.schema,i=t.name,a=t.clientId,s=t.clientSecret,u={Authorization:"Basic "+Object(h.a)(a+":"+s)},c={grant_type:"authorization_code",code:t.code,client_id:a,redirect_uri:n};return r.authorizeRequest({body:Object(h.b)(c),name:i,url:o.get("tokenUrl"),auth:t,headers:u})}},j=function(e){return function(t){var n,r=t.fn,i=t.getConfigs,s=t.authActions,c=t.errActions,p=t.oas3Selectors,f=t.specSelectors,h=t.authSelectors,d=e.body,m=e.query,v=void 0===m?{}:m,g=e.headers,y=void 0===g?{}:g,b=e.name,_=e.url,w=e.auth,x=(h.getConfigs()||{}).additionalQueryStringParams;n=f.isOAS3()?l()(_,p.selectedServer(),!0):l()(_,f.url(),!0),"object"===o()(x)&&(n.query=a()({},n.query,x));var E=n.toString(),S=a()({Accept:"application/json, text/plain, */*","Content-Type":"application/x-www-form-urlencoded","X-Requested-With":"XMLHttpRequest"},y);r.fetch({url:E,method:"post",headers:S,query:v,body:d,requestInterceptor:i().requestInterceptor,responseInterceptor:i().responseInterceptor}).then(function(e){var t=JSON.parse(e.data),n=t&&(t.error||""),r=t&&(t.parseError||"");e.ok?n||r?c.newAuthErr({authId:b,level:"error",source:"auth",message:u()(t)}):s.authorizeOauth2({auth:w,token:t}):c.newAuthErr({authId:b,level:"error",source:"auth",message:e.statusText})}).catch(function(e){var t=new Error(e).message;if(e.response&&e.response.data){var n=e.response.data;try{var r="string"==typeof n?JSON.parse(n):n;r.error&&(t+=", error: ".concat(r.error)),r.error_description&&(t+=", description: ".concat(r.error_description))}catch(e){}}c.newAuthErr({authId:b,level:"error",source:"auth",message:t})})}};function P(e){return{type:_,payload:e}}},function(e,t){var n=e.exports={version:"2.6.5"};"number"==typeof __e&&(__e=n)},function(e,t){e.exports=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e}},function(e,t,n){var r=n(127),o=Math.min;e.exports=function(e){return e>0?o(r(e),9007199254740991):0}},function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},function(e,t,n){var r=n(211),o=n(210);e.exports=function(e){return r(o(e))}},function(e,t,n){var r=n(49),o=n(133);e.exports=n(50)?function(e,t,n){return r.f(e,t,o(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t,n){"use strict";e.exports=function(e){if("function"!=typeof e)throw new TypeError(e+" is not a function");return e}},function(e,t,n){"use strict";n.r(t),n.d(t,"UPDATE_LAYOUT",function(){return o}),n.d(t,"UPDATE_FILTER",function(){return i}),n.d(t,"UPDATE_MODE",function(){return a}),n.d(t,"SHOW",function(){return s}),n.d(t,"updateLayout",function(){return u}),n.d(t,"updateFilter",function(){return c}),n.d(t,"show",function(){return l}),n.d(t,"changeMode",function(){return p});var r=n(3),o="layout_update_layout",i="layout_update_filter",a="layout_update_mode",s="layout_show";function u(e){return{type:o,payload:e}}function c(e){return{type:i,payload:e}}function l(e){var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];return e=Object(r.w)(e),{type:s,payload:{thing:e,shown:t}}}function p(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return e=Object(r.w)(e),{type:a,payload:{thing:e,mode:t}}}},function(e,t,n){"use strict";(function(t){ +/*! + * @description Recursive object extending + * @author Viacheslav Lotsmanov + * @license MIT + * + * The MIT License (MIT) + * + * Copyright (c) 2013-2018 Viacheslav Lotsmanov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +function n(e){return e instanceof t||e instanceof Date||e instanceof RegExp}function r(e){if(e instanceof t){var n=t.alloc?t.alloc(e.length):new t(e.length);return e.copy(n),n}if(e instanceof Date)return new Date(e.getTime());if(e instanceof RegExp)return new RegExp(e);throw new Error("Unexpected situation")}function o(e){var t=[];return e.forEach(function(e,i){"object"==typeof e&&null!==e?Array.isArray(e)?t[i]=o(e):n(e)?t[i]=r(e):t[i]=a({},e):t[i]=e}),t}function i(e,t){return"__proto__"===t?void 0:e[t]}var a=e.exports=function(){if(arguments.length<1||"object"!=typeof arguments[0])return!1;if(arguments.length<2)return arguments[0];var e,t,s=arguments[0],u=Array.prototype.slice.call(arguments,1);return u.forEach(function(u){"object"!=typeof u||null===u||Array.isArray(u)||Object.keys(u).forEach(function(c){return t=i(s,c),(e=i(u,c))===s?void 0:"object"!=typeof e||null===e?void(s[c]=e):Array.isArray(e)?void(s[c]=o(e)):n(e)?void(s[c]=r(e)):"object"!=typeof t||null===t||Array.isArray(t)?void(s[c]=a({},e)):void(s[c]=a(t,e))})}),s}}).call(this,n(64).Buffer)},function(e,t,n){var r=n(151),o=n(336);e.exports=n(126)?function(e,t,n){return r.f(e,t,o(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,t,n){var r=n(106),o=n(603),i=n(604),a="[object Null]",s="[object Undefined]",u=r?r.toStringTag:void 0;e.exports=function(e){return null==e?void 0===e?s:a:u&&u in Object(e)?o(e):i(e)}},function(e,t,n){var r=n(621),o=n(624);e.exports=function(e,t){var n=o(e,t);return r(n)?n:void 0}},function(e,t,n){var r=n(380),o=n(661),i=n(107);e.exports=function(e){return i(e)?r(e):o(e)}},function(e,t,n){"use strict";var r=n(178),o=Object.keys||function(e){var t=[];for(var n in e)t.push(n);return t};e.exports=p;var i=n(137);i.inherits=n(47);var a=n(390),s=n(240);i.inherits(p,a);for(var u=o(s.prototype),c=0;c=t.length?{value:void 0,done:!0}:(e=r(t,n),this._i+=e.length,{value:e,done:!1})})},function(e,t){e.exports={}},function(e,t,n){n(561);for(var r=n(32),o=n(77),i=n(102),a=n(34)("toStringTag"),s="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),u=0;u1){for(var d=Array(h),m=0;m1){for(var g=Array(v),y=0;y=this._finalSize&&(this._update(this._block),this._block.fill(0));var n=8*this._len;if(n<=4294967295)this._block.writeUInt32BE(n,this._blockSize-4);else{var r=(4294967295&n)>>>0,o=(n-r)/4294967296;this._block.writeUInt32BE(o,this._blockSize-8),this._block.writeUInt32BE(r,this._blockSize-4)}this._update(this._block);var i=this._hash();return e?i.toString(e):i},o.prototype._update=function(){throw new Error("_update must be implemented by subclass")},e.exports=o},function(e,t,n){var r=n(63),o=n(406),i=n(407),a=n(46),s=n(158),u=n(225),c={},l={};(t=e.exports=function(e,t,n,p,f){var h,d,m,v,g=f?function(){return e}:u(e),y=r(n,p,t?2:1),b=0;if("function"!=typeof g)throw TypeError(e+" is not iterable!");if(i(g)){for(h=s(e.length);h>b;b++)if((v=t?y(a(d=e[b])[0],d[1]):y(e[b]))===c||v===l)return v}else for(m=g.call(e);!(d=m.next()).done;)if((v=o(m,y,d.value,t))===c||v===l)return v}).BREAK=c,t.RETURN=l},function(e,t,n){"use strict";function r(e){return null==e}e.exports.isNothing=r,e.exports.isObject=function(e){return"object"==typeof e&&null!==e},e.exports.toArray=function(e){return Array.isArray(e)?e:r(e)?[]:[e]},e.exports.repeat=function(e,t){var n,r="";for(n=0;n1&&void 0!==arguments[1]?arguments[1]:{},r=Object(i.A)(t),a=r.type,s=r.example,u=r.properties,c=r.additionalProperties,l=r.items,p=n.includeReadOnly,f=n.includeWriteOnly;if(void 0!==s)return Object(i.e)(s,"$$ref",function(e){return"string"==typeof e&&e.indexOf("#")>-1});if(!a)if(u)a="object";else{if(!l)return;a="array"}if("object"===a){var d=Object(i.A)(u),m={};for(var v in d)d[v]&&d[v].deprecated||d[v]&&d[v].readOnly&&!p||d[v]&&d[v].writeOnly&&!f||(m[v]=e(d[v],n));if(!0===c)m.additionalProp1={};else if(c)for(var g=Object(i.A)(c),y=e(g,n),b=1;b<4;b++)m["additionalProp"+b]=y;return m}return"array"===a?o()(l.anyOf)?l.anyOf.map(function(t){return e(t,n)}):o()(l.oneOf)?l.oneOf.map(function(t){return e(t,n)}):[e(l,n)]:t.enum?t.default?t.default:Object(i.w)(t.enum)[0]:"file"!==a?h(t):void 0},m=function(e){return e.schema&&(e=e.schema),e.properties&&(e.type="object"),e},v=function e(t){var n,r,a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},s=p()({},Object(i.A)(t)),u=s.type,c=s.properties,l=s.additionalProperties,f=s.items,d=s.example,m=a.includeReadOnly,v=a.includeWriteOnly,g=s.default,y={},b={},_=t.xml,w=_.name,x=_.prefix,E=_.namespace,S=s.enum;if(!u)if(c||l)u="object";else{if(!f)return;u="array"}if(n=(x?x+":":"")+(w=w||"notagname"),E){var C=x?"xmlns:"+x:"xmlns";b[C]=E}if("array"===u&&f){if(f.xml=f.xml||_||{},f.xml.name=f.xml.name||_.name,_.wrapped)return y[n]=[],o()(d)?d.forEach(function(t){f.example=t,y[n].push(e(f,a))}):o()(g)?g.forEach(function(t){f.default=t,y[n].push(e(f,a))}):y[n]=[e(f,a)],b&&y[n].push({_attr:b}),y;var k=[];return o()(d)?(d.forEach(function(t){f.example=t,k.push(e(f,a))}),k):o()(g)?(g.forEach(function(t){f.default=t,k.push(e(f,a))}),k):e(f,a)}if("object"===u){var O=Object(i.A)(c);for(var A in y[n]=[],d=d||{},O)if(O.hasOwnProperty(A)&&(!O[A].readOnly||m)&&(!O[A].writeOnly||v))if(O[A].xml=O[A].xml||{},O[A].xml.attribute){var T=o()(O[A].enum)&&O[A].enum[0],j=O[A].example,P=O[A].default;b[O[A].xml.name||A]=void 0!==j&&j||void 0!==d[A]&&d[A]||void 0!==P&&P||T||h(O[A])}else{O[A].xml.name=O[A].xml.name||A,void 0===O[A].example&&void 0!==d[A]&&(O[A].example=d[A]);var I=e(O[A]);o()(I)?y[n]=y[n].concat(I):y[n].push(I)}return!0===l?y[n].push({additionalProp:"Anything can be here"}):l&&y[n].push({additionalProp:h(l)}),b&&y[n].push({_attr:b}),y}return r=void 0!==d?d:void 0!==g?g:o()(S)?S[0]:h(t),y[n]=b?[{_attr:b},r]:r,y};function g(e,t){var n=v(e,t);if(n)return s()(n,{declaration:!0,indent:"\t"})}var y=c()(g),b=c()(d)},function(e,t,n){"use strict";n.r(t),n.d(t,"UPDATE_CONFIGS",function(){return i}),n.d(t,"TOGGLE_CONFIGS",function(){return a}),n.d(t,"update",function(){return s}),n.d(t,"toggle",function(){return u}),n.d(t,"loaded",function(){return c});var r=n(2),o=n.n(r),i="configs_update",a="configs_toggle";function s(e,t){return{type:i,payload:o()({},e,t)}}function u(e){return{type:a,payload:e}}var c=function(){return function(){}}},function(e,t,n){"use strict";n.d(t,"a",function(){return a});var r=n(1),o=n.n(r),i=o.a.Set.of("type","format","items","default","maximum","exclusiveMaximum","minimum","exclusiveMinimum","maxLength","minLength","pattern","maxItems","minItems","uniqueItems","enum","multipleOf");function a(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).isOAS3;if(!o.a.Map.isMap(e))return{schema:o.a.Map(),parameterContentMediaType:null};if(!t)return"body"===e.get("in")?{schema:e.get("schema",o.a.Map()),parameterContentMediaType:null}:{schema:e.filter(function(e,t){return i.includes(t)}),parameterContentMediaType:null};if(e.get("content")){var n=e.get("content",o.a.Map({})).keySeq().first();return{schema:e.getIn(["content",n,"schema"],o.a.Map()),parameterContentMediaType:n}}return{schema:e.get("schema",o.a.Map()),parameterContentMediaType:null}}},function(e,t,n){e.exports=n(781)},function(e,t,n){"use strict";n.r(t);var r=n(469),o="object"==typeof self&&self&&self.Object===Object&&self,i=(r.a||o||Function("return this")()).Symbol,a=Object.prototype,s=a.hasOwnProperty,u=a.toString,c=i?i.toStringTag:void 0;var l=function(e){var t=s.call(e,c),n=e[c];try{e[c]=void 0;var r=!0}catch(e){}var o=u.call(e);return r&&(t?e[c]=n:delete e[c]),o},p=Object.prototype.toString;var f=function(e){return p.call(e)},h="[object Null]",d="[object Undefined]",m=i?i.toStringTag:void 0;var v=function(e){return null==e?void 0===e?d:h:m&&m in Object(e)?l(e):f(e)};var g=function(e,t){return function(n){return e(t(n))}}(Object.getPrototypeOf,Object);var y=function(e){return null!=e&&"object"==typeof e},b="[object Object]",_=Function.prototype,w=Object.prototype,x=_.toString,E=w.hasOwnProperty,S=x.call(Object);var C=function(e){if(!y(e)||v(e)!=b)return!1;var t=g(e);if(null===t)return!0;var n=E.call(t,"constructor")&&t.constructor;return"function"==typeof n&&n instanceof n&&x.call(n)==S},k=n(330),O={INIT:"@@redux/INIT"};function A(e,t,n){var r;if("function"==typeof t&&void 0===n&&(n=t,t=void 0),void 0!==n){if("function"!=typeof n)throw new Error("Expected the enhancer to be a function.");return n(A)(e,t)}if("function"!=typeof e)throw new Error("Expected the reducer to be a function.");var o=e,i=t,a=[],s=a,u=!1;function c(){s===a&&(s=a.slice())}function l(){return i}function p(e){if("function"!=typeof e)throw new Error("Expected listener to be a function.");var t=!0;return c(),s.push(e),function(){if(t){t=!1,c();var n=s.indexOf(e);s.splice(n,1)}}}function f(e){if(!C(e))throw new Error("Actions must be plain objects. Use custom middleware for async actions.");if(void 0===e.type)throw new Error('Actions may not have an undefined "type" property. Have you misspelled a constant?');if(u)throw new Error("Reducers may not dispatch actions.");try{u=!0,i=o(i,e)}finally{u=!1}for(var t=a=s,n=0;n0&&void 0!==arguments[0]?arguments[0]:{},t=arguments[1];if(a)throw a;for(var r=!1,o={},s=0;s0?r:n)(e)}},function(e,t){e.exports={}},function(e,t,n){var r=n(348),o=n(215);e.exports=Object.keys||function(e){return r(e,o)}},function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e,t){e.exports=!0},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},function(e,t,n){var r=n(49).f,o=n(75),i=n(34)("toStringTag");e.exports=function(e,t,n){e&&!o(e=n?e:e.prototype,i)&&r(e,i,{configurable:!0,value:t})}},function(e,t,n){var r=n(159)("meta"),o=n(43),i=n(75),a=n(49).f,s=0,u=Object.isExtensible||function(){return!0},c=!n(82)(function(){return u(Object.preventExtensions({}))}),l=function(e){a(e,r,{value:{i:"O"+ ++s,w:{}}})},p=e.exports={KEY:r,NEED:!1,fastKey:function(e,t){if(!o(e))return"symbol"==typeof e?e:("string"==typeof e?"S":"P")+e;if(!i(e,r)){if(!u(e))return"F";if(!t)return"E";l(e)}return e[r].i},getWeak:function(e,t){if(!i(e,r)){if(!u(e))return!0;if(!t)return!1;l(e)}return e[r].w},onFreeze:function(e){return c&&p.NEED&&u(e)&&!i(e,r)&&l(e),e}}},function(e,t,n){"use strict";e.exports=function(e){for(var t=arguments.length-1,n="Minified React error #"+e+"; visit http://facebook.github.io/react/docs/error-decoder.html?invariant="+e,r=0;r1&&void 0!==arguments[1]?arguments[1]:[],n={arrayBehaviour:(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).arrayBehaviour||"replace"},r=t.map(function(e){return e||{}}),i=e||{},c=0;c1?t-1:0),r=1;r")}),p=function(){var e=/(?:)/,t=e.exec;e.exec=function(){return t.apply(this,arguments)};var n="ab".split(e);return 2===n.length&&"a"===n[0]&&"b"===n[1]}();e.exports=function(e,t,n){var f=s(e),h=!i(function(){var t={};return t[f]=function(){return 7},7!=""[e](t)}),d=h?!i(function(){var t=!1,n=/a/;return n.exec=function(){return t=!0,null},"split"===e&&(n.constructor={},n.constructor[c]=function(){return n}),n[f](""),!t}):void 0;if(!h||!d||"replace"===e&&!l||"split"===e&&!p){var m=/./[f],v=n(a,f,""[e],function(e,t,n,r,o){return t.exec===u?h&&!o?{done:!0,value:m.call(t,n,r)}:{done:!0,value:e.call(n,t,r)}:{done:!1}}),g=v[0],y=v[1];r(String.prototype,e,g),o(RegExp.prototype,f,2==t?function(e,t){return y.call(e,this,t)}:function(e){return y.call(e,this)})}}},function(e,t,n){var r=n(212),o=Math.min;e.exports=function(e){return e>0?o(r(e),9007199254740991):0}},function(e,t){var n=0,r=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++n+r).toString(36))}},function(e,t,n){var r=n(46),o=n(350),i=n(215),a=n(213)("IE_PROTO"),s=function(){},u=function(){var e,t=n(217)("iframe"),r=i.length;for(t.style.display="none",n(351).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write("