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:
Ruaridh 2020-01-14 15:00:37 +01:00 committed by Kevin Davis
parent aa6dfab6fd
commit 22e168d06a
14 changed files with 493 additions and 82 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -227,13 +227,14 @@ func TestStartSurplusAuction(t *testing.T) {
// check auction in store and is correct
require.True(t, found)
expectedAuction := types.Auction(types.SurplusAuction{BaseAuction: types.BaseAuction{
ID: 0,
Initiator: tc.args.seller,
Lot: tc.args.lot,
Bidder: nil,
Bid: c(tc.args.bidDenom, 0),
EndTime: tc.blockTime.Add(types.DefaultMaxAuctionDuration),
MaxEndTime: tc.blockTime.Add(types.DefaultMaxAuctionDuration),
ID: 0,
Initiator: tc.args.seller,
Lot: tc.args.lot,
Bidder: nil,
Bid: c(tc.args.bidDenom, 0),
HasReceivedBids: false,
EndTime: types.DistantFuture,
MaxEndTime: types.DistantFuture,
}})
require.Equal(t, expectedAuction, actualAuc)
} else {

View File

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

View File

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

View File

@ -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
@ -17,13 +22,14 @@ type Auction interface {
// BaseAuction is a common type shared by all Auctions.
type BaseAuction struct {
ID uint64
Initiator string // Module name that starts the auction. Pays out Lot.
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.
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.
ID uint64
Initiator string // Module name that starts the auction. Pays out Lot.
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.
}
// GetID is a getter for auction ID.
@ -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,16 +68,27 @@ 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{
// no ID
Initiator: seller,
Lot: lot,
Bidder: nil,
Bid: sdk.NewInt64Coin(bidDenom, 0),
EndTime: endTime,
MaxEndTime: endTime,
Initiator: seller,
Lot: lot,
Bidder: nil,
Bid: sdk.NewInt64Coin(bidDenom, 0),
HasReceivedBids: false, // new auctions don't have any bids
EndTime: endTime,
MaxEndTime: endTime,
}}
return auction
}
@ -79,20 +103,32 @@ 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.
auction := DebtAuction{
BaseAuction: BaseAuction{
// no ID
Initiator: buyerModAccName,
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},
Initiator: buyerModAccName,
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
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,16 +182,17 @@ 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
Initiator: seller,
Lot: lot,
Bidder: nil,
Bid: sdk.NewInt64Coin(maxBid.Denom, 0),
EndTime: EndTime,
MaxEndTime: EndTime},
Initiator: seller,
Lot: lot,
Bidder: nil,
Bid: sdk.NewInt64Coin(maxBid.Denom, 0),
HasReceivedBids: false, // new auctions don't have any bids
EndTime: endTime,
MaxEndTime: endTime},
CorrespondingDebt: debt,
MaxBid: maxBid,
LotReturns: lotReturns,

View File

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

View File

@ -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"`
NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"`
Params Params `json:"auction_params" yaml:"auction_params"`
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
}

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