From d5da161dd81187d3fc173eff55e3594b2977c111 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Wed, 27 Nov 2019 09:45:59 -0500 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 bf83a9bf8f4c224cbae0bb26b9516124a5af7c4b Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Wed, 4 Dec 2019 11:32:08 -0500 Subject: [PATCH 4/5] 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 5/5] 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), }, }, }