From 8d199746cd5cd179315d41214a146319f879e26f Mon Sep 17 00:00:00 2001 From: jmahess <7819619+jmahess@users.noreply.github.com> Date: Sun, 12 Apr 2020 12:34:01 -0400 Subject: [PATCH] [R4R] Pricefeed simulations (#420) Co-authored-by: rhuairahrighairigh Co-authored-by: John Maheswaran Co-authored-by: Kevin Davis --- app/sim_test.go | 14 +++ x/pricefeed/keeper/keeper.go | 2 + x/pricefeed/simulation/genesis.go | 53 ++++++++- x/pricefeed/simulation/operations/msg.go | 136 +++++++++++++++++++++++ 4 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 x/pricefeed/simulation/operations/msg.go diff --git a/app/sim_test.go b/app/sim_test.go index db6a797b..5a3b1b00 100644 --- a/app/sim_test.go +++ b/app/sim_test.go @@ -34,7 +34,9 @@ import ( "github.com/cosmos/cosmos-sdk/x/staking" stakingsimops "github.com/cosmos/cosmos-sdk/x/staking/simulation/operations" "github.com/cosmos/cosmos-sdk/x/supply" + bep3simops "github.com/kava-labs/kava/x/bep3/simulation/operations" + pricefeedsimops "github.com/kava-labs/kava/x/pricefeed/simulation/operations" ) // Simulation parameter constants @@ -57,6 +59,7 @@ const ( OpWeightMsgUndelegate = "op_weight_msg_undelegate" OpWeightMsgBeginRedelegate = "op_weight_msg_begin_redelegate" OpWeightMsgUnjail = "op_weight_msg_unjail" + OpWeightMsgPricefeed = "op_weight_msg_pricefeed" OpWeightMsgCreateAtomicSwap = "op_weight_msg_create_atomic_Swap" ) @@ -277,6 +280,17 @@ func testAndRunTxs(app *App, config simulation.Config) []simulation.WeightedOper }(nil), bep3simops.SimulateMsgCreateAtomicSwap(app.accountKeeper, app.bep3Keeper), }, + { + func(_ *rand.Rand) int { + var v int + ap.GetOrGenerate(app.cdc, OpWeightMsgPricefeed, &v, nil, + func(_ *rand.Rand) { + v = 10000 // TODO + }) + return v + }(nil), + pricefeedsimops.SimulateMsgUpdatePrices(app.pricefeedKeeper), + }, } } diff --git a/x/pricefeed/keeper/keeper.go b/x/pricefeed/keeper/keeper.go index 0e4fc8e9..159d1c87 100644 --- a/x/pricefeed/keeper/keeper.go +++ b/x/pricefeed/keeper/keeper.go @@ -178,6 +178,7 @@ func (k Keeper) calculateMeanPrice(ctx sdk.Context, prices []types.CurrentPrice) // GetCurrentPrice fetches the current median price of all oracles for a specific market func (k Keeper) GetCurrentPrice(ctx sdk.Context, marketID string) (types.CurrentPrice, sdk.Error) { store := ctx.KVStore(k.key) + bz := store.Get([]byte(types.CurrentPricePrefix + marketID)) if bz == nil { @@ -185,6 +186,7 @@ func (k Keeper) GetCurrentPrice(ctx sdk.Context, marketID string) (types.Current } var price types.CurrentPrice k.cdc.MustUnmarshalBinaryBare(bz, &price) + if price.Price.Equal(sdk.ZeroDec()) { return types.CurrentPrice{}, types.ErrNoValidPrice(k.codespace) } diff --git a/x/pricefeed/simulation/genesis.go b/x/pricefeed/simulation/genesis.go index 756fc988..9a84d291 100644 --- a/x/pricefeed/simulation/genesis.go +++ b/x/pricefeed/simulation/genesis.go @@ -2,21 +2,64 @@ package simulation import ( "fmt" + "time" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/simulation" "github.com/kava-labs/kava/x/pricefeed/types" + pricefeed "github.com/kava-labs/kava/x/pricefeed/types" + + sdk "github.com/cosmos/cosmos-sdk/types" ) // RandomizedGenState generates a random GenesisState for pricefeed func RandomizedGenState(simState *module.SimulationState) { - - // TODO implement this fully - // - randomly generating the genesis params - // - overwriting with genesis provided to simulation - pricefeedGenesis := types.DefaultGenesisState() + // get the params with xrp, btc and bnb to usd + // getPricefeedSimulationParams is defined to return params with xrp:usd, btc:usd, bnb:usd + params := getPricefeedSimulationParams() + markets := []types.Market{} + genPrices := []types.PostedPrice{} + // chose one account to be the oracle + oracle := simState.Accounts[simulation.RandIntBetween(simState.Rand, 0, len(simState.Accounts))] + for _, market := range params.Markets { + updatedMarket := types.Market{market.MarketID, market.BaseAsset, market.QuoteAsset, []sdk.AccAddress{oracle.Address}, true} + markets = append(markets, updatedMarket) + genPrice := types.PostedPrice{market.MarketID, oracle.Address, getInitialPrice(market.MarketID), simState.GenTimestamp.Add(time.Hour * 24)} + genPrices = append(genPrices, genPrice) + } + params = types.NewParams(markets) + pricefeedGenesis := types.NewGenesisState(params, genPrices) fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, pricefeedGenesis)) simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(pricefeedGenesis) } + +// getPricefeedSimulationParams returns the params with xrp:usd, btc:usd, bnb:usd +func getPricefeedSimulationParams() types.Params { + // SET UP THE PRICEFEED GENESIS STATE + pricefeedGenesis := pricefeed.GenesisState{ + Params: pricefeed.Params{ + Markets: []pricefeed.Market{ + pricefeed.Market{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + pricefeed.Market{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + pricefeed.Market{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true}, + }, + }, + } + return pricefeedGenesis.Params +} + +// getInitialPrice gets the starting price for each of the base assets +func getInitialPrice(marketId string) (price sdk.Dec) { + switch marketId { + case "btc:usd": + return sdk.MustNewDecFromStr("7000") + case "bnb:usd": + return sdk.MustNewDecFromStr("14") + case "xrp:usd": + return sdk.MustNewDecFromStr("0.2") + } + panic(fmt.Sprintf("Invalid marketId in getInitialPrice: %s\n", marketId)) +} diff --git a/x/pricefeed/simulation/operations/msg.go b/x/pricefeed/simulation/operations/msg.go new file mode 100644 index 00000000..bcac646d --- /dev/null +++ b/x/pricefeed/simulation/operations/msg.go @@ -0,0 +1,136 @@ +package operations + +import ( + "fmt" + "math/rand" + "time" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/kava-labs/kava/x/pricefeed" + "github.com/kava-labs/kava/x/pricefeed/keeper" + "github.com/kava-labs/kava/x/pricefeed/types" +) + +var ( + noOpMsg = simulation.NoOpMsg(pricefeed.ModuleName) +) + +// SimulateMsgUpdatePrices updates the prices of various assets by randomly varying them based on current price +func SimulateMsgUpdatePrices(keeper keeper.Keeper) simulation.Operation { + // get a pricefeed handler + handler := pricefeed.NewHandler(keeper) + + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account) ( + simulation.OperationMsg, []simulation.FutureOperation, error) { + + // OVERALL LOGIC: + // (1) RANDOMLY PICK AN ASSET OUT OF BNB AN BTC [TODO QUESTION - USDX IS EXCLUDED AS IT IS A STABLE DENOM + // (2) GET THE CURRENT PRICE OF THAT ASSET IN USD + // (3) GENERATE A RANDOM NUMBER IN THE RANGE 0.8-1.2 (UNIFORM DISTRIBUTION) + // (4) MULTIPLY THE CURRENT PRICE BY THE RANDOM NUMBER + // (5) POST THE NEW PRICE TO THE KEEPER + + // pick a random asset out of BNB and BTC + randomMarket := pickRandomAsset(ctx, keeper, r) + + marketID := randomMarket.MarketID + + // Get the current price of the asset + currentPrice, err := keeper.GetCurrentPrice(ctx, marketID) // Note this is marketID AND **NOT** just the base asset + if err != nil { + return noOpMsg, nil, fmt.Errorf("Error getting current price") + } + + // get the address for the account + // this address needs to be an oracle and also exist. genesis should add all the accounts as oracles. + address := getRandomOracle(r, randomMarket) + + // generate a new random price based off the current price + price, err := pickNewRandomPrice(r, currentPrice.Price) + if err != nil { + return noOpMsg, nil, fmt.Errorf("Error picking random price") + } + + // get the expiry time based off the current time + expiry := getExpiryTime(ctx) + + // now create the msg to post price + msg := types.NewMsgPostPrice(address, marketID, price, expiry) + + // Perform basic validation of the msg - don't submit errors that fail ValidateBasic, use unit tests for testing ValidateBasic + if err := msg.ValidateBasic(); err != nil { + return noOpMsg, nil, fmt.Errorf("expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + } + + // now we submit the pricefeed update message + if ok := submitMsg(ctx, handler, msg); !ok { + return noOpMsg, nil, fmt.Errorf("could not submit pricefeed msg") + } + return simulation.NewOperationMsg(msg, true, "pricefeed update submitted"), nil, nil + } +} + +// getRandomOracle picks a random oracle from the list of oracles +func getRandomOracle(r *rand.Rand, market pricefeed.Market) sdk.AccAddress { + randomIndex := simulation.RandIntBetween(r, 0, len(market.Oracles)) + oracle := market.Oracles[randomIndex] + return oracle +} + +// pickRandomAsset picks a random asset out of the assets with equal probability +// it returns the Market which includes the base asset as one of its fields +func pickRandomAsset(ctx sdk.Context, keeper keeper.Keeper, r *rand.Rand) (market types.Market) { + // get the params + params := keeper.GetParams(ctx) + // now pick a random asset + randomIndex := simulation.RandIntBetween(r, 0, len(params.Markets)) + market = params.Markets[randomIndex] + return market +} + +// getExpiryTime gets a price expiry time by taking the current time and adding a delta to it +func getExpiryTime(ctx sdk.Context) (t time.Time) { + // need to use the blocktime from the context as the context generates random start time when running simulations + t = ctx.BlockTime().Add(time.Second * 1000000) + return t +} + +// pickNewRandomPrice picks a new random price given the current price +// It takes the current price then generates a random number to multiply it by to create variation while +// still being in the similar range. Random walk style. +func pickNewRandomPrice(r *rand.Rand, currentPrice sdk.Dec) (price sdk.Dec, err sdk.Error) { + // Pick random price + // this is in the range [0-0.4) because when added to 0.8 it gives a multiplier in the range 0.8-1.2 + got := sdk.MustNewDecFromStr("0.4") + + randomPriceMultiplier := simulation.RandomDecAmount(r, got) // get a random number + if err != nil { + fmt.Errorf("Error generating random price multiplier\n") + return sdk.ZeroDec(), err + } + // 0.8 offset corresponds to 80% of the the current price + offset := sdk.MustNewDecFromStr("0.8") + + // gives a result in range 0.8-1.2 inclusive, so the price can fluctuate from 80% to 120% of its current value + randomPriceMultiplier = randomPriceMultiplier.Add(offset) + + // multiply the current price by the price multiplier + price = randomPriceMultiplier.Mul(currentPrice) + // return the price + return price, nil +} + +// submitMsg submits a message to the current instance of the keeper and returns a boolean whether the operation completed successfully or not +func submitMsg(ctx sdk.Context, handler sdk.Handler, msg sdk.Msg) (ok bool) { + ctx, write := ctx.CacheContext() + got := handler(ctx, msg) + + ok = got.IsOK() + if ok { + write() + } + return ok +}