mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-26 00:05:18 +00:00
Various Auction TODOs (#281)
* make auctions not expire without bids * add events * improve genesis state validation * add genesis tests * update comment Co-Authored-By: Kevin Davis <karzak@users.noreply.github.com> * add more events attributes Co-authored-by: Kevin Davis <karzak@users.noreply.github.com>
This commit is contained in:
parent
aa6dfab6fd
commit
22e168d06a
@ -261,7 +261,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
|
||||
slashing.NewAppModule(app.slashingKeeper, app.stakingKeeper),
|
||||
staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper),
|
||||
validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper),
|
||||
auction.NewAppModule(app.auctionKeeper),
|
||||
auction.NewAppModule(app.auctionKeeper, app.supplyKeeper),
|
||||
cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper),
|
||||
pricefeed.NewAppModule(app.pricefeedKeeper),
|
||||
)
|
||||
|
@ -29,7 +29,6 @@ var (
|
||||
RegisterCodec = types.RegisterCodec
|
||||
NewGenesisState = types.NewGenesisState
|
||||
DefaultGenesisState = types.DefaultGenesisState
|
||||
ValidateGenesis = types.ValidateGenesis
|
||||
GetAuctionKey = types.GetAuctionKey
|
||||
GetAuctionByTimeKey = types.GetAuctionByTimeKey
|
||||
Uint64FromBytes = types.Uint64FromBytes
|
||||
@ -58,7 +57,8 @@ type (
|
||||
CollateralAuction = types.CollateralAuction
|
||||
WeightedAddresses = types.WeightedAddresses
|
||||
SupplyKeeper = types.SupplyKeeper
|
||||
Auctions = types.Auctions
|
||||
GenesisAuctions = types.GenesisAuctions
|
||||
GenesisAuction = types.GenesisAuction
|
||||
GenesisState = types.GenesisState
|
||||
MsgPlaceBid = types.MsgPlaceBid
|
||||
Params = types.Params
|
||||
|
@ -1,17 +1,39 @@
|
||||
package auction
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/kava-labs/kava/x/auction/types"
|
||||
)
|
||||
|
||||
// InitGenesis initializes the store state from genesis data.
|
||||
func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) {
|
||||
keeper.SetNextAuctionID(ctx, data.NextAuctionID)
|
||||
// InitGenesis initializes the store state from a genesis state.
|
||||
func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper types.SupplyKeeper, gs GenesisState) {
|
||||
if err := gs.Validate(); err != nil {
|
||||
panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
|
||||
}
|
||||
|
||||
keeper.SetParams(ctx, data.Params)
|
||||
keeper.SetNextAuctionID(ctx, gs.NextAuctionID)
|
||||
|
||||
for _, a := range data.Auctions {
|
||||
keeper.SetParams(ctx, gs.Params)
|
||||
|
||||
totalAuctionCoins := sdk.NewCoins()
|
||||
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())
|
||||
}
|
||||
|
||||
// check if the module account exists
|
||||
moduleAcc := supplyKeeper.GetModuleAccount(ctx, ModuleName)
|
||||
if moduleAcc == nil {
|
||||
panic(fmt.Sprintf("%s module account has not been set", ModuleName))
|
||||
}
|
||||
// check module coins match auction coins
|
||||
// Note: Other sdk modules do not check this, instead just using the existing module account coins, or if zero, setting them.
|
||||
if !moduleAcc.GetCoins().IsEqual(totalAuctionCoins) {
|
||||
panic(fmt.Sprintf("total auction coins (%s) do not equal (%s) module account (%s) ", moduleAcc.GetCoins(), ModuleName, totalAuctionCoins))
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,9 +46,13 @@ func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState {
|
||||
|
||||
params := keeper.GetParams(ctx)
|
||||
|
||||
var genAuctions Auctions
|
||||
genAuctions := GenesisAuctions{} // return empty list instead of nil if no auctions
|
||||
keeper.IterateAuctions(ctx, func(a Auction) bool {
|
||||
genAuctions = append(genAuctions, a)
|
||||
ga, ok := a.(types.GenesisAuction)
|
||||
if !ok {
|
||||
panic("could not convert stored auction to GenesisAuction type")
|
||||
}
|
||||
genAuctions = append(genAuctions, ga)
|
||||
return false
|
||||
})
|
||||
|
||||
|
109
x/auction/genesis_test.go
Normal file
109
x/auction/genesis_test.go
Normal file
@ -0,0 +1,109 @@
|
||||
package auction_test
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"github.com/kava-labs/kava/app"
|
||||
"github.com/kava-labs/kava/x/auction"
|
||||
)
|
||||
|
||||
var testTime = time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
var testAuction = auction.NewCollateralAuction(
|
||||
"seller",
|
||||
c("lotdenom", 10),
|
||||
testTime,
|
||||
c("biddenom", 1000),
|
||||
auction.WeightedAddresses{},
|
||||
c("debt", 1000),
|
||||
).WithID(3).(auction.GenesisAuction)
|
||||
|
||||
func TestInitGenesis(t *testing.T) {
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
// setup keepers
|
||||
tApp := app.NewTestApp()
|
||||
keeper := tApp.GetAuctionKeeper()
|
||||
ctx := tApp.NewContext(true, abci.Header{})
|
||||
// create genesis
|
||||
gs := auction.NewGenesisState(
|
||||
10,
|
||||
auction.DefaultParams(),
|
||||
auction.GenesisAuctions{testAuction},
|
||||
)
|
||||
|
||||
// run init
|
||||
require.NotPanics(t, func() {
|
||||
auction.InitGenesis(ctx, keeper, tApp.GetSupplyKeeper(), gs)
|
||||
})
|
||||
|
||||
// check state is as expected
|
||||
actualID, err := keeper.GetNextAuctionID(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, gs.NextAuctionID, actualID)
|
||||
|
||||
require.Equal(t, gs.Params, keeper.GetParams(ctx))
|
||||
|
||||
// TODO is there a nicer way of comparing state?
|
||||
sort.Slice(gs.Auctions, func(i, j int) bool {
|
||||
return gs.Auctions[i].GetID() > gs.Auctions[j].GetID()
|
||||
})
|
||||
i := 0
|
||||
keeper.IterateAuctions(ctx, func(a auction.Auction) bool {
|
||||
require.Equal(t, gs.Auctions[i], a)
|
||||
i++
|
||||
return false
|
||||
})
|
||||
})
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
// setup keepers
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{})
|
||||
|
||||
// create invalid genesis
|
||||
gs := auction.NewGenesisState(
|
||||
0, // next id < testAuction ID
|
||||
auction.DefaultParams(),
|
||||
auction.GenesisAuctions{testAuction},
|
||||
)
|
||||
|
||||
// check init fails
|
||||
require.Panics(t, func() {
|
||||
auction.InitGenesis(ctx, tApp.GetAuctionKeeper(), tApp.GetSupplyKeeper(), gs)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestExportGenesis(t *testing.T) {
|
||||
t.Run("default", func(t *testing.T) {
|
||||
// setup state
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{})
|
||||
tApp.InitializeFromGenesisStates()
|
||||
|
||||
// export
|
||||
gs := auction.ExportGenesis(ctx, tApp.GetAuctionKeeper())
|
||||
|
||||
// check state matches
|
||||
require.Equal(t, auction.DefaultGenesisState(), gs)
|
||||
})
|
||||
t.Run("one auction", func(t *testing.T) {
|
||||
// setup state
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{})
|
||||
tApp.InitializeFromGenesisStates()
|
||||
tApp.GetAuctionKeeper().SetAuction(ctx, testAuction)
|
||||
|
||||
// export
|
||||
gs := auction.ExportGenesis(ctx, tApp.GetAuctionKeeper())
|
||||
|
||||
// check state matches
|
||||
expectedGenesisState := auction.DefaultGenesisState()
|
||||
expectedGenesisState.Auctions = append(expectedGenesisState.Auctions, testAuction)
|
||||
require.Equal(t, expectedGenesisState, gs)
|
||||
})
|
||||
}
|
@ -4,11 +4,15 @@ import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/kava-labs/kava/x/auction/types"
|
||||
)
|
||||
|
||||
// NewHandler returns a function to handle all "auction" type messages.
|
||||
func NewHandler(keeper Keeper) sdk.Handler {
|
||||
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
|
||||
ctx = ctx.WithEventManager(sdk.NewEventManager())
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case MsgPlaceBid:
|
||||
return handleMsgPlaceBid(ctx, keeper, msg)
|
||||
@ -26,5 +30,15 @@ func handleMsgPlaceBid(ctx sdk.Context, keeper Keeper, msg MsgPlaceBid) sdk.Resu
|
||||
return err.Result()
|
||||
}
|
||||
|
||||
return sdk.Result{}
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
sdk.EventTypeMessage,
|
||||
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
|
||||
sdk.NewAttribute(sdk.AttributeKeySender, msg.Bidder.String()),
|
||||
),
|
||||
)
|
||||
|
||||
return sdk.Result{
|
||||
Events: ctx.EventManager().Events(),
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin
|
||||
seller,
|
||||
lot,
|
||||
bidDenom,
|
||||
ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration))
|
||||
types.DistantFuture)
|
||||
|
||||
err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot))
|
||||
if err != nil {
|
||||
@ -27,6 +27,16 @@ func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeAuctionStart,
|
||||
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
|
||||
sdk.NewAttribute(types.AttributeKeyAuctionType, auction.Name()),
|
||||
sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom),
|
||||
sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom),
|
||||
),
|
||||
)
|
||||
return auctionID, nil
|
||||
}
|
||||
|
||||
@ -37,7 +47,7 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in
|
||||
buyer,
|
||||
bid,
|
||||
initialLot,
|
||||
ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration),
|
||||
types.DistantFuture,
|
||||
debt)
|
||||
|
||||
// This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker.
|
||||
@ -55,6 +65,16 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeAuctionStart,
|
||||
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
|
||||
sdk.NewAttribute(types.AttributeKeyAuctionType, auction.Name()),
|
||||
sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom),
|
||||
sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom),
|
||||
),
|
||||
)
|
||||
return auctionID, nil
|
||||
}
|
||||
|
||||
@ -68,7 +88,7 @@ func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.C
|
||||
auction := types.NewCollateralAuction(
|
||||
seller,
|
||||
lot,
|
||||
ctx.BlockTime().Add(types.DefaultMaxAuctionDuration),
|
||||
types.DistantFuture,
|
||||
maxBid,
|
||||
weightedAddresses,
|
||||
debt)
|
||||
@ -86,6 +106,16 @@ func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.C
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeAuctionStart,
|
||||
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
|
||||
sdk.NewAttribute(types.AttributeKeyAuctionType, auction.Name()),
|
||||
sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom),
|
||||
sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom),
|
||||
),
|
||||
)
|
||||
return auctionID, nil
|
||||
}
|
||||
|
||||
@ -128,6 +158,7 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddres
|
||||
}
|
||||
|
||||
k.SetAuction(ctx, updatedAuction)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -142,7 +173,7 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder
|
||||
}
|
||||
|
||||
// New bidder pays back old bidder
|
||||
// Catch edge cases of a bidder replacing their own bid, and the amount being zero (sending zero coins produces meaningless send events).
|
||||
// Catch edge cases of a bidder replacing their own bid, or the amount being zero (sending zero coins produces meaningless send events).
|
||||
if !bidder.Equals(a.Bidder) && !a.Bid.IsZero() {
|
||||
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid))
|
||||
if err != nil {
|
||||
@ -166,7 +197,21 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder
|
||||
// Update Auction
|
||||
a.Bidder = bidder
|
||||
a.Bid = bid
|
||||
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout
|
||||
if !a.HasReceivedBids {
|
||||
a.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid
|
||||
}
|
||||
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout, up to MaxEndTime
|
||||
a.HasReceivedBids = true
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeAuctionBid,
|
||||
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
|
||||
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyBidAmount, a.Bid.Amount.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())),
|
||||
),
|
||||
)
|
||||
|
||||
return a, nil
|
||||
}
|
||||
@ -216,13 +261,26 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc
|
||||
return a, err
|
||||
}
|
||||
a.CorrespondingDebt = a.CorrespondingDebt.Sub(debtToReturn) // debtToReturn will always be ≤ a.CorrespondingDebt from the MinInt above
|
||||
// TODO optionally burn out debt and stable just returned to liquidator
|
||||
}
|
||||
|
||||
// Update Auction
|
||||
a.Bidder = bidder
|
||||
a.Bid = bid
|
||||
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout
|
||||
if !a.HasReceivedBids {
|
||||
a.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid
|
||||
}
|
||||
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout, up to MaxEndTime
|
||||
a.HasReceivedBids = true
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeAuctionBid,
|
||||
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
|
||||
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyBidAmount, a.Bid.Amount.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())),
|
||||
),
|
||||
)
|
||||
|
||||
return a, nil
|
||||
}
|
||||
@ -271,7 +329,21 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc
|
||||
// Update Auction
|
||||
a.Bidder = bidder
|
||||
a.Lot = lot
|
||||
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout
|
||||
if !a.HasReceivedBids {
|
||||
a.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid
|
||||
}
|
||||
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout, up to MaxEndTime
|
||||
a.HasReceivedBids = true
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeAuctionBid,
|
||||
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
|
||||
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyLotAmount, a.Lot.Amount.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())),
|
||||
),
|
||||
)
|
||||
|
||||
return a, nil
|
||||
}
|
||||
@ -312,13 +384,26 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac
|
||||
return a, err
|
||||
}
|
||||
a.CorrespondingDebt = a.CorrespondingDebt.Sub(debtToReturn) // debtToReturn will always be ≤ a.CorrespondingDebt from the MinInt above
|
||||
// TODO optionally burn out debt and stable just returned to liquidator
|
||||
}
|
||||
|
||||
// Update Auction
|
||||
a.Bidder = bidder
|
||||
a.Lot = lot
|
||||
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout
|
||||
if !a.HasReceivedBids {
|
||||
a.MaxEndTime = ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration) // set maximum ending time on receipt of first bid
|
||||
}
|
||||
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout, up to MaxEndTime
|
||||
a.HasReceivedBids = true
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeAuctionBid,
|
||||
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
|
||||
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyLotAmount, a.Lot.Amount.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())),
|
||||
),
|
||||
)
|
||||
|
||||
return a, nil
|
||||
}
|
||||
@ -354,6 +439,13 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error {
|
||||
}
|
||||
|
||||
k.DeleteAuction(ctx, auctionID)
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeAuctionClose,
|
||||
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
|
||||
),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -232,8 +232,9 @@ func TestStartSurplusAuction(t *testing.T) {
|
||||
Lot: tc.args.lot,
|
||||
Bidder: nil,
|
||||
Bid: c(tc.args.bidDenom, 0),
|
||||
EndTime: tc.blockTime.Add(types.DefaultMaxAuctionDuration),
|
||||
MaxEndTime: tc.blockTime.Add(types.DefaultMaxAuctionDuration),
|
||||
HasReceivedBids: false,
|
||||
EndTime: types.DistantFuture,
|
||||
MaxEndTime: types.DistantFuture,
|
||||
}})
|
||||
require.Equal(t, expectedAuction, actualAuc)
|
||||
} else {
|
||||
|
@ -2,18 +2,19 @@ package auction
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spf13/cobra"
|
||||
"fmt"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client/context"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/module"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/spf13/cobra"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"github.com/kava-labs/kava/x/auction/client/cli"
|
||||
"github.com/kava-labs/kava/x/auction/client/rest"
|
||||
"github.com/kava-labs/kava/x/auction/types"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -41,12 +42,12 @@ func (AppModuleBasic) DefaultGenesis() json.RawMessage {
|
||||
|
||||
// ValidateGenesis performs genesis state validation for the auction module.
|
||||
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
|
||||
var data GenesisState
|
||||
err := ModuleCdc.UnmarshalJSON(bz, &data)
|
||||
var gs GenesisState
|
||||
err := ModuleCdc.UnmarshalJSON(bz, &gs)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to unmarshal %s genesis state: %w", ModuleName, err)
|
||||
}
|
||||
return ValidateGenesis(data)
|
||||
return gs.Validate()
|
||||
}
|
||||
|
||||
// RegisterRESTRoutes registers the REST routes for the auction module.
|
||||
@ -67,14 +68,17 @@ func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
|
||||
// AppModule implements the sdk.AppModule interface.
|
||||
type AppModule struct {
|
||||
AppModuleBasic
|
||||
|
||||
keeper Keeper
|
||||
supplyKeeper types.SupplyKeeper
|
||||
}
|
||||
|
||||
// NewAppModule creates a new AppModule object
|
||||
func NewAppModule(keeper Keeper) AppModule {
|
||||
func NewAppModule(keeper Keeper, supplyKeeper types.SupplyKeeper) AppModule {
|
||||
return AppModule{
|
||||
AppModuleBasic: AppModuleBasic{},
|
||||
keeper: keeper,
|
||||
supplyKeeper: supplyKeeper,
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,7 +110,7 @@ func (am AppModule) NewQuerierHandler() sdk.Querier {
|
||||
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
|
||||
var genesisState GenesisState
|
||||
ModuleCdc.MustUnmarshalJSON(data, &genesisState)
|
||||
InitGenesis(ctx, am.keeper, genesisState)
|
||||
InitGenesis(ctx, am.keeper, am.supplyKeeper, genesisState)
|
||||
return []abci.ValidatorUpdate{}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,32 @@
|
||||
# Events
|
||||
|
||||
<!--
|
||||
TODO: Add events for auction_start, auction_end, auction_bid
|
||||
-->
|
||||
The `x/auction` module emits the following events:
|
||||
|
||||
## Triggered By Other Modules
|
||||
|
||||
| Type | Attribute Key | Attribute Value |
|
||||
| ------------- | ------------- | ------------------- |
|
||||
| auction_start | auction_id | {auction ID} |
|
||||
| auction_start | auction_type | {auction type} |
|
||||
| auction_start | lot_denom | {auction lot denom} |
|
||||
| auction_start | bid_denom | {auction bid denom} |
|
||||
|
||||
## Handlers
|
||||
|
||||
### MsgPlaceBid
|
||||
|
||||
| Type | Attribute Key | Attribute Value |
|
||||
| ----------- | ------------- | ------------------ |
|
||||
| auction_bid | auction_id | {auction ID} |
|
||||
| auction_bid | bidder | {latest bidder} |
|
||||
| auction_bid | bid_amount | {coin amount} |
|
||||
| auction_bid | lot_amount | {coin amount} |
|
||||
| auction_bid | end_time | {auction end time} |
|
||||
| message | module | auction |
|
||||
| message | sender | {sender address} |
|
||||
|
||||
## EndBlock
|
||||
|
||||
| Type | Attribute Key | Attribute Value |
|
||||
| ------------- | ------------- | --------------- |
|
||||
| auction_close | auction_id | {auction ID} |
|
||||
|
@ -8,6 +8,11 @@ import (
|
||||
"github.com/cosmos/cosmos-sdk/x/supply"
|
||||
)
|
||||
|
||||
// distantFuture is a very large time value to use as initial the ending time for auctions.
|
||||
// It is not set to the max time supported. This can cause problems with time comparisons, see https://stackoverflow.com/a/32620397.
|
||||
// Also amino panics when encoding times ≥ the start of year 10000.
|
||||
var DistantFuture time.Time = time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// Auction is an interface for handling common actions on auctions.
|
||||
type Auction interface {
|
||||
GetID() uint64
|
||||
@ -22,6 +27,7 @@ type BaseAuction struct {
|
||||
Lot sdk.Coin // Coins that will paid out by Initiator to the winning bidder.
|
||||
Bidder sdk.AccAddress // Latest bidder. Receiver of Lot.
|
||||
Bid sdk.Coin // Coins paid into the auction the bidder.
|
||||
HasReceivedBids bool // Whether the auction has received any bids or not.
|
||||
EndTime time.Time // Current auction closing time. Triggers at the end of the block with time ≥ EndTime.
|
||||
MaxEndTime time.Time // Maximum closing time. Auctions can close before this but never after.
|
||||
}
|
||||
@ -32,6 +38,13 @@ func (a BaseAuction) GetID() uint64 { return a.ID }
|
||||
// GetEndTime is a getter for auction end time.
|
||||
func (a BaseAuction) GetEndTime() time.Time { return a.EndTime }
|
||||
|
||||
func (a BaseAuction) Validate() error {
|
||||
if a.EndTime.After(a.MaxEndTime) {
|
||||
return fmt.Errorf("MaxEndTime < EndTime (%s < %s)", a.MaxEndTime, a.EndTime)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a BaseAuction) String() string {
|
||||
return fmt.Sprintf(`Auction %d:
|
||||
Initiator: %s
|
||||
@ -55,6 +68,16 @@ type SurplusAuction struct {
|
||||
// WithID returns an auction with the ID set.
|
||||
func (a SurplusAuction) WithID(id uint64) Auction { a.ID = id; return a }
|
||||
|
||||
// Name returns a name for this auction type. Used to identify auctions in event attributes.
|
||||
func (a SurplusAuction) Name() string { return "surplus" }
|
||||
|
||||
// GetModuleAccountCoins returns the total number of coins held in the module account for this auction.
|
||||
// It is used in genesis initialize the module account correctly.
|
||||
func (a SurplusAuction) GetModuleAccountCoins() sdk.Coins {
|
||||
// a.Bid is paid out on bids, so is never stored in the module account
|
||||
return sdk.NewCoins(a.Lot)
|
||||
}
|
||||
|
||||
// NewSurplusAuction returns a new surplus auction.
|
||||
func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime time.Time) SurplusAuction {
|
||||
auction := SurplusAuction{BaseAuction{
|
||||
@ -63,6 +86,7 @@ func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime tim
|
||||
Lot: lot,
|
||||
Bidder: nil,
|
||||
Bid: sdk.NewInt64Coin(bidDenom, 0),
|
||||
HasReceivedBids: false, // new auctions don't have any bids
|
||||
EndTime: endTime,
|
||||
MaxEndTime: endTime,
|
||||
}}
|
||||
@ -79,8 +103,19 @@ type DebtAuction struct {
|
||||
// WithID returns an auction with the ID set.
|
||||
func (a DebtAuction) WithID(id uint64) Auction { a.ID = id; return a }
|
||||
|
||||
// Name returns a name for this auction type. Used to identify auctions in event attributes.
|
||||
func (a DebtAuction) Name() string { return "debt" }
|
||||
|
||||
// GetModuleAccountCoins returns the total number of coins held in the module account for this auction.
|
||||
// It is used in genesis initialize the module account correctly.
|
||||
func (a DebtAuction) GetModuleAccountCoins() sdk.Coins {
|
||||
// a.Lot is minted at auction close, so is never stored in the module account
|
||||
// a.Bid is paid out on bids, so is never stored in the module account
|
||||
return sdk.NewCoins(a.CorrespondingDebt)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// Setting to the module account address bypasses calling supply.SendCoinsFromModuleToModule, instead calls SendCoinsFromModuleToAccount.
|
||||
// This isn't a problem currently, but if additional logic/validation was added for sending to coins to Module Accounts, it would be bypassed.
|
||||
@ -91,8 +126,9 @@ func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, E
|
||||
Lot: initialLot,
|
||||
Bidder: supply.NewModuleAddress(buyerModAccName), // send proceeds from the first bid to the buyer.
|
||||
Bid: bid, // amount that the buyer is buying - doesn't change over course of auction
|
||||
EndTime: EndTime,
|
||||
MaxEndTime: EndTime},
|
||||
HasReceivedBids: false, // new auctions don't have any bids
|
||||
EndTime: endTime,
|
||||
MaxEndTime: endTime},
|
||||
CorrespondingDebt: debt,
|
||||
}
|
||||
return auction
|
||||
@ -113,6 +149,16 @@ type CollateralAuction struct {
|
||||
// WithID returns an auction with the ID set.
|
||||
func (a CollateralAuction) WithID(id uint64) Auction { a.ID = id; return a }
|
||||
|
||||
// Name returns a name for this auction type. Used to identify auctions in event attributes.
|
||||
func (a CollateralAuction) Name() string { return "collateral" }
|
||||
|
||||
// GetModuleAccountCoins returns the total number of coins held in the module account for this auction.
|
||||
// It is used in genesis initialize the module account correctly.
|
||||
func (a CollateralAuction) GetModuleAccountCoins() sdk.Coins {
|
||||
// a.Bid is paid out on bids, so is never stored in the module account
|
||||
return sdk.NewCoins(a.Lot).Add(sdk.NewCoins(a.CorrespondingDebt))
|
||||
}
|
||||
|
||||
// IsReversePhase returns whether the auction has switched over to reverse phase or not.
|
||||
// Auction initially start in forward phase.
|
||||
func (a CollateralAuction) IsReversePhase() bool {
|
||||
@ -136,7 +182,7 @@ func (a CollateralAuction) String() string {
|
||||
}
|
||||
|
||||
// NewCollateralAuction returns a new collateral auction.
|
||||
func NewCollateralAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses, debt sdk.Coin) CollateralAuction {
|
||||
func NewCollateralAuction(seller string, lot sdk.Coin, endTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses, debt sdk.Coin) CollateralAuction {
|
||||
auction := CollateralAuction{
|
||||
BaseAuction: BaseAuction{
|
||||
// no ID
|
||||
@ -144,8 +190,9 @@ func NewCollateralAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid
|
||||
Lot: lot,
|
||||
Bidder: nil,
|
||||
Bid: sdk.NewInt64Coin(maxBid.Denom, 0),
|
||||
EndTime: EndTime,
|
||||
MaxEndTime: EndTime},
|
||||
HasReceivedBids: false, // new auctions don't have any bids
|
||||
EndTime: endTime,
|
||||
MaxEndTime: endTime},
|
||||
CorrespondingDebt: debt,
|
||||
MaxBid: maxBid,
|
||||
LotReturns: lotReturns,
|
||||
|
@ -15,6 +15,7 @@ func init() {
|
||||
func RegisterCodec(cdc *codec.Codec) {
|
||||
cdc.RegisterConcrete(MsgPlaceBid{}, "auction/MsgPlaceBid", nil)
|
||||
|
||||
cdc.RegisterInterface((*GenesisAuction)(nil), nil)
|
||||
cdc.RegisterInterface((*Auction)(nil), nil)
|
||||
cdc.RegisterConcrete(SurplusAuction{}, "auction/SurplusAuction", nil)
|
||||
cdc.RegisterConcrete(DebtAuction{}, "auction/DebtAuction", nil)
|
||||
|
17
x/auction/types/events.go
Normal file
17
x/auction/types/events.go
Normal file
@ -0,0 +1,17 @@
|
||||
package types
|
||||
|
||||
const (
|
||||
EventTypeAuctionStart = "auction_start"
|
||||
EventTypeAuctionBid = "auction_bid"
|
||||
EventTypeAuctionClose = "auction_close"
|
||||
|
||||
AttributeValueCategory = ModuleName
|
||||
AttributeKeyAuctionID = "auction_id"
|
||||
AttributeKeyAuctionType = "auction_type"
|
||||
AttributeKeyBidder = "bidder"
|
||||
AttributeKeyBidDenom = "bid_denom"
|
||||
AttributeKeyLotDenom = "lot_denom"
|
||||
AttributeKeyBidAmount = "bid_amount"
|
||||
AttributeKeyLotAmount = "lot_amount"
|
||||
AttributeKeyEndTime = "end_time"
|
||||
)
|
@ -2,20 +2,30 @@ package types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// Auctions is a slice of auctions.
|
||||
type Auctions []Auction
|
||||
// GenesisAuction is an interface that extends the auction interface to add functionality needed for initializing auctions from genesis.
|
||||
type GenesisAuction interface {
|
||||
Auction
|
||||
GetModuleAccountCoins() sdk.Coins
|
||||
Validate() error
|
||||
}
|
||||
|
||||
// GenesisAuctions is a slice of genesis auctions.
|
||||
type GenesisAuctions []GenesisAuction
|
||||
|
||||
// GenesisState is auction state that must be provided at chain genesis.
|
||||
type GenesisState struct {
|
||||
NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"`
|
||||
Params Params `json:"auction_params" yaml:"auction_params"`
|
||||
Auctions Auctions `json:"genesis_auctions" yaml:"genesis_auctions"`
|
||||
Auctions GenesisAuctions `json:"genesis_auctions" yaml:"genesis_auctions"`
|
||||
}
|
||||
|
||||
// NewGenesisState returns a new genesis state object for auctions module.
|
||||
func NewGenesisState(nextID uint64, ap Params, ga Auctions) GenesisState {
|
||||
func NewGenesisState(nextID uint64, ap Params, ga GenesisAuctions) GenesisState {
|
||||
return GenesisState{
|
||||
NextAuctionID: nextID,
|
||||
Params: ap,
|
||||
@ -25,25 +35,42 @@ func NewGenesisState(nextID uint64, ap Params, ga Auctions) GenesisState {
|
||||
|
||||
// DefaultGenesisState returns the default genesis state for auction module.
|
||||
func DefaultGenesisState() GenesisState {
|
||||
return NewGenesisState(0, DefaultParams(), Auctions{})
|
||||
return NewGenesisState(0, DefaultParams(), GenesisAuctions{})
|
||||
}
|
||||
|
||||
// Equal checks whether two GenesisState structs are equivalent.
|
||||
func (data GenesisState) Equal(data2 GenesisState) bool {
|
||||
b1 := ModuleCdc.MustMarshalBinaryBare(data)
|
||||
b2 := ModuleCdc.MustMarshalBinaryBare(data2)
|
||||
func (gs GenesisState) Equal(gs2 GenesisState) bool {
|
||||
b1 := ModuleCdc.MustMarshalBinaryBare(gs)
|
||||
b2 := ModuleCdc.MustMarshalBinaryBare(gs2)
|
||||
return bytes.Equal(b1, b2)
|
||||
}
|
||||
|
||||
// IsEmpty returns true if a GenesisState is empty.
|
||||
func (data GenesisState) IsEmpty() bool {
|
||||
return data.Equal(GenesisState{})
|
||||
func (gs GenesisState) IsEmpty() bool {
|
||||
return gs.Equal(GenesisState{})
|
||||
}
|
||||
|
||||
// ValidateGenesis validates genesis inputs. It returns error if validation of any input fails.
|
||||
func ValidateGenesis(data GenesisState) error {
|
||||
if err := data.Params.Validate(); err != nil {
|
||||
func (gs GenesisState) Validate() error {
|
||||
if err := gs.Params.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ids := map[uint64]bool{}
|
||||
for _, a := range gs.Auctions {
|
||||
|
||||
if err := a.Validate(); err != nil {
|
||||
return fmt.Errorf("found invalid auction: %w", err)
|
||||
}
|
||||
|
||||
if ids[a.GetID()] {
|
||||
return fmt.Errorf("found duplicate auction ID (%d)", a.GetID())
|
||||
}
|
||||
ids[a.GetID()] = true
|
||||
|
||||
if a.GetID() >= gs.NextAuctionID {
|
||||
return fmt.Errorf("found auction ID >= the nextAuctionID (%d >= %d)", a.GetID(), gs.NextAuctionID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
46
x/auction/types/genesis_test.go
Normal file
46
x/auction/types/genesis_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var testCoin = sdk.NewInt64Coin("test", 20)
|
||||
|
||||
func TestGenesisState_Validate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
nextID uint64
|
||||
auctions GenesisAuctions
|
||||
expectPass bool
|
||||
}{
|
||||
{"default", DefaultGenesisState().NextAuctionID, DefaultGenesisState().Auctions, true},
|
||||
{"invalid next ID", 54, GenesisAuctions{SurplusAuction{BaseAuction{ID: 105}}}, false},
|
||||
{
|
||||
"repeated ID",
|
||||
1000,
|
||||
GenesisAuctions{
|
||||
SurplusAuction{BaseAuction{ID: 105}},
|
||||
DebtAuction{BaseAuction{ID: 105}, testCoin},
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
gs := NewGenesisState(tc.nextID, DefaultParams(), tc.auctions)
|
||||
|
||||
err := gs.Validate()
|
||||
|
||||
if tc.expectPass {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user