mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-11-20 15:05:21 +00:00
Add Auction Simulations (#419)
* first pass * fix bid amount calculation * untested refactor of sim ops and genesis * refactor operations and fix auction bug * add param changes and genesis * address minor TODO * add first draft of invariants * improve param generation * complete invariants * fix genesis tests * log no-op better * small fixes * add missed comma Co-authored-by: John Maheswaran <john@kava.io>
This commit is contained in:
parent
8d199746cd
commit
5bdffd5c1c
13
app/app.go
13
app/app.go
@ -302,16 +302,15 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
||||
|
||||
app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName)
|
||||
|
||||
// Note: genutils must occur after staking so that pools are properly
|
||||
// initialized with tokens from genesis accounts.
|
||||
//
|
||||
// Note: Changing the order of the auth module and modules that use module accounts
|
||||
// results in subtle changes to the way accounts are loaded from genesis.
|
||||
app.mm.SetOrderInitGenesis(
|
||||
auth.ModuleName, validatorvesting.ModuleName, distr.ModuleName,
|
||||
auth.ModuleName, // loads all accounts - should run before any module with a module account
|
||||
validatorvesting.ModuleName, distr.ModuleName,
|
||||
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
|
||||
gov.ModuleName, mint.ModuleName, supply.ModuleName, crisis.ModuleName, genutil.ModuleName,
|
||||
gov.ModuleName, mint.ModuleName,
|
||||
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, bep3.ModuleName, kavadist.ModuleName, // TODO is this order ok?
|
||||
supply.ModuleName, // calculates the total supply from account - should run after modules that modify accounts in genesis
|
||||
crisis.ModuleName, // runs the invariants at genesis - should run after other modules
|
||||
genutil.ModuleName, // genutils must occur after staking so that pools are properly initialized with tokens from genesis accounts.
|
||||
)
|
||||
|
||||
app.mm.RegisterInvariants(&app.crisisKeeper)
|
||||
|
@ -35,6 +35,7 @@ import (
|
||||
stakingsimops "github.com/cosmos/cosmos-sdk/x/staking/simulation/operations"
|
||||
"github.com/cosmos/cosmos-sdk/x/supply"
|
||||
|
||||
auctionsimops "github.com/kava-labs/kava/x/auction/simulation/operations"
|
||||
bep3simops "github.com/kava-labs/kava/x/bep3/simulation/operations"
|
||||
pricefeedsimops "github.com/kava-labs/kava/x/pricefeed/simulation/operations"
|
||||
)
|
||||
@ -59,6 +60,7 @@ const (
|
||||
OpWeightMsgUndelegate = "op_weight_msg_undelegate"
|
||||
OpWeightMsgBeginRedelegate = "op_weight_msg_begin_redelegate"
|
||||
OpWeightMsgUnjail = "op_weight_msg_unjail"
|
||||
OpWeightMsgPlaceBid = "op_weight_msg_place_bid"
|
||||
OpWeightMsgPricefeed = "op_weight_msg_pricefeed"
|
||||
OpWeightMsgCreateAtomicSwap = "op_weight_msg_create_atomic_Swap"
|
||||
)
|
||||
@ -269,6 +271,17 @@ func testAndRunTxs(app *App, config simulation.Config) []simulation.WeightedOper
|
||||
}(nil),
|
||||
slashingsimops.SimulateMsgUnjail(app.slashingKeeper),
|
||||
},
|
||||
{
|
||||
func(_ *rand.Rand) int {
|
||||
var v int
|
||||
ap.GetOrGenerate(app.cdc, OpWeightMsgPlaceBid, &v, nil,
|
||||
func(_ *rand.Rand) {
|
||||
v = 100
|
||||
})
|
||||
return v
|
||||
}(nil),
|
||||
auctionsimops.SimulateMsgPlaceBid(app.accountKeeper, app.auctionKeeper),
|
||||
},
|
||||
{
|
||||
func(_ *rand.Rand) int {
|
||||
var v int
|
||||
@ -285,7 +298,7 @@ func testAndRunTxs(app *App, config simulation.Config) []simulation.WeightedOper
|
||||
var v int
|
||||
ap.GetOrGenerate(app.cdc, OpWeightMsgPricefeed, &v, nil,
|
||||
func(_ *rand.Rand) {
|
||||
v = 10000 // TODO
|
||||
v = 100
|
||||
})
|
||||
return v
|
||||
}(nil),
|
||||
|
@ -55,6 +55,7 @@ var (
|
||||
// functions aliases
|
||||
NewKeeper = keeper.NewKeeper
|
||||
NewQuerier = keeper.NewQuerier
|
||||
RegisterInvariants = keeper.RegisterInvariants
|
||||
NewSurplusAuction = types.NewSurplusAuction
|
||||
NewDebtAuction = types.NewDebtAuction
|
||||
NewCollateralAuction = types.NewCollateralAuction
|
||||
|
@ -22,7 +22,7 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper types.SupplyKeeper
|
||||
for _, a := range gs.Auctions {
|
||||
keeper.SetAuction(ctx, a)
|
||||
// find the total coins that should be present in the module account
|
||||
totalAuctionCoins.Add(a.GetModuleAccountCoins())
|
||||
totalAuctionCoins = totalAuctionCoins.Add(a.GetModuleAccountCoins())
|
||||
}
|
||||
|
||||
// check if the module account exists
|
||||
|
@ -29,6 +29,12 @@ func TestInitGenesis(t *testing.T) {
|
||||
tApp := app.NewTestApp()
|
||||
keeper := tApp.GetAuctionKeeper()
|
||||
ctx := tApp.NewContext(true, abci.Header{})
|
||||
// setup module account
|
||||
supplyKeeper := tApp.GetSupplyKeeper()
|
||||
moduleAcc := supplyKeeper.GetModuleAccount(ctx, auction.ModuleName)
|
||||
require.NoError(t, moduleAcc.SetCoins(testAuction.GetModuleAccountCoins()))
|
||||
supplyKeeper.SetModuleAccount(ctx, moduleAcc)
|
||||
|
||||
// create genesis
|
||||
gs := auction.NewGenesisState(
|
||||
10,
|
||||
@ -38,7 +44,7 @@ func TestInitGenesis(t *testing.T) {
|
||||
|
||||
// run init
|
||||
require.NotPanics(t, func() {
|
||||
auction.InitGenesis(ctx, keeper, tApp.GetSupplyKeeper(), gs)
|
||||
auction.InitGenesis(ctx, keeper, supplyKeeper, gs)
|
||||
})
|
||||
|
||||
// check state is as expected
|
||||
@ -59,7 +65,7 @@ func TestInitGenesis(t *testing.T) {
|
||||
return false
|
||||
})
|
||||
})
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
t.Run("invalid (invalid nextAuctionID)", func(t *testing.T) {
|
||||
// setup keepers
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{})
|
||||
@ -71,6 +77,24 @@ func TestInitGenesis(t *testing.T) {
|
||||
auction.GenesisAuctions{testAuction},
|
||||
)
|
||||
|
||||
// check init fails
|
||||
require.Panics(t, func() {
|
||||
auction.InitGenesis(ctx, tApp.GetAuctionKeeper(), tApp.GetSupplyKeeper(), gs)
|
||||
})
|
||||
})
|
||||
t.Run("invalid (missing mod account coins)", func(t *testing.T) {
|
||||
// setup keepers
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{})
|
||||
|
||||
// create invalid genesis
|
||||
gs := auction.NewGenesisState(
|
||||
10,
|
||||
auction.DefaultParams(),
|
||||
auction.GenesisAuctions{testAuction},
|
||||
)
|
||||
// invalid as there is no module account setup
|
||||
|
||||
// check init fails
|
||||
require.Panics(t, func() {
|
||||
auction.InitGenesis(ctx, tApp.GetAuctionKeeper(), tApp.GetSupplyKeeper(), gs)
|
||||
|
@ -175,7 +175,7 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder
|
||||
),
|
||||
)
|
||||
if bid.Amount.LT(minNewBidAmt) {
|
||||
return a, types.ErrBidTooSmall(k.codespace, bid, sdk.NewCoin(a.Bid.Denom, minNewBidAmt))
|
||||
return a, types.ErrBidTooSmall(k.codespace, bid, sdk.Coin{Denom: a.Bid.Denom, Amount: minNewBidAmt}) // not using NewCoin as it can panic
|
||||
}
|
||||
|
||||
// New bidder pays back old bidder
|
||||
@ -239,7 +239,7 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc
|
||||
)
|
||||
minNewBidAmt = sdk.MinInt(minNewBidAmt, a.MaxBid.Amount) // allow new bids to hit MaxBid even though it may be less than the increment %
|
||||
if bid.Amount.LT(minNewBidAmt) {
|
||||
return a, types.ErrBidTooSmall(k.codespace, bid, sdk.NewCoin(a.Bid.Denom, minNewBidAmt))
|
||||
return a, types.ErrBidTooSmall(k.codespace, bid, sdk.Coin{Denom: a.Bid.Denom, Amount: minNewBidAmt}) // not using NewCoin as it can panic
|
||||
}
|
||||
if a.MaxBid.IsLT(bid) {
|
||||
return a, types.ErrBidTooLarge(k.codespace, bid, a.MaxBid)
|
||||
@ -314,10 +314,10 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc
|
||||
),
|
||||
)
|
||||
if lot.Amount.GT(maxNewLotAmt) {
|
||||
return a, types.ErrLotTooLarge(k.codespace, lot, sdk.NewCoin(a.Lot.Denom, maxNewLotAmt))
|
||||
return a, types.ErrLotTooLarge(k.codespace, lot, sdk.Coin{Denom: a.Lot.Denom, Amount: maxNewLotAmt}) // not using NewCoin as it can panic
|
||||
}
|
||||
if lot.IsNegative() {
|
||||
return a, types.ErrLotTooSmall(k.codespace, lot, sdk.NewCoin(a.Lot.Denom, sdk.ZeroInt()))
|
||||
return a, types.ErrLotTooSmall(k.codespace, lot, sdk.Coin{Denom: a.Lot.Denom, Amount: sdk.ZeroInt()})
|
||||
}
|
||||
|
||||
// New bidder pays back old bidder
|
||||
@ -380,10 +380,10 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac
|
||||
),
|
||||
)
|
||||
if lot.Amount.GT(maxNewLotAmt) {
|
||||
return a, types.ErrLotTooLarge(k.codespace, lot, sdk.NewCoin(a.Lot.Denom, maxNewLotAmt))
|
||||
return a, types.ErrLotTooLarge(k.codespace, lot, sdk.Coin{Denom: a.Lot.Denom, Amount: maxNewLotAmt}) // not using NewCoin as it can panic
|
||||
}
|
||||
if lot.IsNegative() {
|
||||
return a, types.ErrLotTooSmall(k.codespace, lot, sdk.NewCoin(a.Lot.Denom, sdk.ZeroInt()))
|
||||
return a, types.ErrLotTooSmall(k.codespace, lot, sdk.Coin{Denom: a.Lot.Denom, Amount: sdk.ZeroInt()})
|
||||
}
|
||||
|
||||
// New bidder pays back old bidder
|
||||
|
143
x/auction/keeper/invariants.go
Normal file
143
x/auction/keeper/invariants.go
Normal file
@ -0,0 +1,143 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/store/prefix"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/kava-labs/kava/x/auction/types"
|
||||
)
|
||||
|
||||
// RegisterInvariants registers all staking invariants
|
||||
func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) {
|
||||
|
||||
ir.RegisterRoute(types.ModuleName, "module-account",
|
||||
ModuleAccountInvariants(k))
|
||||
ir.RegisterRoute(types.ModuleName, "valid-auctions",
|
||||
ValidAuctionInvariant(k))
|
||||
ir.RegisterRoute(types.ModuleName, "valid-index",
|
||||
ValidIndexInvariant(k))
|
||||
}
|
||||
|
||||
// ModuleAccountInvariant checks that the module account's coins matches those stored in auctions
|
||||
func ModuleAccountInvariants(k Keeper) sdk.Invariant {
|
||||
return func(ctx sdk.Context) (string, bool) {
|
||||
|
||||
totalAuctionCoins := sdk.NewCoins()
|
||||
k.IterateAuctions(ctx, func(auction types.Auction) bool {
|
||||
a, ok := auction.(types.GenesisAuction)
|
||||
if !ok {
|
||||
panic("stored auction type does not fulfill GenesisAuction interface")
|
||||
}
|
||||
totalAuctionCoins = totalAuctionCoins.Add(a.GetModuleAccountCoins())
|
||||
return false
|
||||
})
|
||||
|
||||
moduleAccCoins := k.supplyKeeper.GetModuleAccount(ctx, types.ModuleName).GetCoins()
|
||||
broken := !moduleAccCoins.IsEqual(totalAuctionCoins)
|
||||
|
||||
invariantMessage := sdk.FormatInvariant(
|
||||
types.ModuleName,
|
||||
"module account",
|
||||
fmt.Sprintf(
|
||||
"\texpected ModuleAccount coins: %s\n"+
|
||||
"\tactual ModuleAccount coins: %s\n",
|
||||
totalAuctionCoins, moduleAccCoins),
|
||||
)
|
||||
return invariantMessage, broken
|
||||
}
|
||||
}
|
||||
|
||||
// ValidAuctionInvariant verifies that all auctions in the store are independently valid
|
||||
func ValidAuctionInvariant(k Keeper) sdk.Invariant {
|
||||
return func(ctx sdk.Context) (string, bool) {
|
||||
var validationErr error
|
||||
var invalidAuction types.Auction
|
||||
k.IterateAuctions(ctx, func(auction types.Auction) bool {
|
||||
a, ok := auction.(types.GenesisAuction)
|
||||
if !ok {
|
||||
panic("stored auction type does not fulfill GenesisAuction interface")
|
||||
}
|
||||
|
||||
currentTime := ctx.BlockTime()
|
||||
if !currentTime.Equal(time.Time{}) { // this avoids a simulator bug where app.InitGenesis is called with blockTime=0 instead of the correct time
|
||||
if a.GetEndTime().Before(currentTime) {
|
||||
validationErr = fmt.Errorf("endTime after current block time (%s)", currentTime)
|
||||
invalidAuction = a
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.Validate(); err != nil {
|
||||
validationErr = err
|
||||
invalidAuction = a
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
broken := validationErr != nil
|
||||
invariantMessage := sdk.FormatInvariant(
|
||||
types.ModuleName,
|
||||
"valid auctions",
|
||||
fmt.Sprintf(
|
||||
"\tfound invalid auction, reason: %s\n"+
|
||||
"\tauction:\n\t%s\n",
|
||||
validationErr, invalidAuction),
|
||||
)
|
||||
return invariantMessage, broken
|
||||
}
|
||||
}
|
||||
|
||||
// ValidIndexInvariant checks that all auctions in the store are also in the index and vice versa.
|
||||
func ValidIndexInvariant(k Keeper) sdk.Invariant {
|
||||
return func(ctx sdk.Context) (string, bool) {
|
||||
/* Method:
|
||||
- check all the auction IDs in the index have a corresponding auction in the store
|
||||
- index is now valid but there could be extra auction in the store
|
||||
- check for these extra auctions by checking num items in the store equals that of index (store keys are always unique)
|
||||
- doesn't check the IDs in the auction structs match the IDs in the keys
|
||||
*/
|
||||
|
||||
// Check all auction IDs in the index are in the auction store
|
||||
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix)
|
||||
|
||||
indexIterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix)
|
||||
defer indexIterator.Close()
|
||||
|
||||
var indexLength int
|
||||
for ; indexIterator.Valid(); indexIterator.Next() {
|
||||
indexLength++
|
||||
|
||||
idBytes := indexIterator.Value()
|
||||
auctionBytes := store.Get(idBytes)
|
||||
if auctionBytes == nil {
|
||||
invariantMessage := sdk.FormatInvariant(
|
||||
types.ModuleName,
|
||||
"valid index",
|
||||
fmt.Sprintf("\tauction with ID '%d' found in index but not in store", types.Uint64FromBytes(idBytes)))
|
||||
return invariantMessage, true
|
||||
}
|
||||
}
|
||||
|
||||
// Check length of auction store matches the length of the index
|
||||
storeIterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix)
|
||||
defer storeIterator.Close()
|
||||
var storeLength int
|
||||
for ; storeIterator.Valid(); storeIterator.Next() {
|
||||
storeLength++
|
||||
}
|
||||
|
||||
if storeLength != indexLength {
|
||||
invariantMessage := sdk.FormatInvariant(
|
||||
types.ModuleName,
|
||||
"valid index",
|
||||
fmt.Sprintf("\tmismatched number of items in auction store (%d) and index (%d)", storeLength, indexLength))
|
||||
return invariantMessage, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
}
|
@ -110,8 +110,10 @@ func NewAppModule(keeper Keeper, supplyKeeper types.SupplyKeeper) AppModule {
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterInvariants performs a no-op.
|
||||
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
|
||||
// RegisterInvariants registers the module invariants.
|
||||
func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {
|
||||
RegisterInvariants(ir, am.keeper)
|
||||
}
|
||||
|
||||
// Route module message route name
|
||||
func (AppModule) Route() string {
|
||||
|
@ -2,21 +2,170 @@ package simulation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/module"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
|
||||
"github.com/cosmos/cosmos-sdk/x/simulation"
|
||||
"github.com/cosmos/cosmos-sdk/x/supply"
|
||||
|
||||
"github.com/kava-labs/kava/x/auction/types"
|
||||
cdptypes "github.com/kava-labs/kava/x/cdp/types"
|
||||
)
|
||||
|
||||
const (
|
||||
// Block time params are un-exported constants in cosmos-sdk/x/simulation.
|
||||
// Copy them here in lieu of importing them.
|
||||
minTimePerBlock time.Duration = (10000 / 2) * time.Second
|
||||
maxTimePerBlock time.Duration = 10000 * time.Second
|
||||
|
||||
// Calculate the average block time
|
||||
AverageBlockTime time.Duration = (maxTimePerBlock - minTimePerBlock) / 2
|
||||
// MaxBidDuration is a crude way of ensuring that BidDuration ≤ MaxAuctionDuration for all generated params
|
||||
MaxBidDuration time.Duration = AverageBlockTime * 50
|
||||
)
|
||||
|
||||
func GenBidDuration(r *rand.Rand) time.Duration {
|
||||
d, err := RandomPositiveDuration(r, 0, MaxBidDuration)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return d
|
||||
}
|
||||
func GenMaxAuctionDuration(r *rand.Rand) time.Duration {
|
||||
d, err := RandomPositiveDuration(r, MaxBidDuration, AverageBlockTime*200)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func GenIncrementCollateral(r *rand.Rand) sdk.Dec {
|
||||
return simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("1"))
|
||||
}
|
||||
|
||||
var GenIncrementDebt = GenIncrementCollateral
|
||||
var GenIncrementSurplus = GenIncrementCollateral
|
||||
|
||||
// RandomizedGenState generates a random GenesisState for auction
|
||||
func RandomizedGenState(simState *module.SimulationState) {
|
||||
|
||||
// TODO implement this fully
|
||||
// - randomly generating the genesis params
|
||||
// - overwriting with genesis provided to simulation
|
||||
auctionGenesis := types.DefaultGenesisState()
|
||||
p := types.NewParams(
|
||||
GenMaxAuctionDuration(simState.Rand),
|
||||
GenBidDuration(simState.Rand),
|
||||
GenIncrementSurplus(simState.Rand),
|
||||
GenIncrementDebt(simState.Rand),
|
||||
GenIncrementCollateral(simState.Rand),
|
||||
)
|
||||
if err := p.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
auctionGenesis := types.NewGenesisState(
|
||||
types.DefaultNextAuctionID,
|
||||
p,
|
||||
nil,
|
||||
)
|
||||
|
||||
// Add auctions
|
||||
auctions := types.GenesisAuctions{
|
||||
types.NewDebtAuction(
|
||||
cdptypes.LiquidatorMacc, // using cdp account rather than generic test one to avoid having to set permissions on the supply keeper
|
||||
sdk.NewInt64Coin("usdx", 100),
|
||||
sdk.NewInt64Coin("ukava", 1000000000000),
|
||||
simState.GenTimestamp.Add(time.Hour*5),
|
||||
sdk.NewInt64Coin("debt", 100), // same as usdx
|
||||
),
|
||||
}
|
||||
var startingID = auctionGenesis.NextAuctionID
|
||||
var ok bool
|
||||
var totalAuctionCoins sdk.Coins
|
||||
for i, a := range auctions {
|
||||
auctions[i], ok = a.WithID(uint64(i) + startingID).(types.GenesisAuction)
|
||||
if !ok {
|
||||
panic("can't convert Auction to GenesisAuction")
|
||||
}
|
||||
totalAuctionCoins = totalAuctionCoins.Add(a.GetModuleAccountCoins())
|
||||
}
|
||||
auctionGenesis.NextAuctionID = startingID + uint64(len(auctions))
|
||||
auctionGenesis.Auctions = append(auctionGenesis.Auctions, auctions...)
|
||||
|
||||
// Also need to update the auction module account (to reflect the coins held in the auctions)
|
||||
var authGenesis auth.GenesisState
|
||||
simState.Cdc.MustUnmarshalJSON(simState.GenState[auth.ModuleName], &authGenesis)
|
||||
|
||||
auctionModAcc, found := getAccount(authGenesis.Accounts, supply.NewModuleAddress(types.ModuleName))
|
||||
if !found {
|
||||
auctionModAcc = supply.NewEmptyModuleAccount(types.ModuleName)
|
||||
}
|
||||
if err := auctionModAcc.SetCoins(totalAuctionCoins); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
authGenesis.Accounts = replaceOrAppendAccount(authGenesis.Accounts, auctionModAcc)
|
||||
|
||||
// TODO adding bidder coins as well - this should be moved elsewhere
|
||||
bidder, found := getAccount(authGenesis.Accounts, simState.Accounts[0].Address) // 0 is the bidder // FIXME
|
||||
if !found {
|
||||
panic("bidder not found")
|
||||
}
|
||||
bidderCoins := sdk.NewCoins(sdk.NewInt64Coin("usdx", 10000000000))
|
||||
if err := bidder.SetCoins(bidder.GetCoins().Add(bidderCoins)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
authGenesis.Accounts = replaceOrAppendAccount(authGenesis.Accounts, bidder)
|
||||
|
||||
simState.GenState[auth.ModuleName] = simState.Cdc.MustMarshalJSON(authGenesis)
|
||||
|
||||
// Update the supply genesis state to reflect the new coins
|
||||
// TODO find some way for this to happen automatically / move it elsewhere
|
||||
var supplyGenesis supply.GenesisState
|
||||
simState.Cdc.MustUnmarshalJSON(simState.GenState[supply.ModuleName], &supplyGenesis)
|
||||
supplyGenesis.Supply = supplyGenesis.Supply.Add(totalAuctionCoins).Add(bidderCoins)
|
||||
simState.GenState[supply.ModuleName] = simState.Cdc.MustMarshalJSON(supplyGenesis)
|
||||
|
||||
// TODO liquidator mod account doesn't need to be initialized for this example
|
||||
// - it just mints kava, doesn't need a starting balance
|
||||
// - and supply.GetModuleAccount creates one if it doesn't exist
|
||||
|
||||
// Note: this line prints out the auction genesis state, not just the auction parameters. Some sdk modules print out just the parameters.
|
||||
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, auctionGenesis))
|
||||
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(auctionGenesis)
|
||||
}
|
||||
|
||||
// Return an account from a list of accounts that matches an address.
|
||||
func getAccount(accounts []authexported.GenesisAccount, addr sdk.AccAddress) (authexported.GenesisAccount, bool) {
|
||||
for _, a := range accounts {
|
||||
if a.GetAddress().Equals(addr) {
|
||||
return a, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// In a list of accounts, replace the first account found with the same address. If not found, append the account.
|
||||
func replaceOrAppendAccount(accounts []authexported.GenesisAccount, acc authexported.GenesisAccount) []authexported.GenesisAccount {
|
||||
newAccounts := accounts
|
||||
for i, a := range accounts {
|
||||
if a.GetAddress().Equals(acc.GetAddress()) {
|
||||
newAccounts[i] = acc
|
||||
return newAccounts
|
||||
}
|
||||
}
|
||||
return append(newAccounts, acc)
|
||||
}
|
||||
|
||||
func RandomPositiveDuration(r *rand.Rand, inclusiveMin, exclusiveMax time.Duration) (time.Duration, error) {
|
||||
min := int64(inclusiveMin)
|
||||
max := int64(exclusiveMax)
|
||||
if min < 0 || max < 0 {
|
||||
return 0, fmt.Errorf("min and max must be positive")
|
||||
}
|
||||
if min >= max {
|
||||
return 0, fmt.Errorf("max must be < min")
|
||||
}
|
||||
randPositiveInt64 := r.Int63n(max-min) + min
|
||||
return time.Duration(randPositiveInt64), nil
|
||||
}
|
||||
|
177
x/auction/simulation/operations/msg.go
Normal file
177
x/auction/simulation/operations/msg.go
Normal file
@ -0,0 +1,177 @@
|
||||
package operations
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/baseapp"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
|
||||
"github.com/cosmos/cosmos-sdk/x/simulation"
|
||||
|
||||
"github.com/kava-labs/kava/x/auction"
|
||||
)
|
||||
|
||||
var (
|
||||
noOpMsg = simulation.NoOpMsg(auction.ModuleName)
|
||||
ErrorNotEnoughCoins = errors.New("account doesn't have enough coins")
|
||||
)
|
||||
|
||||
// Return a function that runs a random state change on the module keeper.
|
||||
// There's two error paths
|
||||
// - return a OpMessage, but nil error - this will log a message but keep running the simulation
|
||||
// - return an error - this will stop the simulation
|
||||
func SimulateMsgPlaceBid(authKeeper auth.AccountKeeper, keeper auction.Keeper) simulation.Operation {
|
||||
handler := auction.NewHandler(keeper)
|
||||
|
||||
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account) (
|
||||
simulation.OperationMsg, []simulation.FutureOperation, error) {
|
||||
|
||||
// get open auctions
|
||||
openAuctions := auction.Auctions{}
|
||||
keeper.IterateAuctions(ctx, func(a auction.Auction) bool {
|
||||
openAuctions = append(openAuctions, a)
|
||||
return false
|
||||
})
|
||||
|
||||
// shuffle auctions slice so that bids are evenly distributed across auctions
|
||||
rand.Shuffle(len(openAuctions), func(i, j int) {
|
||||
openAuctions[i], openAuctions[j] = openAuctions[j], openAuctions[i]
|
||||
})
|
||||
// TODO do the same for accounts?
|
||||
var accounts []authexported.Account
|
||||
for _, acc := range accs {
|
||||
accounts = append(accounts, authKeeper.GetAccount(ctx, acc.Address))
|
||||
}
|
||||
|
||||
// search through auctions and an accounts to find a pair where a bid can be placed (ie account has enough coins to place bid on auction)
|
||||
blockTime := ctx.BlockHeader().Time
|
||||
bidder, openAuction, found := findValidAccountAuctionPair(accounts, openAuctions, func(acc authexported.Account, auc auction.Auction) bool {
|
||||
_, err := generateBidAmount(r, auc, acc, blockTime)
|
||||
if err == ErrorNotEnoughCoins {
|
||||
return false // keep searching
|
||||
} else if err != nil {
|
||||
panic(err) // raise errors
|
||||
}
|
||||
return true // found valid pair
|
||||
})
|
||||
if !found {
|
||||
return simulation.NewOperationMsgBasic(auction.ModuleName, "no-operation (no valid auction and bidder)", "", false, nil), nil, nil
|
||||
}
|
||||
|
||||
// pick a bid amount for the chosen auction and bidder
|
||||
amount, _ := generateBidAmount(r, openAuction, bidder, blockTime)
|
||||
|
||||
// create a msg
|
||||
msg := auction.NewMsgPlaceBid(openAuction.GetID(), bidder.GetAddress(), amount)
|
||||
if err := msg.ValidateBasic(); err != nil { // don't submit errors that fail ValidateBasic, use unit tests for testing ValidateBasic
|
||||
return noOpMsg, nil, fmt.Errorf("expected msg to pass ValidateBasic: %s", msg.GetSignBytes())
|
||||
}
|
||||
|
||||
// submit the msg
|
||||
result := submitMsg(ctx, handler, msg)
|
||||
// Return an operationMsg indicating whether the msg was submitted successfully
|
||||
// Using result.Log as the comment field as it contains any error message emitted by the keeper
|
||||
return simulation.NewOperationMsg(msg, result.IsOK(), result.Log), nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func submitMsg(ctx sdk.Context, handler sdk.Handler, msg sdk.Msg) sdk.Result {
|
||||
ctx, write := ctx.CacheContext()
|
||||
result := handler(ctx, msg)
|
||||
if result.IsOK() {
|
||||
write()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func generateBidAmount(r *rand.Rand, auc auction.Auction, bidder authexported.Account, blockTime time.Time) (sdk.Coin, error) {
|
||||
bidderBalance := bidder.SpendableCoins(blockTime)
|
||||
|
||||
switch a := auc.(type) {
|
||||
|
||||
case auction.DebtAuction:
|
||||
if bidderBalance.AmountOf(a.Bid.Denom).LT(a.Bid.Amount) { // stable coin
|
||||
return sdk.Coin{}, ErrorNotEnoughCoins
|
||||
}
|
||||
amt, err := RandIntInclusive(r, sdk.ZeroInt(), a.Lot.Amount) // pick amount less than current lot amount // TODO min bid increments
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sdk.NewCoin(a.Lot.Denom, amt), nil // gov coin
|
||||
|
||||
case auction.SurplusAuction:
|
||||
if bidderBalance.AmountOf(a.Bid.Denom).LT(a.Bid.Amount) { // gov coin // TODO account for bid increments
|
||||
return sdk.Coin{}, ErrorNotEnoughCoins
|
||||
}
|
||||
amt, err := RandIntInclusive(r, a.Bid.Amount, bidderBalance.AmountOf(a.Bid.Denom))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sdk.NewCoin(a.Bid.Denom, amt), nil // gov coin
|
||||
|
||||
case auction.CollateralAuction:
|
||||
if bidderBalance.AmountOf(a.Bid.Denom).LT(a.Bid.Amount) { // stable coin // TODO account for bid increments (in forward phase)
|
||||
return sdk.Coin{}, ErrorNotEnoughCoins
|
||||
}
|
||||
if a.IsReversePhase() {
|
||||
amt, err := RandIntInclusive(r, sdk.ZeroInt(), a.Lot.Amount) // pick amount less than current lot amount
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sdk.NewCoin(a.Lot.Denom, amt), nil // collateral coin
|
||||
} else {
|
||||
amt, err := RandIntInclusive(r, a.Bid.Amount, sdk.MinInt(bidderBalance.AmountOf(a.Bid.Denom), a.MaxBid.Amount))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// pick the MaxBid amount more frequently to increase chance auctions phase get into reverse phase
|
||||
if r.Intn(10) == 0 { // 10%
|
||||
amt = a.MaxBid.Amount
|
||||
}
|
||||
return sdk.NewCoin(a.Bid.Denom, amt), nil // stable coin
|
||||
}
|
||||
|
||||
default:
|
||||
return sdk.Coin{}, fmt.Errorf("unknown auction type")
|
||||
}
|
||||
}
|
||||
|
||||
// findValidAccountAuctionPair finds an auction and account for which the callback func returns true
|
||||
func findValidAccountAuctionPair(accounts []authexported.Account, auctions auction.Auctions, cb func(authexported.Account, auction.Auction) bool) (authexported.Account, auction.Auction, bool) {
|
||||
for _, auc := range auctions {
|
||||
for _, acc := range accounts {
|
||||
if isValid := cb(acc, auc); isValid {
|
||||
return acc, auc, true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
// RandInt randomly generates an sdk.Int in the range [inclusiveMin, inclusiveMax]. It works for negative and positive integers.
|
||||
func RandIntInclusive(r *rand.Rand, inclusiveMin, inclusiveMax sdk.Int) (sdk.Int, error) {
|
||||
if inclusiveMin.GT(inclusiveMax) {
|
||||
return sdk.Int{}, fmt.Errorf("min larger than max")
|
||||
}
|
||||
return RandInt(r, inclusiveMin, inclusiveMax.Add(sdk.OneInt()))
|
||||
}
|
||||
|
||||
// RandInt randomly generates an sdk.Int in the range [inclusiveMin, exclusiveMax). It works for negative and positive integers.
|
||||
func RandInt(r *rand.Rand, inclusiveMin, exclusiveMax sdk.Int) (sdk.Int, error) {
|
||||
// validate input
|
||||
if inclusiveMin.GTE(exclusiveMax) {
|
||||
return sdk.Int{}, fmt.Errorf("min larger or equal to max")
|
||||
}
|
||||
// shift the range to start at 0
|
||||
shiftedRange := exclusiveMax.Sub(inclusiveMin) // should always be positive given the check above
|
||||
// randomly pick from the shifted range
|
||||
shiftedRandInt := sdk.NewIntFromBigInt(new(big.Int).Rand(r, shiftedRange.BigInt()))
|
||||
// shift back to the original range
|
||||
return shiftedRandInt.Add(inclusiveMin), nil
|
||||
}
|
@ -1,14 +1,46 @@
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/x/simulation"
|
||||
|
||||
"github.com/kava-labs/kava/x/auction/types"
|
||||
)
|
||||
|
||||
// ParamChanges defines the parameters that can be modified by param change proposals
|
||||
// on the simulation
|
||||
func ParamChanges(r *rand.Rand) []simulation.ParamChange {
|
||||
// TODO implement this
|
||||
return []simulation.ParamChange{}
|
||||
// Note: params are encoded to JSON before being stored in the param store. These param changes
|
||||
// update the raw values in the store so values need to be JSON. This is why values that are represented
|
||||
// as strings in JSON (such as time.Duration) have the escaped quotes.
|
||||
// TODO should we encode the values properly with ModuleCdc.MustMarshalJSON()?
|
||||
return []simulation.ParamChange{
|
||||
simulation.NewSimParamChange(types.ModuleName, string(types.KeyBidDuration), "",
|
||||
func(r *rand.Rand) string {
|
||||
return fmt.Sprintf("\"%d\"", GenBidDuration(r))
|
||||
},
|
||||
),
|
||||
simulation.NewSimParamChange(types.ModuleName, string(types.KeyMaxAuctionDuration), "",
|
||||
func(r *rand.Rand) string {
|
||||
return fmt.Sprintf("\"%d\"", GenMaxAuctionDuration(r))
|
||||
},
|
||||
),
|
||||
simulation.NewSimParamChange(types.ModuleName, string(types.KeyIncrementCollateral), "",
|
||||
func(r *rand.Rand) string {
|
||||
return fmt.Sprintf("\"%d\"", GenIncrementCollateral(r))
|
||||
},
|
||||
),
|
||||
simulation.NewSimParamChange(types.ModuleName, string(types.KeyIncrementDebt), "",
|
||||
func(r *rand.Rand) string {
|
||||
return fmt.Sprintf("\"%d\"", GenIncrementDebt(r))
|
||||
},
|
||||
),
|
||||
simulation.NewSimParamChange(types.ModuleName, string(types.KeyIncrementSurplus), "",
|
||||
func(r *rand.Rand) string {
|
||||
return fmt.Sprintf("\"%d\"", GenIncrementSurplus(r))
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -69,6 +69,12 @@ func (a BaseAuction) Validate() error {
|
||||
if a.EndTime.After(a.MaxEndTime) {
|
||||
return fmt.Errorf("MaxEndTime < EndTime (%s < %s)", a.MaxEndTime, a.EndTime)
|
||||
}
|
||||
if !a.Lot.IsValid() {
|
||||
return fmt.Errorf("invalid lot: %s", a.Lot)
|
||||
}
|
||||
if !a.Bid.IsValid() {
|
||||
return fmt.Errorf("invalid bid: %s", a.Bid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -148,6 +154,13 @@ func (a DebtAuction) GetModuleAccountCoins() sdk.Coins {
|
||||
// GetPhase returns the direction of a debt auction, which never changes.
|
||||
func (a DebtAuction) GetPhase() string { return "reverse" }
|
||||
|
||||
func (a DebtAuction) Validate() error {
|
||||
if !a.CorrespondingDebt.IsValid() {
|
||||
return fmt.Errorf("invalid corresponding debt: %s", a.CorrespondingDebt)
|
||||
}
|
||||
return a.BaseAuction.Validate()
|
||||
}
|
||||
|
||||
// NewDebtAuction returns a new debt auction.
|
||||
func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, endTime time.Time, debt sdk.Coin) DebtAuction {
|
||||
// Note: Bidder is set to the initiator's module account address instead of module name. (when the first bid is placed, it is paid out to the initiator)
|
||||
@ -208,6 +221,19 @@ func (a CollateralAuction) GetPhase() string {
|
||||
return "forward"
|
||||
}
|
||||
|
||||
func (a CollateralAuction) Validate() error {
|
||||
if !a.CorrespondingDebt.IsValid() {
|
||||
return fmt.Errorf("invalid corresponding debt: %s", a.CorrespondingDebt)
|
||||
}
|
||||
if !a.MaxBid.IsValid() {
|
||||
return fmt.Errorf("invalid max bid: %s", a.MaxBid)
|
||||
}
|
||||
if err := a.LotReturns.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid lot returns: %w", err)
|
||||
}
|
||||
return a.BaseAuction.Validate()
|
||||
}
|
||||
|
||||
func (a CollateralAuction) String() string {
|
||||
return fmt.Sprintf(`Auction %d:
|
||||
Initiator: %s
|
||||
@ -251,16 +277,24 @@ type WeightedAddresses struct {
|
||||
|
||||
// NewWeightedAddresses returns a new list addresses with weights.
|
||||
func NewWeightedAddresses(addrs []sdk.AccAddress, weights []sdk.Int) (WeightedAddresses, sdk.Error) {
|
||||
if len(addrs) != len(weights) {
|
||||
return WeightedAddresses{}, sdk.ErrInternal("number of addresses doesn't match number of weights")
|
||||
}
|
||||
for _, w := range weights {
|
||||
if w.IsNegative() {
|
||||
return WeightedAddresses{}, sdk.ErrInternal("weights contain a negative amount")
|
||||
}
|
||||
}
|
||||
return WeightedAddresses{
|
||||
wa := WeightedAddresses{
|
||||
Addresses: addrs,
|
||||
Weights: weights,
|
||||
}, nil
|
||||
}
|
||||
if err := wa.Validate(); err != nil {
|
||||
return WeightedAddresses{}, sdk.ErrInternal(err.Error())
|
||||
}
|
||||
return wa, nil
|
||||
}
|
||||
|
||||
func (wa WeightedAddresses) Validate() error {
|
||||
if len(wa.Addresses) != len(wa.Weights) {
|
||||
return fmt.Errorf("number of addresses doesn't match number of weights")
|
||||
}
|
||||
for _, w := range wa.Weights {
|
||||
if w.IsNegative() {
|
||||
return fmt.Errorf("weights contain a negative amount")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user