mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-11-13 03:25:20 +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)
|
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(
|
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,
|
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?
|
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)
|
app.mm.RegisterInvariants(&app.crisisKeeper)
|
||||||
|
@ -35,6 +35,7 @@ import (
|
|||||||
stakingsimops "github.com/cosmos/cosmos-sdk/x/staking/simulation/operations"
|
stakingsimops "github.com/cosmos/cosmos-sdk/x/staking/simulation/operations"
|
||||||
"github.com/cosmos/cosmos-sdk/x/supply"
|
"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"
|
bep3simops "github.com/kava-labs/kava/x/bep3/simulation/operations"
|
||||||
pricefeedsimops "github.com/kava-labs/kava/x/pricefeed/simulation/operations"
|
pricefeedsimops "github.com/kava-labs/kava/x/pricefeed/simulation/operations"
|
||||||
)
|
)
|
||||||
@ -59,6 +60,7 @@ const (
|
|||||||
OpWeightMsgUndelegate = "op_weight_msg_undelegate"
|
OpWeightMsgUndelegate = "op_weight_msg_undelegate"
|
||||||
OpWeightMsgBeginRedelegate = "op_weight_msg_begin_redelegate"
|
OpWeightMsgBeginRedelegate = "op_weight_msg_begin_redelegate"
|
||||||
OpWeightMsgUnjail = "op_weight_msg_unjail"
|
OpWeightMsgUnjail = "op_weight_msg_unjail"
|
||||||
|
OpWeightMsgPlaceBid = "op_weight_msg_place_bid"
|
||||||
OpWeightMsgPricefeed = "op_weight_msg_pricefeed"
|
OpWeightMsgPricefeed = "op_weight_msg_pricefeed"
|
||||||
OpWeightMsgCreateAtomicSwap = "op_weight_msg_create_atomic_Swap"
|
OpWeightMsgCreateAtomicSwap = "op_weight_msg_create_atomic_Swap"
|
||||||
)
|
)
|
||||||
@ -269,6 +271,17 @@ func testAndRunTxs(app *App, config simulation.Config) []simulation.WeightedOper
|
|||||||
}(nil),
|
}(nil),
|
||||||
slashingsimops.SimulateMsgUnjail(app.slashingKeeper),
|
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 {
|
func(_ *rand.Rand) int {
|
||||||
var v int
|
var v int
|
||||||
@ -285,7 +298,7 @@ func testAndRunTxs(app *App, config simulation.Config) []simulation.WeightedOper
|
|||||||
var v int
|
var v int
|
||||||
ap.GetOrGenerate(app.cdc, OpWeightMsgPricefeed, &v, nil,
|
ap.GetOrGenerate(app.cdc, OpWeightMsgPricefeed, &v, nil,
|
||||||
func(_ *rand.Rand) {
|
func(_ *rand.Rand) {
|
||||||
v = 10000 // TODO
|
v = 100
|
||||||
})
|
})
|
||||||
return v
|
return v
|
||||||
}(nil),
|
}(nil),
|
||||||
|
@ -55,6 +55,7 @@ var (
|
|||||||
// functions aliases
|
// functions aliases
|
||||||
NewKeeper = keeper.NewKeeper
|
NewKeeper = keeper.NewKeeper
|
||||||
NewQuerier = keeper.NewQuerier
|
NewQuerier = keeper.NewQuerier
|
||||||
|
RegisterInvariants = keeper.RegisterInvariants
|
||||||
NewSurplusAuction = types.NewSurplusAuction
|
NewSurplusAuction = types.NewSurplusAuction
|
||||||
NewDebtAuction = types.NewDebtAuction
|
NewDebtAuction = types.NewDebtAuction
|
||||||
NewCollateralAuction = types.NewCollateralAuction
|
NewCollateralAuction = types.NewCollateralAuction
|
||||||
|
@ -22,7 +22,7 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper types.SupplyKeeper
|
|||||||
for _, a := range gs.Auctions {
|
for _, a := range gs.Auctions {
|
||||||
keeper.SetAuction(ctx, a)
|
keeper.SetAuction(ctx, a)
|
||||||
// find the total coins that should be present in the module account
|
// 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
|
// check if the module account exists
|
||||||
|
@ -29,6 +29,12 @@ func TestInitGenesis(t *testing.T) {
|
|||||||
tApp := app.NewTestApp()
|
tApp := app.NewTestApp()
|
||||||
keeper := tApp.GetAuctionKeeper()
|
keeper := tApp.GetAuctionKeeper()
|
||||||
ctx := tApp.NewContext(true, abci.Header{})
|
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
|
// create genesis
|
||||||
gs := auction.NewGenesisState(
|
gs := auction.NewGenesisState(
|
||||||
10,
|
10,
|
||||||
@ -38,7 +44,7 @@ func TestInitGenesis(t *testing.T) {
|
|||||||
|
|
||||||
// run init
|
// run init
|
||||||
require.NotPanics(t, func() {
|
require.NotPanics(t, func() {
|
||||||
auction.InitGenesis(ctx, keeper, tApp.GetSupplyKeeper(), gs)
|
auction.InitGenesis(ctx, keeper, supplyKeeper, gs)
|
||||||
})
|
})
|
||||||
|
|
||||||
// check state is as expected
|
// check state is as expected
|
||||||
@ -59,7 +65,7 @@ func TestInitGenesis(t *testing.T) {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
t.Run("invalid", func(t *testing.T) {
|
t.Run("invalid (invalid nextAuctionID)", func(t *testing.T) {
|
||||||
// setup keepers
|
// setup keepers
|
||||||
tApp := app.NewTestApp()
|
tApp := app.NewTestApp()
|
||||||
ctx := tApp.NewContext(true, abci.Header{})
|
ctx := tApp.NewContext(true, abci.Header{})
|
||||||
@ -71,6 +77,24 @@ func TestInitGenesis(t *testing.T) {
|
|||||||
auction.GenesisAuctions{testAuction},
|
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
|
// check init fails
|
||||||
require.Panics(t, func() {
|
require.Panics(t, func() {
|
||||||
auction.InitGenesis(ctx, tApp.GetAuctionKeeper(), tApp.GetSupplyKeeper(), gs)
|
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) {
|
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
|
// 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 %
|
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) {
|
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) {
|
if a.MaxBid.IsLT(bid) {
|
||||||
return a, types.ErrBidTooLarge(k.codespace, bid, a.MaxBid)
|
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) {
|
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() {
|
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
|
// 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) {
|
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() {
|
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
|
// 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.
|
// RegisterInvariants registers the module invariants.
|
||||||
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
|
func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {
|
||||||
|
RegisterInvariants(ir, am.keeper)
|
||||||
|
}
|
||||||
|
|
||||||
// Route module message route name
|
// Route module message route name
|
||||||
func (AppModule) Route() string {
|
func (AppModule) Route() string {
|
||||||
|
@ -2,21 +2,170 @@ package simulation
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/cosmos/cosmos-sdk/codec"
|
"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/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"
|
"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
|
// RandomizedGenState generates a random GenesisState for auction
|
||||||
func RandomizedGenState(simState *module.SimulationState) {
|
func RandomizedGenState(simState *module.SimulationState) {
|
||||||
|
|
||||||
// TODO implement this fully
|
p := types.NewParams(
|
||||||
// - randomly generating the genesis params
|
GenMaxAuctionDuration(simState.Rand),
|
||||||
// - overwriting with genesis provided to simulation
|
GenBidDuration(simState.Rand),
|
||||||
auctionGenesis := types.DefaultGenesisState()
|
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))
|
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)
|
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
|
package simulation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
|
||||||
"github.com/cosmos/cosmos-sdk/x/simulation"
|
"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
|
// ParamChanges defines the parameters that can be modified by param change proposals
|
||||||
// on the simulation
|
// on the simulation
|
||||||
func ParamChanges(r *rand.Rand) []simulation.ParamChange {
|
func ParamChanges(r *rand.Rand) []simulation.ParamChange {
|
||||||
// TODO implement this
|
// Note: params are encoded to JSON before being stored in the param store. These param changes
|
||||||
return []simulation.ParamChange{}
|
// 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) {
|
if a.EndTime.After(a.MaxEndTime) {
|
||||||
return fmt.Errorf("MaxEndTime < EndTime (%s < %s)", a.MaxEndTime, a.EndTime)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,6 +154,13 @@ func (a DebtAuction) GetModuleAccountCoins() sdk.Coins {
|
|||||||
// GetPhase returns the direction of a debt auction, which never changes.
|
// GetPhase returns the direction of a debt auction, which never changes.
|
||||||
func (a DebtAuction) GetPhase() string { return "reverse" }
|
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.
|
// NewDebtAuction returns a new debt auction.
|
||||||
func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, endTime time.Time, debt sdk.Coin) DebtAuction {
|
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)
|
// 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"
|
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 {
|
func (a CollateralAuction) String() string {
|
||||||
return fmt.Sprintf(`Auction %d:
|
return fmt.Sprintf(`Auction %d:
|
||||||
Initiator: %s
|
Initiator: %s
|
||||||
@ -251,16 +277,24 @@ type WeightedAddresses struct {
|
|||||||
|
|
||||||
// NewWeightedAddresses returns a new list addresses with weights.
|
// NewWeightedAddresses returns a new list addresses with weights.
|
||||||
func NewWeightedAddresses(addrs []sdk.AccAddress, weights []sdk.Int) (WeightedAddresses, sdk.Error) {
|
func NewWeightedAddresses(addrs []sdk.AccAddress, weights []sdk.Int) (WeightedAddresses, sdk.Error) {
|
||||||
if len(addrs) != len(weights) {
|
wa := WeightedAddresses{
|
||||||
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{
|
|
||||||
Addresses: addrs,
|
Addresses: addrs,
|
||||||
Weights: weights,
|
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