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:
Ruaridh 2020-04-13 17:01:54 +01:00 committed by GitHub
parent 8d199746cd
commit 5bdffd5c1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 609 additions and 35 deletions

View File

@ -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)

View File

@ -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),

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View 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
}
}

View File

@ -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 {

View File

@ -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
}

View 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
}

View File

@ -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))
},
),
}
}

View File

@ -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
}