diff --git a/app/sim_test.go b/app/sim_test.go index 37ad51a3..40a0c5e4 100644 --- a/app/sim_test.go +++ b/app/sim_test.go @@ -304,7 +304,7 @@ func testAndRunTxs(app *App, config simulation.Config) []simulation.WeightedOper }) return v }(nil), - pricefeedsimops.SimulateMsgUpdatePrices(app.pricefeedKeeper), + pricefeedsimops.SimulateMsgUpdatePrices(app.pricefeedKeeper, config.NumBlocks), }, { func(_ *rand.Rand) int { diff --git a/x/cdp/simulation/operations/msgs.go b/x/cdp/simulation/operations/msgs.go index dd1eb506..c4bacec8 100644 --- a/x/cdp/simulation/operations/msgs.go +++ b/x/cdp/simulation/operations/msgs.go @@ -156,7 +156,7 @@ func SimulateMsgCdp(ak auth.AccountKeeper, k cdp.Keeper, pfk pricefeed.Keeper) s // repay debt 25% of the time if hasCoins(acc, randDebtParam.Denom) { - debt := (existingCDP.Principal.Add(existingCDP.AccumulatedFees)).AmountOf(randDebtParam.Denom) + debt := existingCDP.Principal.AmountOf(randDebtParam.Denom) maxRepay := acc.GetCoins().AmountOf(randDebtParam.Denom) payableDebt := debt.Sub(randDebtParam.DebtFloor) if maxRepay.GT(payableDebt) { diff --git a/x/pricefeed/simulation/genesis.go b/x/pricefeed/simulation/genesis.go index bcbbff01..272695aa 100644 --- a/x/pricefeed/simulation/genesis.go +++ b/x/pricefeed/simulation/genesis.go @@ -12,51 +12,61 @@ import ( pricefeed "github.com/kava-labs/kava/x/pricefeed/types" ) +var ( + // BaseAssets is a list of collateral asset denoms + BaseAssets = [3]string{"bnb", "xrp", "btc"} + QuoteAsset = "usd" +) + // RandomizedGenState generates a random GenesisState for pricefeed func RandomizedGenState(simState *module.SimulationState) { - // 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) + pricefeedGenesis := loadPricefeedGenState(simState) 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}, - }, - }, +// loadPricefeedGenState loads a valid pricefeed gen state +func loadPricefeedGenState(simState *module.SimulationState) pricefeed.GenesisState { + var markets []pricefeed.Market + var postedPrices []pricefeed.PostedPrice + for _, denom := range BaseAssets { + // Select an account to be the oracle + oracle := simState.Accounts[simulation.RandIntBetween(simState.Rand, 0, len(simState.Accounts))] + + marketID := fmt.Sprintf("%s:%s", denom, QuoteAsset) + // Construct market for asset + market := pricefeed.Market{ + MarketID: marketID, + BaseAsset: denom, + QuoteAsset: QuoteAsset, + Oracles: []sdk.AccAddress{oracle.Address}, + Active: true, + } + + // Construct posted price for asset + postedPrice := pricefeed.PostedPrice{ + MarketID: market.MarketID, + OracleAddress: oracle.Address, + Price: getInitialPrice(marketID), + Expiry: simState.GenTimestamp.Add(time.Hour * 24), + } + markets = append(markets, market) + postedPrices = append(postedPrices, postedPrice) } - return pricefeedGenesis.Params + params := pricefeed.NewParams(markets) + return pricefeed.NewGenesisState(params, postedPrices) } // getInitialPrice gets the starting price for each of the base assets -func getInitialPrice(marketId string) (price sdk.Dec) { - switch marketId { +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") + default: + return sdk.MustNewDecFromStr("20") // Catch future additional assets } - 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 index bcac646d..385168a8 100644 --- a/x/pricefeed/simulation/operations/msg.go +++ b/x/pricefeed/simulation/operations/msg.go @@ -3,6 +3,7 @@ package operations import ( "fmt" "math/rand" + "sync" "time" "github.com/cosmos/cosmos-sdk/baseapp" @@ -15,44 +16,54 @@ import ( ) var ( - noOpMsg = simulation.NoOpMsg(pricefeed.ModuleName) + noOpMsg = simulation.NoOpMsg(pricefeed.ModuleName) + btcPrices = []sdk.Dec{} + bnbPrices = []sdk.Dec{} + xrpPrices = []sdk.Dec{} + genPrices sync.Once ) // SimulateMsgUpdatePrices updates the prices of various assets by randomly varying them based on current price -func SimulateMsgUpdatePrices(keeper keeper.Keeper) simulation.Operation { +func SimulateMsgUpdatePrices(keeper keeper.Keeper, blocks int) 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) { + genPrices.Do(func() { + // generate a random walk for each asset exactly once, with observations equal to the number of blocks in the sim + for _, m := range keeper.GetMarkets(ctx) { + startPrice := getStartPrice(m.MarketID) + // allow prices to fluctuate from 10x GAINZ to 100x REKT + maxPrice := sdk.MustNewDecFromStr("10.0").Mul(startPrice) + minPrice := sdk.MustNewDecFromStr("0.01").Mul(startPrice) + previousPrice := startPrice + for i := 0; i < blocks; i++ { + increment := getIncrement(m.MarketID) + // note calling r instead of rand here breaks determinism + upDown := rand.Intn(2) + if upDown == 0 { + if previousPrice.Add(increment).GT(maxPrice) { + previousPrice = maxPrice + } else { + previousPrice = previousPrice.Add(increment) + } + } else { + if previousPrice.Sub(increment).LT(minPrice) { + previousPrice = minPrice + } else { + previousPrice = previousPrice.Sub(increment) + } + } + setPrice(m.MarketID, previousPrice) + } + } + }) - // 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") - } + price := pickNewRandomPrice(marketID, int(ctx.BlockHeight())) // get the expiry time based off the current time expiry := getExpiryTime(ctx) @@ -73,6 +84,51 @@ func SimulateMsgUpdatePrices(keeper keeper.Keeper) simulation.Operation { } } +func getStartPrice(marketID string) (startPrice sdk.Dec) { + switch marketID { + case "btc:usd": + return sdk.MustNewDecFromStr("7000") + case "bnb:usd": + return sdk.MustNewDecFromStr("15") + case "xrp:usd": + return sdk.MustNewDecFromStr("0.25") + } + return sdk.MustNewDecFromStr("100") +} + +func getIncrement(marketID string) (increment sdk.Dec) { + startPrice := getStartPrice(marketID) + divisor := sdk.MustNewDecFromStr("20") + increment = startPrice.Quo(divisor) + return increment +} + +func setPrice(marketID string, price sdk.Dec) { + switch marketID { + case "btc:usd": + btcPrices = append(btcPrices, price) + return + case "bnb:usd": + bnbPrices = append(bnbPrices, price) + return + case "xrp:usd": + xrpPrices = append(xrpPrices, price) + } + return +} + +func pickNewRandomPrice(marketID string, blockHeight int) (newPrice sdk.Dec) { + switch marketID { + case "btc:usd": + return btcPrices[blockHeight-1] + case "bnb:usd": + return bnbPrices[blockHeight-1] + case "xrp:usd": + return xrpPrices[blockHeight-1] + } + panic("invalid price request") +} + // 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)) @@ -98,31 +154,6 @@ func getExpiryTime(ctx sdk.Context) (t time.Time) { 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()