diff --git a/CHANGELOG.md b/CHANGELOG.md index ff87bffb9..e9fb8daa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [#901](https://github.com/NibiruChain/nibiru/pull/901) - refactor(vpool): remove `GetUnderlyingPrice` method * [#902](https://github.com/NibiruChain/nibiru/pull/902) - refactor(common): improve usability of `common.AssetPair` * [#913](https://github.com/NibiruChain/nibiru/pull/913) - chore(epochs): update x/epochs module +* [#911](https://github.com/NibiruChain/nibiru/pull/911) - test(perp): add `MsgOpenPosition` simulation tests * [#917](https://github.com/NibiruChain/nibiru/pull/917) - refactor(proto): perp module files consistency * [#920](https://github.com/NibiruChain/nibiru/pull/920) - refactor(proto): pricefeed module files consistency * [#926](https://github.com/NibiruChain/nibiru/pull/926) - feat: use spot twap for funding rate calculation @@ -101,6 +102,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [#892](https://github.com/NibiruChain/nibiru/pull/892) - chore: fix localnet script * [#925](https://github.com/NibiruChain/nibiru/pull/925) - fix(vpool): snapshot iteration * [#930](https://github.com/NibiruChain/nibiru/pull/930) - fix(vpool): snapshot iteration on mark twap +* [#911](https://github.com/NibiruChain/nibiru/pull/911) - fix(perp): handle issue where no vpool snapshots are found * [#958](https://github.com/NibiruChain/nibiru/pull/930) - fix(pricefeed): add twap to prices query * [#961](https://github.com/NibiruChain/nibiru/pull/961) - fix(perp): wire the funding rate query * [#968](https://github.com/NibiruChain/nibiru/pull/968) - fix(perp): compute correct funding rate diff --git a/Makefile b/Makefile index 78cafd565..4f0fff1fe 100644 --- a/Makefile +++ b/Makefile @@ -139,10 +139,10 @@ test-sim-nondeterminism: @go test -mod=readonly $(SIMAPP) -run TestAppStateDeterminism -Enabled=true \ -NumBlocks=100 -BlockSize=200 -Commit=true -Period=0 -v -timeout 24h -test-sim-custom-genesis-fast: - @echo "Running custom genesis simulation..." +test-sim-default-genesis-fast: + @echo "Running default genesis simulation..." @go test -mod=readonly $(SIMAPP) -run TestFullAppSimulation \ - -Enabled=true -NumBlocks=100 -BlockSize=200 -Commit=true -Seed=99 -Period=5 -v -timeout 24h + -Enabled=true -NumBlocks=100 -BlockSize=200 -Commit=true -Seed=99 -Period=5 -v test-sim-custom-genesis-multi-seed: runsim @echo "Running multi-seed custom genesis simulation..." diff --git a/simapp/app.go b/simapp/app.go index bfbb24e8d..7c051cfa1 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -314,8 +314,7 @@ func NewNibiruTestApp( legacyAmino := encodingConfig.Amino interfaceRegistry := encodingConfig.InterfaceRegistry - bApp := baseapp.NewBaseApp( - AppName, logger, db, encodingConfig.TxConfig.TxDecoder(), baseAppOptions...) + bApp := baseapp.NewBaseApp(AppName, logger, db, encodingConfig.TxConfig.TxDecoder(), baseAppOptions...) bApp.SetCommitMultiStoreTracer(traceStore) bApp.SetVersion(version.Version) bApp.SetInterfaceRegistry(interfaceRegistry) @@ -748,6 +747,7 @@ func NewNibiruTestApp( // native x/ pricefeedModule, epochsModule, + perpModule, // ibc capability.NewAppModule(appCodec, *app.CapabilityKeeper), evidence.NewAppModule(app.EvidenceKeeper), diff --git a/simapp/genesis.go b/simapp/genesis.go index 30f9adce9..f19371079 100644 --- a/simapp/genesis.go +++ b/simapp/genesis.go @@ -5,15 +5,12 @@ import ( "io" "io/ioutil" - tmjson "github.com/tendermint/tendermint/libs/json" - tmtypes "github.com/tendermint/tendermint/types" - - "github.com/NibiruChain/nibiru/app" - "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + tmjson "github.com/tendermint/tendermint/libs/json" + tmtypes "github.com/tendermint/tendermint/types" ) // AppStateFromGenesisFileFn util function to generate the genesis AppState @@ -31,7 +28,7 @@ func AppStateFromGenesisFileFn(r io.Reader, cdc codec.JSONCodec, genesisFile str panic(err) } - var appState app.GenesisState + var appState GenesisState err = json.Unmarshal(genesis.AppState, &appState) if err != nil { panic(err) diff --git a/x/dex/simulation/operations.go b/x/dex/simulation/operations.go index 1e66f275e..4d5f84fda 100644 --- a/x/dex/simulation/operations.go +++ b/x/dex/simulation/operations.go @@ -90,7 +90,6 @@ func SimulateMsgCreatePool(ak types.AccountKeeper, bk types.BankKeeper, k keeper /* SimulateMsgSwap generates a MsgSwap with random values -This function has a 33% chance of swapping a random fraction of the balance of a random token */ func SimulateMsgSwap(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.Operation { return func( @@ -98,11 +97,6 @@ func SimulateMsgSwap(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keepe ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { msg := &types.MsgSwapAssets{} - // only run 1/3 of the time - if simtypes.RandomDecAmount(r, sdk.MustNewDecFromStr("1")).GTE(sdk.MustNewDecFromStr("0.33")) { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "No swap done"), nil, nil - } - simAccount, _ := simtypes.RandomAcc(r, accs) fundAccountWithTokens(ctx, simAccount.Address, bk) spendableCoins := bk.SpendableCoins(ctx, simAccount.Address) @@ -116,10 +110,7 @@ func SimulateMsgSwap(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keepe return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "No tokens to swap in"), nil, nil } - // choose some random amount of balanceIn to swap - intensityFactor := simtypes.RandomDecAmount(r, sdk.MustNewDecFromStr("0.05")).Add(sdk.MustNewDecFromStr("0.1")) - tokenIn := sdk.NewCoin(denomIn, intensityFactor.MulInt(balanceIn).TruncateInt()) - + tokenIn := sdk.NewCoin(denomIn, balanceIn) msg = &types.MsgSwapAssets{ Sender: simAccount.Address.String(), PoolId: poolId, diff --git a/x/perp/module.go b/x/perp/module.go index fbbdf5e4f..373ed4b7a 100644 --- a/x/perp/module.go +++ b/x/perp/module.go @@ -5,15 +5,14 @@ import ( "encoding/json" "fmt" - "github.com/gorilla/mux" - "github.com/grpc-ecosystem/grpc-gateway/runtime" - "github.com/spf13/cobra" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + "github.com/gorilla/mux" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" abci "github.com/tendermint/tendermint/abci/types" "github.com/NibiruChain/nibiru/x/perp/client/cli" diff --git a/x/perp/module_simulation.go b/x/perp/module_simulation.go new file mode 100644 index 000000000..b869ed242 --- /dev/null +++ b/x/perp/module_simulation.go @@ -0,0 +1,34 @@ +package perp + +import ( + "math/rand" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + + "github.com/NibiruChain/nibiru/x/perp/simulation" +) + +// GenerateGenesisState creates a default GenState of the module +func (AppModule) GenerateGenesisState(simState *module.SimulationState) { + simulation.RandomizedGenState(simState) +} + +// ProposalContents doesn't return any content functions for governance proposals +func (AppModule) ProposalContents(_ module.SimulationState) []simtypes.WeightedProposalContent { + return nil +} + +// RandomizedParams creates randomized param changes for the simulator +func (am AppModule) RandomizedParams(_ *rand.Rand) []simtypes.ParamChange { + return nil +} + +// RegisterStoreDecoder registers a decoder +func (am AppModule) RegisterStoreDecoder(_ sdk.StoreDecoderRegistry) {} + +// WeightedOperations returns the all the gov module operations with their respective weights. +func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { + return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.ak, am.bk, am.keeper) +} diff --git a/x/perp/simulation/genesis.go b/x/perp/simulation/genesis.go new file mode 100644 index 000000000..b97bc1ba0 --- /dev/null +++ b/x/perp/simulation/genesis.go @@ -0,0 +1,64 @@ +package simulation + +import ( + "encoding/json" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/NibiruChain/nibiru/x/common" + "github.com/NibiruChain/nibiru/x/perp/types" + pricefeedtypes "github.com/NibiruChain/nibiru/x/pricefeed/types" + vpooltypes "github.com/NibiruChain/nibiru/x/vpool/types" +) + +// RandomizedGenState generates a random GenesisState for the perp module +func RandomizedGenState(simState *module.SimulationState) { + vpoolGenesis := vpooltypes.GenesisState{ + Vpools: []*vpooltypes.VPool{ + { + Pair: common.Pair_BTC_NUSD, + TradeLimitRatio: sdk.OneDec(), + QuoteAssetReserve: sdk.NewDec(10e12), + BaseAssetReserve: sdk.NewDec(10e12), + FluctuationLimitRatio: sdk.OneDec(), + MaxOracleSpreadRatio: sdk.OneDec(), + MaintenanceMarginRatio: sdk.MustNewDecFromStr("0.0625"), + MaxLeverage: sdk.NewDec(10), + }, + }, + } + + vpools, err := json.MarshalIndent(&vpoolGenesis.Vpools, "", " ") + if err != nil { + panic(err) + } + fmt.Printf("Selected randomly generated vpools:\n%s\n", vpools) + simState.GenState[vpooltypes.ModuleName] = simState.Cdc.MustMarshalJSON(&vpoolGenesis) + + pricefeedGenesis := pricefeedtypes.DefaultGenesis() + + pricefeedGenesisBytes, err := json.MarshalIndent(&pricefeedGenesis, "", " ") + if err != nil { + panic(err) + } + fmt.Printf("Selected randomly generated pricefeed genesis:\n%s\n", pricefeedGenesisBytes) + simState.GenState[pricefeedtypes.ModuleName] = simState.Cdc.MustMarshalJSON(pricefeedGenesis) + + perpGenesis := types.GenesisState{ + Params: types.DefaultParams(), + PairMetadata: []types.PairMetadata{ + { + Pair: common.Pair_BTC_NUSD, + CumulativeFundingRates: []sdk.Dec{sdk.ZeroDec()}, + }, + }, + } + perpGenesisBytes, err := json.MarshalIndent(&perpGenesis, "", " ") + if err != nil { + panic(err) + } + fmt.Printf("Selected randomly generated perp genesis:\n%s\n", perpGenesisBytes) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(&perpGenesis) +} diff --git a/x/perp/simulation/operations.go b/x/perp/simulation/operations.go new file mode 100644 index 000000000..9a96c6924 --- /dev/null +++ b/x/perp/simulation/operations.go @@ -0,0 +1,102 @@ +package simulation + +import ( + "fmt" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/NibiruChain/nibiru/x/common" + "github.com/NibiruChain/nibiru/x/perp/keeper" + "github.com/NibiruChain/nibiru/x/perp/types" +) + +const defaultWeight = 100 + +// WeightedOperations returns all the operations from the module with their respective weights +func WeightedOperations( + appParams simtypes.AppParams, + cdc codec.JSONCodec, + ak types.AccountKeeper, + bk types.BankKeeper, + k keeper.Keeper) simulation.WeightedOperations { + return simulation.WeightedOperations{ + simulation.NewWeightedOperation( + defaultWeight, + SimulateMsgOpenPosition(ak, bk, k), + ), + } +} + +// SimulateMsgCreateBalancerPool generates a MsgCreatePool with random values. +func SimulateMsgOpenPosition(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + simAccount, _ := simtypes.RandomAcc(r, accs) + fundAccountWithTokens(ctx, simAccount.Address, bk) + spendableCoins := bk.SpendableCoins(ctx, simAccount.Address) + + quoteAmt, _ := simtypes.RandPositiveInt(r, spendableCoins.AmountOf(common.DenomNUSD)) + leverage := simtypes.RandomDecAmount(r, sdk.NewDec(9)).Add(sdk.OneDec()) // between [1, 10] + openNotional := leverage.MulInt(quoteAmt) + feesAmt := openNotional.Mul(sdk.MustNewDecFromStr("0.002")).Ceil().TruncateInt() + spentCoins := sdk.NewCoins(sdk.NewCoin(common.DenomNUSD, quoteAmt.Add(feesAmt))) + + msg := &types.MsgOpenPosition{ + Sender: simAccount.Address.String(), + TokenPair: common.Pair_BTC_NUSD.String(), + Side: types.Side_BUY, + QuoteAssetAmount: quoteAmt, + Leverage: leverage, + BaseAssetAmountLimit: sdk.ZeroInt(), + } + + opMsg, futureOps, err := simulation.GenAndDeliverTxWithRandFees( + simulation.OperationInput{ + R: r, + App: app, + TxGen: simapp.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + CoinsSpentInMsg: spentCoins, + }, + ) + if err != nil { + fmt.Println(spendableCoins) + fmt.Println(quoteAmt) + } + + return opMsg, futureOps, err + } +} + +func fundAccountWithTokens(ctx sdk.Context, receiver sdk.AccAddress, bk types.BankKeeper) { + newCoins := sdk.NewCoins( + sdk.NewCoin(common.DenomNUSD, sdk.NewInt(1e6)), + ) + + if err := bk.MintCoins(ctx, types.ModuleName, newCoins); err != nil { + panic(err) + } + + if err := bk.SendCoinsFromModuleToAccount( + ctx, + types.ModuleName, + receiver, + newCoins, + ); err != nil { + panic(err) + } +} diff --git a/x/perp/types/keys.go b/x/perp/types/keys.go index 9d22f53ab..629d11f40 100644 --- a/x/perp/types/keys.go +++ b/x/perp/types/keys.go @@ -6,7 +6,7 @@ var ( MemStoreKey = "mem_perp" - // RouterKey is the message route for slashing. + // RouterKey is the message route for perp. RouterKey = ModuleName // QuerierRoute defines the module's query routing key. diff --git a/x/perp/types/msgs.go b/x/perp/types/msgs.go index 5bed3ded5..aad9f325e 100644 --- a/x/perp/types/msgs.go +++ b/x/perp/types/msgs.go @@ -94,6 +94,9 @@ func (m MsgAddMargin) GetSigners() []sdk.AccAddress { // MsgOpenPosition +func (m MsgOpenPosition) Route() string { return RouterKey } +func (m MsgOpenPosition) Type() string { return "open_position_msg" } + func (msg *MsgOpenPosition) ValidateBasic() error { if msg.Side != Side_SELL && msg.Side != Side_BUY { return fmt.Errorf("invalid side") @@ -117,6 +120,10 @@ func (msg *MsgOpenPosition) ValidateBasic() error { return nil } +func (m MsgOpenPosition) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&m)) +} + func (m *MsgOpenPosition) GetSigners() []sdk.AccAddress { signer, err := sdk.AccAddressFromBech32(m.Sender) if err != nil { diff --git a/x/vpool/keeper/prices.go b/x/vpool/keeper/prices.go index 06b736642..42a89fd0e 100644 --- a/x/vpool/keeper/prices.go +++ b/x/vpool/keeper/prices.go @@ -234,11 +234,12 @@ func (k Keeper) calcTwap( var cumulativePeriodMs int64 = 0 var prevTimestampMs int64 = ctx.BlockTime().UnixMilli() var currentSnapshot types.ReserveSnapshot + var currentPrice sdk.Dec = sdk.ZeroDec() for ; iter.Valid(); iter.Next() { k.codec.MustUnmarshal(iter.Value(), ¤tSnapshot) - currentPrice, err := getPriceWithSnapshot( + currentPrice, err = getPriceWithSnapshot( currentSnapshot, snapshotPriceOptions{ pair: pair, @@ -269,6 +270,10 @@ func (k Keeper) calcTwap( prevTimestampMs = currentSnapshot.TimestampMs } + if cumulativePeriodMs <= 0 { + return currentPrice, nil + } + // definition of TWAP return cumulativePrice.QuoInt64(cumulativePeriodMs), nil } diff --git a/x/vpool/keeper/prices_test.go b/x/vpool/keeper/prices_test.go index 867d2c8fe..e185b1036 100644 --- a/x/vpool/keeper/prices_test.go +++ b/x/vpool/keeper/prices_test.go @@ -302,6 +302,23 @@ func TestCalcTwap(t *testing.T) { twapCalcOption: types.TwapCalcOption_SPOT, expectedPrice: sdk.MustNewDecFromStr("8.895833333333333333"), }, + { + name: "spot price twap calc, t=[10,10]", + pair: common.Pair_BTC_NUSD, + reserveSnapshots: []types.ReserveSnapshot{ + { + QuoteAssetReserve: sdk.NewDec(90), + BaseAssetReserve: sdk.NewDec(10), + TimestampMs: 10, + BlockNumber: 1, + }, + }, + currentBlockTime: time.UnixMilli(10), + currentBlockHeight: 1, + lookbackInterval: 5 * time.Millisecond, + twapCalcOption: types.TwapCalcOption_SPOT, + expectedPrice: sdk.NewDec(9), + }, { name: "quote asset swap twap calc, add to pool, t=[10,30]", pair: common.Pair_BTC_NUSD, @@ -417,7 +434,7 @@ func TestCalcTwap(t *testing.T) { t.Run(tc.name, func(t *testing.T) { vpoolKeeper, ctx := VpoolKeeper(t, mock.NewMockPricefeedKeeper(gomock.NewController(t))) - ctx = ctx.WithBlockTime(time.UnixMilli(0)).WithBlockHeight(0) + ctx = ctx.WithBlockTime(time.UnixMilli(0)).WithBlockHeight(1) t.Log("Create an empty pool for the first block") vpoolKeeper.CreatePool(