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), slashing.NewAppModule(app.slashingKeeper, app.stakingKeeper),
staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper), staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper),
validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper), validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper),
auction.NewAppModule(app.auctionKeeper), auction.NewAppModule(app.auctionKeeper, app.supplyKeeper),
cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper), cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper),
pricefeed.NewAppModule(app.pricefeedKeeper), pricefeed.NewAppModule(app.pricefeedKeeper),
) )

View File

@ -29,7 +29,6 @@ var (
RegisterCodec = types.RegisterCodec RegisterCodec = types.RegisterCodec
NewGenesisState = types.NewGenesisState NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState DefaultGenesisState = types.DefaultGenesisState
ValidateGenesis = types.ValidateGenesis
GetAuctionKey = types.GetAuctionKey GetAuctionKey = types.GetAuctionKey
GetAuctionByTimeKey = types.GetAuctionByTimeKey GetAuctionByTimeKey = types.GetAuctionByTimeKey
Uint64FromBytes = types.Uint64FromBytes Uint64FromBytes = types.Uint64FromBytes
@ -58,7 +57,8 @@ type (
CollateralAuction = types.CollateralAuction CollateralAuction = types.CollateralAuction
WeightedAddresses = types.WeightedAddresses WeightedAddresses = types.WeightedAddresses
SupplyKeeper = types.SupplyKeeper SupplyKeeper = types.SupplyKeeper
Auctions = types.Auctions GenesisAuctions = types.GenesisAuctions
GenesisAuction = types.GenesisAuction
GenesisState = types.GenesisState GenesisState = types.GenesisState
MsgPlaceBid = types.MsgPlaceBid MsgPlaceBid = types.MsgPlaceBid
Params = types.Params Params = types.Params

View File

@ -1,17 +1,39 @@
package auction package auction
import ( import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/auction/types"
) )
// InitGenesis initializes the store state from genesis data. // InitGenesis initializes the store state from a genesis state.
func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper types.SupplyKeeper, gs GenesisState) {
keeper.SetNextAuctionID(ctx, data.NextAuctionID) 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) 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) 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 { 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 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" "fmt"
sdk "github.com/cosmos/cosmos-sdk/types" 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. // NewHandler returns a function to handle all "auction" type messages.
func NewHandler(keeper Keeper) sdk.Handler { func NewHandler(keeper Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
ctx = ctx.WithEventManager(sdk.NewEventManager())
switch msg := msg.(type) { switch msg := msg.(type) {
case MsgPlaceBid: case MsgPlaceBid:
return handleMsgPlaceBid(ctx, keeper, msg) return handleMsgPlaceBid(ctx, keeper, msg)
@ -26,5 +30,15 @@ func handleMsgPlaceBid(ctx sdk.Context, keeper Keeper, msg MsgPlaceBid) sdk.Resu
return err.Result() 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, seller,
lot, lot,
bidDenom, bidDenom,
ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration)) types.DistantFuture)
err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot))
if err != nil { if err != nil {
@ -27,6 +27,16 @@ func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin
if err != nil { if err != nil {
return 0, err 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 return auctionID, nil
} }
@ -37,7 +47,7 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in
buyer, buyer,
bid, bid,
initialLot, initialLot,
ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration), types.DistantFuture,
debt) debt)
// This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker. // 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 { if err != nil {
return 0, err 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 return auctionID, nil
} }
@ -68,7 +88,7 @@ func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.C
auction := types.NewCollateralAuction( auction := types.NewCollateralAuction(
seller, seller,
lot, lot,
ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), types.DistantFuture,
maxBid, maxBid,
weightedAddresses, weightedAddresses,
debt) debt)
@ -86,6 +106,16 @@ func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.C
if err != nil { if err != nil {
return 0, err 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 return auctionID, nil
} }
@ -128,6 +158,7 @@ func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddres
} }
k.SetAuction(ctx, updatedAuction) k.SetAuction(ctx, updatedAuction)
return nil return nil
} }
@ -142,7 +173,7 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder
} }
// New bidder pays back old 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() { if !bidder.Equals(a.Bidder) && !a.Bid.IsZero() {
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid)) err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid))
if err != nil { if err != nil {
@ -166,7 +197,21 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder
// Update Auction // Update Auction
a.Bidder = bidder a.Bidder = bidder
a.Bid = bid 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 return a, nil
} }
@ -216,13 +261,26 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc
return a, err return a, err
} }
a.CorrespondingDebt = a.CorrespondingDebt.Sub(debtToReturn) // debtToReturn will always be ≤ a.CorrespondingDebt from the MinInt above 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 // Update Auction
a.Bidder = bidder a.Bidder = bidder
a.Bid = bid 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 return a, nil
} }
@ -271,7 +329,21 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc
// Update Auction // Update Auction
a.Bidder = bidder a.Bidder = bidder
a.Lot = lot 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 return a, nil
} }
@ -312,13 +384,26 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac
return a, err return a, err
} }
a.CorrespondingDebt = a.CorrespondingDebt.Sub(debtToReturn) // debtToReturn will always be ≤ a.CorrespondingDebt from the MinInt above 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 // Update Auction
a.Bidder = bidder a.Bidder = bidder
a.Lot = lot 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 return a, nil
} }
@ -354,6 +439,13 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error {
} }
k.DeleteAuction(ctx, auctionID) k.DeleteAuction(ctx, auctionID)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeAuctionClose,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
),
)
return nil return nil
} }

View File

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

View File

@ -2,18 +2,19 @@ package auction
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/gorilla/mux"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/cosmos-sdk/types/module"
"github.com/gorilla/mux"
"github.com/spf13/cobra"
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/auction/client/cli" "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/client/rest"
"github.com/kava-labs/kava/x/auction/types"
) )
var ( var (
@ -41,12 +42,12 @@ func (AppModuleBasic) DefaultGenesis() json.RawMessage {
// ValidateGenesis performs genesis state validation for the auction module. // ValidateGenesis performs genesis state validation for the auction module.
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var data GenesisState var gs GenesisState
err := ModuleCdc.UnmarshalJSON(bz, &data) err := ModuleCdc.UnmarshalJSON(bz, &gs)
if err != nil { 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. // 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. // AppModule implements the sdk.AppModule interface.
type AppModule struct { type AppModule struct {
AppModuleBasic AppModuleBasic
keeper Keeper
keeper Keeper
supplyKeeper types.SupplyKeeper
} }
// NewAppModule creates a new AppModule object // NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper) AppModule { func NewAppModule(keeper Keeper, supplyKeeper types.SupplyKeeper) AppModule {
return AppModule{ return AppModule{
AppModuleBasic: AppModuleBasic{}, AppModuleBasic: AppModuleBasic{},
keeper: keeper, 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 { func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState GenesisState var genesisState GenesisState
ModuleCdc.MustUnmarshalJSON(data, &genesisState) ModuleCdc.MustUnmarshalJSON(data, &genesisState)
InitGenesis(ctx, am.keeper, genesisState) InitGenesis(ctx, am.keeper, am.supplyKeeper, genesisState)
return []abci.ValidatorUpdate{} return []abci.ValidatorUpdate{}
} }

View File

@ -1,5 +1,32 @@
# Events # Events
<!-- The `x/auction` module emits the following events:
TODO: Add events for auction_start, auction_end, auction_bid
--> ## 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" "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. // Auction is an interface for handling common actions on auctions.
type Auction interface { type Auction interface {
GetID() uint64 GetID() uint64
@ -17,13 +22,14 @@ type Auction interface {
// BaseAuction is a common type shared by all Auctions. // BaseAuction is a common type shared by all Auctions.
type BaseAuction struct { type BaseAuction struct {
ID uint64 ID uint64
Initiator string // Module name that starts the auction. Pays out Lot. 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. Lot sdk.Coin // Coins that will paid out by Initiator to the winning bidder.
Bidder sdk.AccAddress // Latest bidder. Receiver of Lot. Bidder sdk.AccAddress // Latest bidder. Receiver of Lot.
Bid sdk.Coin // Coins paid into the auction the bidder. 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. HasReceivedBids bool // Whether the auction has received any bids or not.
MaxEndTime time.Time // Maximum closing time. Auctions can close before this but never after. 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. // 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. // GetEndTime is a getter for auction end time.
func (a BaseAuction) GetEndTime() time.Time { return a.EndTime } 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 { func (a BaseAuction) String() string {
return fmt.Sprintf(`Auction %d: return fmt.Sprintf(`Auction %d:
Initiator: %s Initiator: %s
@ -55,16 +68,27 @@ type SurplusAuction struct {
// WithID returns an auction with the ID set. // WithID returns an auction with the ID set.
func (a SurplusAuction) WithID(id uint64) Auction { a.ID = id; return a } 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. // NewSurplusAuction returns a new surplus auction.
func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime time.Time) SurplusAuction { func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime time.Time) SurplusAuction {
auction := SurplusAuction{BaseAuction{ auction := SurplusAuction{BaseAuction{
// no ID // no ID
Initiator: seller, Initiator: seller,
Lot: lot, Lot: lot,
Bidder: nil, Bidder: nil,
Bid: sdk.NewInt64Coin(bidDenom, 0), Bid: sdk.NewInt64Coin(bidDenom, 0),
EndTime: endTime, HasReceivedBids: false, // new auctions don't have any bids
MaxEndTime: endTime, EndTime: endTime,
MaxEndTime: endTime,
}} }}
return auction return auction
} }
@ -79,20 +103,32 @@ type DebtAuction struct {
// WithID returns an auction with the ID set. // WithID returns an auction with the ID set.
func (a DebtAuction) WithID(id uint64) Auction { a.ID = id; return a } 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. // 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)
// Setting to the module account address bypasses calling supply.SendCoinsFromModuleToModule, instead calls SendCoinsFromModuleToAccount. // 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. // 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{ auction := DebtAuction{
BaseAuction: BaseAuction{ BaseAuction: BaseAuction{
// no ID // no ID
Initiator: buyerModAccName, Initiator: buyerModAccName,
Lot: initialLot, Lot: initialLot,
Bidder: supply.NewModuleAddress(buyerModAccName), // send proceeds from the first bid to the buyer. 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 Bid: bid, // amount that the buyer is buying - doesn't change over course of auction
EndTime: EndTime, HasReceivedBids: false, // new auctions don't have any bids
MaxEndTime: EndTime}, EndTime: endTime,
MaxEndTime: endTime},
CorrespondingDebt: debt, CorrespondingDebt: debt,
} }
return auction return auction
@ -113,6 +149,16 @@ type CollateralAuction struct {
// WithID returns an auction with the ID set. // WithID returns an auction with the ID set.
func (a CollateralAuction) WithID(id uint64) Auction { a.ID = id; return a } 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. // IsReversePhase returns whether the auction has switched over to reverse phase or not.
// Auction initially start in forward phase. // Auction initially start in forward phase.
func (a CollateralAuction) IsReversePhase() bool { func (a CollateralAuction) IsReversePhase() bool {
@ -136,16 +182,17 @@ func (a CollateralAuction) String() string {
} }
// NewCollateralAuction returns a new collateral auction. // 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{ auction := CollateralAuction{
BaseAuction: BaseAuction{ BaseAuction: BaseAuction{
// no ID // no ID
Initiator: seller, Initiator: seller,
Lot: lot, Lot: lot,
Bidder: nil, Bidder: nil,
Bid: sdk.NewInt64Coin(maxBid.Denom, 0), Bid: sdk.NewInt64Coin(maxBid.Denom, 0),
EndTime: EndTime, HasReceivedBids: false, // new auctions don't have any bids
MaxEndTime: EndTime}, EndTime: endTime,
MaxEndTime: endTime},
CorrespondingDebt: debt, CorrespondingDebt: debt,
MaxBid: maxBid, MaxBid: maxBid,
LotReturns: lotReturns, LotReturns: lotReturns,

View File

@ -15,6 +15,7 @@ func init() {
func RegisterCodec(cdc *codec.Codec) { func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgPlaceBid{}, "auction/MsgPlaceBid", nil) cdc.RegisterConcrete(MsgPlaceBid{}, "auction/MsgPlaceBid", nil)
cdc.RegisterInterface((*GenesisAuction)(nil), nil)
cdc.RegisterInterface((*Auction)(nil), nil) cdc.RegisterInterface((*Auction)(nil), nil)
cdc.RegisterConcrete(SurplusAuction{}, "auction/SurplusAuction", nil) cdc.RegisterConcrete(SurplusAuction{}, "auction/SurplusAuction", nil)
cdc.RegisterConcrete(DebtAuction{}, "auction/DebtAuction", 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 ( import (
"bytes" "bytes"
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
) )
// Auctions is a slice of auctions. // GenesisAuction is an interface that extends the auction interface to add functionality needed for initializing auctions from genesis.
type Auctions []Auction 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. // GenesisState is auction state that must be provided at chain genesis.
type GenesisState struct { type GenesisState struct {
NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"`
Params Params `json:"auction_params" yaml:"auction_params"` 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. // 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{ return GenesisState{
NextAuctionID: nextID, NextAuctionID: nextID,
Params: ap, Params: ap,
@ -25,25 +35,42 @@ func NewGenesisState(nextID uint64, ap Params, ga Auctions) GenesisState {
// DefaultGenesisState returns the default genesis state for auction module. // DefaultGenesisState returns the default genesis state for auction module.
func DefaultGenesisState() GenesisState { func DefaultGenesisState() GenesisState {
return NewGenesisState(0, DefaultParams(), Auctions{}) return NewGenesisState(0, DefaultParams(), GenesisAuctions{})
} }
// Equal checks whether two GenesisState structs are equivalent. // Equal checks whether two GenesisState structs are equivalent.
func (data GenesisState) Equal(data2 GenesisState) bool { func (gs GenesisState) Equal(gs2 GenesisState) bool {
b1 := ModuleCdc.MustMarshalBinaryBare(data) b1 := ModuleCdc.MustMarshalBinaryBare(gs)
b2 := ModuleCdc.MustMarshalBinaryBare(data2) b2 := ModuleCdc.MustMarshalBinaryBare(gs2)
return bytes.Equal(b1, b2) return bytes.Equal(b1, b2)
} }
// IsEmpty returns true if a GenesisState is empty. // IsEmpty returns true if a GenesisState is empty.
func (data GenesisState) IsEmpty() bool { func (gs GenesisState) IsEmpty() bool {
return data.Equal(GenesisState{}) return gs.Equal(GenesisState{})
} }
// ValidateGenesis validates genesis inputs. It returns error if validation of any input fails. // ValidateGenesis validates genesis inputs. It returns error if validation of any input fails.
func ValidateGenesis(data GenesisState) error { func (gs GenesisState) Validate() error {
if err := data.Params.Validate(); err != nil { if err := gs.Params.Validate(); err != nil {
return err 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 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)
}
})
}
}