2020-04-23 16:35:58 +00:00
|
|
|
package simulation
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"math/big"
|
|
|
|
"math/rand"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/cosmos/cosmos-sdk/baseapp"
|
|
|
|
"github.com/cosmos/cosmos-sdk/codec"
|
|
|
|
"github.com/cosmos/cosmos-sdk/simapp/helpers"
|
|
|
|
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"
|
|
|
|
|
|
|
|
appparams "github.com/kava-labs/kava/app/params"
|
|
|
|
"github.com/kava-labs/kava/x/auction/keeper"
|
|
|
|
"github.com/kava-labs/kava/x/auction/types"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2020-04-23 21:51:37 +00:00
|
|
|
errorNotEnoughCoins = errors.New("account doesn't have enough coins")
|
|
|
|
errorCantReceiveBids = errors.New("auction can't receive bids (lot = 0 in reverse auction)")
|
2020-04-23 16:35:58 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Simulation operation weights constants
|
|
|
|
const (
|
|
|
|
OpWeightMsgPlaceBid = "op_weight_msg_place_bid"
|
|
|
|
)
|
|
|
|
|
|
|
|
// WeightedOperations returns all the operations from the module with their respective weights
|
|
|
|
func WeightedOperations(
|
|
|
|
appParams simulation.AppParams, cdc *codec.Codec, ak auth.AccountKeeper, k keeper.Keeper,
|
|
|
|
) simulation.WeightedOperations {
|
|
|
|
var weightMsgPlaceBid int
|
|
|
|
|
|
|
|
appParams.GetOrGenerate(cdc, OpWeightMsgPlaceBid, &weightMsgPlaceBid, nil,
|
|
|
|
func(_ *rand.Rand) {
|
|
|
|
weightMsgPlaceBid = appparams.DefaultWeightMsgPlaceBid
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
return simulation.WeightedOperations{
|
|
|
|
simulation.NewWeightedOperation(
|
|
|
|
weightMsgPlaceBid,
|
|
|
|
SimulateMsgPlaceBid(ak, k),
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SimulateMsgPlaceBid returns 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(ak auth.AccountKeeper, keeper keeper.Keeper) simulation.Operation {
|
|
|
|
return func(
|
|
|
|
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
|
|
|
|
) (simulation.OperationMsg, []simulation.FutureOperation, error) {
|
|
|
|
// get open auctions
|
|
|
|
openAuctions := types.Auctions{}
|
|
|
|
keeper.IterateAuctions(ctx, func(a types.Auction) bool {
|
|
|
|
openAuctions = append(openAuctions, a)
|
|
|
|
return false
|
|
|
|
})
|
|
|
|
|
|
|
|
// shuffle auctions slice so that bids are evenly distributed across auctions
|
|
|
|
r.Shuffle(len(openAuctions), func(i, j int) {
|
|
|
|
openAuctions[i], openAuctions[j] = openAuctions[j], openAuctions[i]
|
|
|
|
})
|
|
|
|
|
2020-09-01 15:33:12 +00:00
|
|
|
// search through auctions and accounts to find a pair where a bid can be placed (ie account has enough coins to place bid on auction)
|
2020-04-23 16:35:58 +00:00
|
|
|
blockTime := ctx.BlockHeader().Time
|
2020-04-23 21:51:37 +00:00
|
|
|
params := keeper.GetParams(ctx)
|
2020-04-23 16:35:58 +00:00
|
|
|
bidder, openAuction, found := findValidAccountAuctionPair(accs, openAuctions, func(acc simulation.Account, auc types.Auction) bool {
|
|
|
|
account := ak.GetAccount(ctx, acc.Address)
|
2020-04-23 21:51:37 +00:00
|
|
|
_, err := generateBidAmount(r, params, auc, account, blockTime)
|
|
|
|
if err == errorNotEnoughCoins || err == errorCantReceiveBids {
|
2020-04-23 16:35:58 +00:00
|
|
|
return false // keep searching
|
|
|
|
} else if err != nil {
|
|
|
|
panic(err) // raise errors
|
|
|
|
}
|
|
|
|
return true // found valid pair
|
|
|
|
})
|
|
|
|
if !found {
|
|
|
|
return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation (no valid auction and bidder)", "", false, nil), nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
bidderAcc := ak.GetAccount(ctx, bidder.Address)
|
|
|
|
if bidderAcc == nil {
|
2020-04-23 21:51:37 +00:00
|
|
|
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("couldn't find account %s", bidder.Address)
|
2020-04-23 16:35:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// pick a bid amount for the chosen auction and bidder
|
2020-04-23 21:51:37 +00:00
|
|
|
amount, err := generateBidAmount(r, params, openAuction, bidderAcc, blockTime)
|
|
|
|
if err != nil { // shouldn't happen given the checks above
|
2020-04-23 16:35:58 +00:00
|
|
|
return simulation.NoOpMsg(types.ModuleName), nil, err
|
|
|
|
}
|
|
|
|
|
2020-04-23 21:51:37 +00:00
|
|
|
// create and deliver a tx
|
2020-04-23 16:35:58 +00:00
|
|
|
msg := types.NewMsgPlaceBid(openAuction.GetID(), bidder.Address, amount)
|
|
|
|
|
|
|
|
tx := helpers.GenTx(
|
|
|
|
[]sdk.Msg{msg},
|
2020-04-23 21:51:37 +00:00
|
|
|
sdk.NewCoins(), // TODO pick a random amount fees
|
2020-04-23 16:35:58 +00:00
|
|
|
helpers.DefaultGenTxGas,
|
|
|
|
chainID,
|
|
|
|
[]uint64{bidderAcc.GetAccountNumber()},
|
|
|
|
[]uint64{bidderAcc.GetSequence()},
|
|
|
|
bidder.PrivKey,
|
|
|
|
)
|
|
|
|
|
2020-04-30 21:02:42 +00:00
|
|
|
_, _, err = app.Deliver(tx)
|
2020-04-23 16:35:58 +00:00
|
|
|
if err != nil {
|
2020-04-23 21:51:37 +00:00
|
|
|
// to aid debugging, add the stack trace to the comment field of the returned opMsg
|
|
|
|
return simulation.NewOperationMsg(msg, false, fmt.Sprintf("%+v", err)), nil, err
|
2020-04-23 16:35:58 +00:00
|
|
|
}
|
2020-04-30 21:02:42 +00:00
|
|
|
return simulation.NewOperationMsg(msg, true, ""), nil, nil
|
2020-04-23 16:35:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-23 21:51:37 +00:00
|
|
|
func generateBidAmount(
|
|
|
|
r *rand.Rand, params types.Params, auc types.Auction,
|
|
|
|
bidder authexported.Account, blockTime time.Time) (sdk.Coin, error) {
|
2020-04-23 16:35:58 +00:00
|
|
|
bidderBalance := bidder.SpendableCoins(blockTime)
|
|
|
|
|
|
|
|
switch a := auc.(type) {
|
|
|
|
|
|
|
|
case types.DebtAuction:
|
2020-04-23 21:51:37 +00:00
|
|
|
// Check bidder has enough (stable coin) to pay in
|
2020-04-23 16:35:58 +00:00
|
|
|
if bidderBalance.AmountOf(a.Bid.Denom).LT(a.Bid.Amount) { // stable coin
|
2020-04-23 21:51:37 +00:00
|
|
|
return sdk.Coin{}, errorNotEnoughCoins
|
|
|
|
}
|
|
|
|
// Check auction can still receive new bids
|
|
|
|
if a.Lot.Amount.Equal(sdk.ZeroInt()) {
|
|
|
|
return sdk.Coin{}, errorCantReceiveBids
|
2020-04-23 16:35:58 +00:00
|
|
|
}
|
2020-04-23 21:51:37 +00:00
|
|
|
// Generate a new lot amount (gov coin)
|
|
|
|
maxNewLotAmt := a.Lot.Amount.Sub( // new lot must be some % less than old lot, and at least 1 smaller to avoid replacing an old bid at no cost
|
|
|
|
sdk.MaxInt(
|
|
|
|
sdk.NewInt(1),
|
|
|
|
sdk.NewDecFromInt(a.Lot.Amount).Mul(params.IncrementDebt).RoundInt(),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
amt, err := RandIntInclusive(r, sdk.ZeroInt(), maxNewLotAmt) // maxNewLotAmt shouldn't be < 0 given the check above
|
2020-04-23 16:35:58 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return sdk.NewCoin(a.Lot.Denom, amt), nil // gov coin
|
|
|
|
|
|
|
|
case types.SurplusAuction:
|
2020-04-23 21:51:37 +00:00
|
|
|
// Check the bidder has enough (gov coin) to pay in
|
|
|
|
minNewBidAmt := a.Bid.Amount.Add( // new bids must be some % greater than old bid, and at least 1 larger to avoid replacing an old bid at no cost
|
|
|
|
sdk.MaxInt(
|
|
|
|
sdk.NewInt(1),
|
|
|
|
sdk.NewDecFromInt(a.Bid.Amount).Mul(params.IncrementSurplus).RoundInt(),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
if bidderBalance.AmountOf(a.Bid.Denom).LT(minNewBidAmt) { // gov coin
|
|
|
|
return sdk.Coin{}, errorNotEnoughCoins
|
2020-04-23 16:35:58 +00:00
|
|
|
}
|
2020-04-23 21:51:37 +00:00
|
|
|
// Generate a new bid amount (gov coin)
|
|
|
|
amt, err := RandIntInclusive(r, minNewBidAmt, bidderBalance.AmountOf(a.Bid.Denom))
|
2020-04-23 16:35:58 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return sdk.NewCoin(a.Bid.Denom, amt), nil // gov coin
|
|
|
|
|
|
|
|
case types.CollateralAuction:
|
2020-04-23 21:51:37 +00:00
|
|
|
// Check the bidder has enough (stable coin) to pay in
|
|
|
|
minNewBidAmt := a.Bid.Amount.Add( // new bids must be some % greater than old bid, and at least 1 larger to avoid replacing an old bid at no cost
|
|
|
|
sdk.MaxInt(
|
|
|
|
sdk.NewInt(1),
|
|
|
|
sdk.NewDecFromInt(a.Bid.Amount).Mul(params.IncrementCollateral).RoundInt(),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
minNewBidAmt = sdk.MinInt(minNewBidAmt, a.MaxBid.Amount) // allow new bids to hit MaxBid even though it may be less than the increment %
|
|
|
|
if bidderBalance.AmountOf(a.Bid.Denom).LT(minNewBidAmt) {
|
|
|
|
return sdk.Coin{}, errorNotEnoughCoins
|
2020-04-23 16:35:58 +00:00
|
|
|
}
|
2020-04-23 21:51:37 +00:00
|
|
|
// Check auction can still receive new bids
|
|
|
|
if a.IsReversePhase() && a.Lot.Amount.Equal(sdk.ZeroInt()) {
|
|
|
|
return sdk.Coin{}, errorCantReceiveBids
|
|
|
|
}
|
|
|
|
// Generate a new bid amount (collateral coin in reverse phase)
|
2020-04-23 16:35:58 +00:00
|
|
|
if a.IsReversePhase() {
|
2020-04-23 21:51:37 +00:00
|
|
|
maxNewLotAmt := a.Lot.Amount.Sub( // new lot must be some % less than old lot, and at least 1 smaller to avoid replacing an old bid at no cost
|
|
|
|
sdk.MaxInt(
|
|
|
|
sdk.NewInt(1),
|
|
|
|
sdk.NewDecFromInt(a.Lot.Amount).Mul(params.IncrementCollateral).RoundInt(),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
amt, err := RandIntInclusive(r, sdk.ZeroInt(), maxNewLotAmt) // maxNewLotAmt shouldn't be < 0 given the check above
|
2020-04-23 16:35:58 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return sdk.NewCoin(a.Lot.Denom, amt), nil // collateral coin
|
2020-04-23 21:51:37 +00:00
|
|
|
|
|
|
|
// Generate a new bid amount (stable coin in forward phase)
|
|
|
|
} else {
|
|
|
|
amt, err := RandIntInclusive(r, minNewBidAmt, sdk.MinInt(bidderBalance.AmountOf(a.Bid.Denom), a.MaxBid.Amount))
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
// when the bidder has enough coins, pick the MaxBid amount more frequently to increase chance auctions phase get into reverse phase
|
|
|
|
if r.Intn(2) == 0 && bidderBalance.AmountOf(a.Bid.Denom).GTE(a.MaxBid.Amount) { // 50%
|
|
|
|
amt = a.MaxBid.Amount
|
|
|
|
}
|
|
|
|
return sdk.NewCoin(a.Bid.Denom, amt), nil // stable coin
|
2020-04-23 16:35:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 []simulation.Account, auctions types.Auctions, cb func(simulation.Account, types.Auction) bool) (simulation.Account, types.Auction, bool) {
|
|
|
|
for _, auc := range auctions {
|
|
|
|
for _, acc := range accounts {
|
|
|
|
if isValid := cb(acc, auc); isValid {
|
|
|
|
return acc, auc, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return simulation.Account{}, nil, false
|
|
|
|
}
|
|
|
|
|
|
|
|
// RandIntInclusive 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
|
|
|
|
}
|