From c6d287a5b35e65b9d8cd9ab695cb3d10e31d4f52 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Thu, 7 May 2020 11:11:10 -0400 Subject: [PATCH] feat: add pricefeed spec --- x/pricefeed/abci.go | 8 +++--- x/pricefeed/alias.go | 39 +++++++++++++------------- x/pricefeed/spec/01_concepts.md | 3 ++ x/pricefeed/spec/02_state.md | 44 ++++++++++++++++++++++++++++++ x/pricefeed/spec/03_messages.md | 20 ++++++++++++++ x/pricefeed/spec/04_events.md | 22 +++++++++++++++ x/pricefeed/spec/05_params.md | 17 ++++++++++++ x/pricefeed/spec/06_begin_block.md | 26 ++++++++++++++++++ x/pricefeed/spec/README.md | 13 +++++++++ x/pricefeed/types/events.go | 11 ++++---- 10 files changed, 173 insertions(+), 30 deletions(-) create mode 100644 x/pricefeed/spec/01_concepts.md create mode 100644 x/pricefeed/spec/02_state.md create mode 100644 x/pricefeed/spec/03_messages.md create mode 100644 x/pricefeed/spec/04_events.md create mode 100644 x/pricefeed/spec/05_params.md create mode 100644 x/pricefeed/spec/06_begin_block.md create mode 100644 x/pricefeed/spec/README.md diff --git a/x/pricefeed/abci.go b/x/pricefeed/abci.go index 1e56f918..49623353 100644 --- a/x/pricefeed/abci.go +++ b/x/pricefeed/abci.go @@ -9,15 +9,15 @@ import ( // EndBlocker updates the current pricefeed func EndBlocker(ctx sdk.Context, k Keeper) { // Update the current price of each asset. - for _, a := range k.GetMarkets(ctx) { - if a.Active { - err := k.SetCurrentPrices(ctx, a.MarketID) + for _, market := range k.GetMarkets(ctx) { + if market.Active { + err := k.SetCurrentPrices(ctx, market.MarketID) if err != nil { // In the event of failure, emit an event. ctx.EventManager().EmitEvent( sdk.NewEvent( EventTypeNoValidPrices, - sdk.NewAttribute(AttributeKeyPriceUpdateFailed, fmt.Sprintf("%s", a.MarketID)), + sdk.NewAttribute(AttributeMarketID, fmt.Sprintf("%s", market.MarketID)), ), ) continue diff --git a/x/pricefeed/alias.go b/x/pricefeed/alias.go index 75affe59..6a734e06 100644 --- a/x/pricefeed/alias.go +++ b/x/pricefeed/alias.go @@ -12,26 +12,25 @@ import ( // nolint const ( - EventTypeMarketPriceUpdated = types.EventTypeMarketPriceUpdated - EventTypeOracleUpdatedPrice = types.EventTypeOracleUpdatedPrice - EventTypeNoValidPrices = types.EventTypeNoValidPrices - AttributeValueCategory = types.AttributeValueCategory - AttributeMarketID = types.AttributeMarketID - AttributeMarketPrice = types.AttributeMarketPrice - AttributeOracle = types.AttributeOracle - AttributeExpiry = types.AttributeExpiry - AttributeKeyPriceUpdateFailed = types.AttributeKeyPriceUpdateFailed - ModuleName = types.ModuleName - StoreKey = types.StoreKey - RouterKey = types.RouterKey - QuerierRoute = types.QuerierRoute - DefaultParamspace = types.DefaultParamspace - TypeMsgPostPrice = types.TypeMsgPostPrice - QueryGetParams = types.QueryGetParams - QueryMarkets = types.QueryMarkets - QueryOracles = types.QueryOracles - QueryRawPrices = types.QueryRawPrices - QueryPrice = types.QueryPrice + EventTypeMarketPriceUpdated = types.EventTypeMarketPriceUpdated + EventTypeOracleUpdatedPrice = types.EventTypeOracleUpdatedPrice + EventTypeNoValidPrices = types.EventTypeNoValidPrices + AttributeValueCategory = types.AttributeValueCategory + AttributeMarketID = types.AttributeMarketID + AttributeMarketPrice = types.AttributeMarketPrice + AttributeOracle = types.AttributeOracle + AttributeExpiry = types.AttributeExpiry + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey + QuerierRoute = types.QuerierRoute + DefaultParamspace = types.DefaultParamspace + TypeMsgPostPrice = types.TypeMsgPostPrice + QueryGetParams = types.QueryGetParams + QueryMarkets = types.QueryMarkets + QueryOracles = types.QueryOracles + QueryRawPrices = types.QueryRawPrices + QueryPrice = types.QueryPrice ) // nolint diff --git a/x/pricefeed/spec/01_concepts.md b/x/pricefeed/spec/01_concepts.md new file mode 100644 index 00000000..76627d8d --- /dev/null +++ b/x/pricefeed/spec/01_concepts.md @@ -0,0 +1,3 @@ +# Concepts + +Prices can be posted by any account which is added as an oracle. Oracles are specific to each market and can be updated via param change proposals. When an oracle posts a price, they submit a message to the blockchain that contains the current price for that market and a time when that price should be considered expired. If an oracle posts a new price, that price becomes the current price for that oracle, regardless of the previous price's expiry. A group of prices posted by a set of oracles for a particular market are referred to as 'raw prices' and the current median price of all valid oracle prices is referred to as the 'current price'. Each block, the current price for each market is determined by calculating the median of the raw prices. diff --git a/x/pricefeed/spec/02_state.md b/x/pricefeed/spec/02_state.md new file mode 100644 index 00000000..5ff2e5a8 --- /dev/null +++ b/x/pricefeed/spec/02_state.md @@ -0,0 +1,44 @@ +# State + +## Parameters and genesis state + +`Paramaters` determine which markets are tracked by the pricefeed and which oracles are authorized to post prices for a given market. There is only one active parameter set at any given time. Updates to parameters can be made via on-chain parameter update proposals. + +```go +// Params params for pricefeed. Can be altered via governance +type Params struct { + Markets Markets `json:"markets" yaml:"markets"` // Array containing the markets supported by the pricefeed +} + +// Market an asset in the pricefeed +type Market struct { + MarketID string `json:"market_id" yaml:"market_id"` + BaseAsset string `json:"base_asset" yaml:"base_asset"` + QuoteAsset string `json:"quote_asset" yaml:"quote_asset"` + Oracles []sdk.AccAddress `json:"oracles" yaml:"oracles"` + Active bool `json:"active" yaml:"active"` +} + +type Markets []Market +``` + +`GenesisState` defines the state that must be persisted when the blockchain stops/stars in order for the normal function of the pricefeed to resume. + +```go +// GenesisState - pricefeed state that must be provided at genesis +type GenesisState struct { + Params Params `json:"params" yaml:"params"` + PostedPrices []PostedPrice `json:"posted_prices" yaml:"posted_prices"` +} + +// PostedPrice price for market posted by a specific oracle +type PostedPrice struct { + MarketID string `json:"market_id" yaml:"market_id"` + OracleAddress sdk.AccAddress `json:"oracle_address" yaml:"oracle_address"` + Price sdk.Dec `json:"price" yaml:"price"` + Expiry time.Time `json:"expiry" yaml:"expiry"` +} + +type PostedPrices []PostedPrice +``` + diff --git a/x/pricefeed/spec/03_messages.md b/x/pricefeed/spec/03_messages.md new file mode 100644 index 00000000..8ddb5f07 --- /dev/null +++ b/x/pricefeed/spec/03_messages.md @@ -0,0 +1,20 @@ +# Messages + +## Posting Prices + +An authorized oraclef for a particular market can post the current price for that market using the `MsgPostPrice` type. + +```go +// MsgPostPrice struct representing a posted price message. +// Used by oracles to input prices to the pricefeed +type MsgPostPrice struct { + From sdk.AccAddress `json:"from" yaml:"from"` // client that sent in this address + MarketID string `json:"market_id" yaml:"market_id"` // asset code used by exchanges/api + Price sdk.Dec `json:"price" yaml:"price"` // price in decimal (max precision 18) + Expiry time.Time `json:"expiry" yaml:"expiry"` // expiry time +} +``` + +### State Modifications + +* Update the raw price for the oracle for this market. This replaces any previous price for that oracle. diff --git a/x/pricefeed/spec/04_events.md b/x/pricefeed/spec/04_events.md new file mode 100644 index 00000000..d114dcec --- /dev/null +++ b/x/pricefeed/spec/04_events.md @@ -0,0 +1,22 @@ +# Events + +The `x/pricefeed` module emits the following events: + +## MsgPostPrice + +| Type | Attribute Key | Attribute Value | +|----------------------|---------------|------------------| +| oracle_updated_price | market_id | {market ID} | +| oracle_updated_price | oracle | {oracle} | +| oracle_updated_price | market_price | {price} | +| oracle_updated_price | expiry | {expiry} | +| message | module | pricefeed | +| message | sender | {sender address} | + +## BeginBlock + +| Type | Attribute Key | Attribute Value | +|----------------------|-----------------|-----------------| +| market_price_updated | market_id | {market ID} | +| market_price_updated | market_price | {price} | +| no_valid_prices | market_id | {market ID} | diff --git a/x/pricefeed/spec/05_params.md b/x/pricefeed/spec/05_params.md new file mode 100644 index 00000000..1dc18f41 --- /dev/null +++ b/x/pricefeed/spec/05_params.md @@ -0,0 +1,17 @@ +# Parameters + +The pricefeed module has the following parameters: + +| Key | Type | Example | Description | +|------------|----------------|---------------|--------------------------------------------------| +| Markets | array (Market) | [{see below}] | array of params for each market in the pricefeed | + +Each `Market` has the following parameters + +| Key | Type | Example | Description | +|------------|--------------------|--------------------------|----------------------------------------------------------------| +| MarketID | string | "bnb:usd" | identifier for the market -- **must** be unique across markets | +| BaseAsset | string | "bnb" | the base asset for the market pair | +| QuoteAsset | string | "usd" | the quote asset for the market pair | +| Oracles | array (AccAddress) | ["kava1...", "kava1..."] | addresses which can post prices for the market | +| Active | bool | true | flag to disable oracle interactions with the module | diff --git a/x/pricefeed/spec/06_begin_block.md b/x/pricefeed/spec/06_begin_block.md new file mode 100644 index 00000000..812db197 --- /dev/null +++ b/x/pricefeed/spec/06_begin_block.md @@ -0,0 +1,26 @@ +# Begin Block + +At the start of each block, the current price is calculated as the median of all raw prices for each market. The logic is as follows: + +```go +// EndBlocker updates the current pricefeed +func EndBlocker(ctx sdk.Context, k Keeper) { + // Update the current price of each asset. + for _, market := range k.GetMarkets(ctx) { + if market.Active { + err := k.SetCurrentPrices(ctx, market.MarketID) + if err != nil { + // In the event of failure, emit an event. + ctx.EventManager().EmitEvent( + sdk.NewEvent( + EventTypeNoValidPrices, + sdk.NewAttribute(AttributeMarketID, fmt.Sprintf("%s", market.MarketID)), + ), + ) + continue + } + } + } + return +} +``` diff --git a/x/pricefeed/spec/README.md b/x/pricefeed/spec/README.md new file mode 100644 index 00000000..075a6a1a --- /dev/null +++ b/x/pricefeed/spec/README.md @@ -0,0 +1,13 @@ +# `pricefeed` + + +1. **[Concepts](01_concepts.md)** +2. **[State](02_state.md)** +3. **[Messages](03_messages.md)** +4. **[Events](04_events.md)** +5. **[Params](05_params.md)** +6. **[BeginBlock](06_begin_block.md)** + +## Abstract + +`x/pricefeed` is an implementation of a Cosmos SDK Module that handles the posting of prices for various markets by a group of whitelisted oracles. At the beginning of each block, the median price of all oracle posted prices is determined for each market and stored. diff --git a/x/pricefeed/types/events.go b/x/pricefeed/types/events.go index 07a0007d..683a379b 100644 --- a/x/pricefeed/types/events.go +++ b/x/pricefeed/types/events.go @@ -6,10 +6,9 @@ const ( EventTypeOracleUpdatedPrice = "oracle_updated_price" EventTypeNoValidPrices = "no_valid_prices" - AttributeValueCategory = ModuleName - AttributeMarketID = "market_id" - AttributeMarketPrice = "market_price" - AttributeOracle = "oracle" - AttributeExpiry = "expiry" - AttributeKeyPriceUpdateFailed = "price_update_failed" + AttributeValueCategory = ModuleName + AttributeMarketID = "market_id" + AttributeMarketPrice = "market_price" + AttributeOracle = "oracle" + AttributeExpiry = "expiry" )