Update Auction Module (#276)

* rough auction type refactor

* replace endTime type

* split keeper file up

* update store methods

* move store methods to keeper.go

* move nextAuctionID from params to genState

* simplify auction type to not use pointers

* add basic auction tests

* update endblocker test

* add payout to depositors feature

* add more tests

* move index updates to Get/Set for more safety

* remove slightly unecessary ID type

* remove unused message types

* feat: add spec, update redundant type names

* stop sending zero coins

* use only one coins field in MsgPlaceBid

* remove uncessary Auction interface methods

* give auction types more accurate names

* remove vuepress comments from spec

* minor spec updates

* update doc comments

* add params validation

* code cleanup, address review comments

* resolve minor TODOs

* sync spec with code

Co-authored-by: Kevin Davis <karzak@users.noreply.github.com>
This commit is contained in:
Ruaridh 2020-01-12 16:12:22 +01:00 committed by Kevin Davis
parent c5db0ff680
commit e1c11d411a
41 changed files with 1511 additions and 1326 deletions

View File

@ -61,7 +61,7 @@ var (
supply.AppModuleBasic{},
auction.AppModuleBasic{},
cdp.AppModuleBasic{},
liquidator.AppModuleBasic{},
//liquidator.AppModuleBasic{},
pricefeed.AppModuleBasic{},
)
@ -74,6 +74,8 @@ var (
staking.NotBondedPoolName: {supply.Burner, supply.Staking},
gov.ModuleName: {supply.Burner},
validatorvesting.ModuleName: {supply.Burner},
auction.ModuleName: nil,
liquidator.ModuleName: {supply.Minter, supply.Burner},
}
)
@ -151,7 +153,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
crisisSubspace := app.paramsKeeper.Subspace(crisis.DefaultParamspace)
auctionSubspace := app.paramsKeeper.Subspace(auction.DefaultParamspace)
cdpSubspace := app.paramsKeeper.Subspace(cdp.DefaultParamspace)
liquidatorSubspace := app.paramsKeeper.Subspace(liquidator.DefaultParamspace)
//liquidatorSubspace := app.paramsKeeper.Subspace(liquidator.DefaultParamspace)
pricefeedSubspace := app.paramsKeeper.Subspace(pricefeed.DefaultParamspace)
// add keepers
@ -237,16 +239,16 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
app.bankKeeper)
app.auctionKeeper = auction.NewKeeper(
app.cdc,
app.cdpKeeper, // CDP keeper standing in for bank
keys[auction.StoreKey],
app.supplyKeeper,
auctionSubspace)
app.liquidatorKeeper = liquidator.NewKeeper(
app.cdc,
keys[liquidator.StoreKey],
liquidatorSubspace,
app.cdpKeeper,
app.auctionKeeper,
app.cdpKeeper) // CDP keeper standing in for bank
// app.liquidatorKeeper = liquidator.NewKeeper(
// app.cdc,
// keys[liquidator.StoreKey],
// liquidatorSubspace,
// app.cdpKeeper,
// app.auctionKeeper,
// app.cdpKeeper) // CDP keeper standing in for bank
// register the staking hooks
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
@ -269,7 +271,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper),
auction.NewAppModule(app.auctionKeeper),
cdp.NewAppModule(app.cdpKeeper, app.pricefeedKeeper),
liquidator.NewAppModule(app.liquidatorKeeper),
//liquidator.NewAppModule(app.liquidatorKeeper),
pricefeed.NewAppModule(app.pricefeedKeeper),
)
@ -289,7 +291,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
auth.ModuleName, validatorvesting.ModuleName, distr.ModuleName,
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
gov.ModuleName, mint.ModuleName, supply.ModuleName, crisis.ModuleName, genutil.ModuleName,
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, liquidator.ModuleName, // TODO is this order ok?
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, //liquidator.ModuleName, // TODO is this order ok?
)
app.mm.RegisterInvariants(&app.crisisKeeper)

View File

@ -97,8 +97,9 @@ func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) T
}
func (tApp TestApp) CheckBalance(t *testing.T, ctx sdk.Context, owner sdk.AccAddress, expectedCoins sdk.Coins) {
actualCoins := tApp.GetAccountKeeper().GetAccount(ctx, owner).GetCoins()
require.Equal(t, expectedCoins, actualCoins)
acc := tApp.GetAccountKeeper().GetAccount(ctx, owner)
require.NotNilf(t, acc, "account with address '%s' doesn't exist", owner)
require.Equal(t, expectedCoins, acc.GetCoins())
}
// Create a new auth genesis state from some addresses and coins. The state is returned marshalled into a map.

View File

@ -6,18 +6,8 @@ import (
// EndBlocker runs at the end of every block.
func EndBlocker(ctx sdk.Context, k Keeper) {
// get an iterator of expired auctions
expiredAuctions := k.GetQueueIterator(ctx, EndTime(ctx.BlockHeight()))
defer expiredAuctions.Close()
// loop through and close them - distribute funds, delete from store (and queue)
for ; expiredAuctions.Valid(); expiredAuctions.Next() {
auctionID := k.DecodeAuctionID(ctx, expiredAuctions.Value())
err := k.CloseAuction(ctx, auctionID)
if err != nil {
panic(err) // TODO how should errors be handled here?
}
err := k.CloseExpiredAuctions(ctx)
if err != nil {
panic(err)
}
}

View File

@ -4,40 +4,53 @@ import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/supply"
"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"
"github.com/kava-labs/kava/x/liquidator"
)
func TestKeeper_EndBlocker(t *testing.T) {
// Setup
_, addrs := app.GeneratePrivKeyAddressPairs(1)
seller := addrs[0]
_, addrs := app.GeneratePrivKeyAddressPairs(2)
buyer := addrs[0]
returnAddrs := addrs[1:]
returnWeights := []sdk.Int{sdk.NewInt(1)}
sellerModName := liquidator.ModuleName
tApp := app.NewTestApp()
sellerAcc := supply.NewEmptyModuleAccount(sellerModName)
require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100))))
tApp.InitializeFromGenesisStates(
app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100))}),
NewAuthGenStateFromAccs(authexported.GenesisAccounts{
auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
sellerAcc,
}),
)
ctx := tApp.NewContext(true, abci.Header{})
keeper := tApp.GetAuctionKeeper()
auctionID, err := keeper.StartForwardAuction(ctx, seller, c("token1", 20), c("token2", 0))
auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights)
require.NoError(t, err)
require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 30)))
// Run the endblocker, simulating a block height just before auction expiry
preExpiryHeight := ctx.BlockHeight() + int64(auction.DefaultMaxAuctionDuration) - 1
auction.EndBlocker(ctx.WithBlockHeight(preExpiryHeight), keeper)
// Run the endblocker, simulating a block time 1ns before auction expiry
preExpiryTime := ctx.BlockTime().Add(auction.DefaultBidDuration - 1)
auction.EndBlocker(ctx.WithBlockTime(preExpiryTime), keeper)
// Check auction has not been closed yet
_, found := keeper.GetAuction(ctx, auctionID)
require.True(t, found)
// Run the endblocker, simulating a block height just after auction expiry
expiryHeight := preExpiryHeight + 1
auction.EndBlocker(ctx.WithBlockHeight(expiryHeight), keeper)
// Run the endblocker, simulating a block time equal to auction expiry
expiryTime := ctx.BlockTime().Add(auction.DefaultBidDuration)
auction.EndBlocker(ctx.WithBlockTime(expiryTime), keeper)
// Check auction has been closed
_, found = keeper.GetAuction(ctx, auctionID)
@ -46,3 +59,8 @@ func TestKeeper_EndBlocker(t *testing.T) {
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
func NewAuthGenStateFromAccs(accounts authexported.GenesisAccounts) app.GenesisState {
authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts)
return app.GenesisState{auth.ModuleName: auth.ModuleCdc.MustMarshalJSON(authGenesis)}
}

View File

@ -16,51 +16,52 @@ const (
RouterKey = types.RouterKey
DefaultParamspace = types.DefaultParamspace
DefaultMaxAuctionDuration = types.DefaultMaxAuctionDuration
DefaultMaxBidDuration = types.DefaultMaxBidDuration
DefaultStartingAuctionID = types.DefaultStartingAuctionID
DefaultBidDuration = types.DefaultBidDuration
QueryGetAuction = types.QueryGetAuction
)
var (
// functions aliases
NewIDFromString = types.NewIDFromString
NewBaseAuction = types.NewBaseAuction
NewForwardAuction = types.NewForwardAuction
NewReverseAuction = types.NewReverseAuction
NewForwardReverseAuction = types.NewForwardReverseAuction
RegisterCodec = types.RegisterCodec
NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState
ValidateGenesis = types.ValidateGenesis
NewMsgPlaceBid = types.NewMsgPlaceBid
NewAuctionParams = types.NewAuctionParams
DefaultAuctionParams = types.DefaultAuctionParams
ParamKeyTable = types.ParamKeyTable
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
NewSurplusAuction = types.NewSurplusAuction
NewDebtAuction = types.NewDebtAuction
NewCollateralAuction = types.NewCollateralAuction
NewWeightedAddresses = types.NewWeightedAddresses
RegisterCodec = types.RegisterCodec
NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState
ValidateGenesis = types.ValidateGenesis
GetAuctionKey = types.GetAuctionKey
GetAuctionByTimeKey = types.GetAuctionByTimeKey
Uint64FromBytes = types.Uint64FromBytes
Uint64ToBytes = types.Uint64ToBytes
NewMsgPlaceBid = types.NewMsgPlaceBid
NewParams = types.NewParams
DefaultParams = types.DefaultParams
ParamKeyTable = types.ParamKeyTable
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
// variable aliases
ModuleCdc = types.ModuleCdc
KeyAuctionBidDuration = types.KeyAuctionBidDuration
KeyAuctionDuration = types.KeyAuctionDuration
KeyAuctionStartingID = types.KeyAuctionStartingID
ModuleCdc = types.ModuleCdc
AuctionKeyPrefix = types.AuctionKeyPrefix
AuctionByTimeKeyPrefix = types.AuctionByTimeKeyPrefix
NextAuctionIDKey = types.NextAuctionIDKey
KeyAuctionBidDuration = types.KeyAuctionBidDuration
KeyAuctionDuration = types.KeyAuctionDuration
)
type (
Auction = types.Auction
BaseAuction = types.BaseAuction
ID = types.ID
EndTime = types.EndTime
BankInput = types.BankInput
BankOutput = types.BankOutput
ForwardAuction = types.ForwardAuction
ReverseAuction = types.ReverseAuction
ForwardReverseAuction = types.ForwardReverseAuction
BankKeeper = types.BankKeeper
GenesisAuctions = types.GenesisAuctions
GenesisState = types.GenesisState
MsgPlaceBid = types.MsgPlaceBid
AuctionParams = types.AuctionParams
QueryResAuctions = types.QueryResAuctions
Keeper = keeper.Keeper
Auction = types.Auction
BaseAuction = types.BaseAuction
SurplusAuction = types.SurplusAuction
DebtAuction = types.DebtAuction
CollateralAuction = types.CollateralAuction
WeightedAddresses = types.WeightedAddresses
SupplyKeeper = types.SupplyKeeper
Auctions = types.Auctions
GenesisState = types.GenesisState
MsgPlaceBid = types.MsgPlaceBid
Params = types.Params
QueryResAuctions = types.QueryResAuctions
Keeper = keeper.Keeper
)

View File

@ -2,6 +2,7 @@ package cli
import (
"fmt"
"strconv"
"github.com/kava-labs/kava/x/auction/types"
"github.com/spf13/cobra"
@ -32,31 +33,26 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
// GetCmdPlaceBid cli command for creating and modifying cdps.
func GetCmdPlaceBid(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "placebid [AuctionID] [Bidder] [Bid] [Lot]",
Use: "placebid [auctionID] [amount]",
Short: "place a bid on an auction",
Args: cobra.ExactArgs(4),
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
id, err := types.NewIDFromString(args[0])
id, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
fmt.Printf("invalid auction id - %s \n", string(args[0]))
return err
}
bid, err := sdk.ParseCoin(args[2])
amt, err := sdk.ParseCoin(args[2])
if err != nil {
fmt.Printf("invalid bid amount - %s \n", string(args[2]))
fmt.Printf("invalid amount - %s \n", string(args[2]))
return err
}
lot, err := sdk.ParseCoin(args[3])
if err != nil {
fmt.Printf("invalid lot - %s \n", string(args[3]))
return err
}
msg := types.NewMsgPlaceBid(id, cliCtx.GetFromAddress(), bid, lot)
msg := types.NewMsgPlaceBid(id, cliCtx.GetFromAddress(), amt)
err = msg.ValidateBasic()
if err != nil {
return err

View File

@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"net/http"
"strconv"
"github.com/gorilla/mux"
@ -32,7 +33,7 @@ const (
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(
fmt.Sprintf("/auction/bid/{%s}/{%s}/{%s}/{%s}", restAuctionID, restBidder, restBid, restLot), bidHandlerFn(cliCtx)).Methods("PUT")
fmt.Sprintf("/auction/bid/{%s}/{%s}/{%s}", restAuctionID, restBidder, restBid), bidHandlerFn(cliCtx)).Methods("PUT")
}
func bidHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
@ -43,9 +44,8 @@ func bidHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
strAuctionID := vars[restAuctionID]
bechBidder := vars[restBidder]
strBid := vars[restBid]
strLot := vars[restLot]
auctionID, err := types.NewIDFromString(strAuctionID)
auctionID, err := strconv.ParseUint(strAuctionID, 10, 64)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
@ -63,13 +63,7 @@ func bidHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return
}
lot, err := sdk.ParseCoin(strLot)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
msg := types.NewMsgPlaceBid(auctionID, bidder, bid, lot)
msg := types.NewMsgPlaceBid(auctionID, bidder, bid)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return

View File

@ -1,14 +0,0 @@
/*
Package auction is a module for creating generic auctions and allowing users to place bids until a timeout is reached.
TODO
- investigate when exactly auctions close and verify queue/endblocker logic is ok
- add more test cases, add stronger validation to user inputs
- add minimum bid increment
- decided whether to put auction params like default timeouts into the auctions themselves
- add docs
- Add constants for the module and route names
- user facing things like cli, rest, querier, tags
- custom error types, codespace
*/
package auction

View File

@ -4,9 +4,11 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// InitGenesis - initializes the store state from genesis data
// InitGenesis initializes the store state from genesis data.
func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) {
keeper.SetParams(ctx, data.AuctionParams)
keeper.SetNextAuctionID(ctx, data.NextAuctionID)
keeper.SetParams(ctx, data.Params)
for _, a := range data.Auctions {
keeper.SetAuction(ctx, a)
@ -15,16 +17,18 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) {
// ExportGenesis returns a GenesisState for a given context and keeper.
func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState {
nextAuctionID, err := keeper.GetNextAuctionID(ctx)
if err != nil {
panic(err)
}
params := keeper.GetParams(ctx)
var genAuctions GenesisAuctions
iterator := keeper.GetAuctionIterator(ctx)
var genAuctions Auctions
keeper.IterateAuctions(ctx, func(a Auction) bool {
genAuctions = append(genAuctions, a)
return false
})
for ; iterator.Valid(); iterator.Next() {
auction := keeper.DecodeAuction(ctx, iterator.Value())
genAuctions = append(genAuctions, auction)
}
return NewGenesisState(params, genAuctions)
return NewGenesisState(nextAuctionID, params, genAuctions)
}

View File

@ -21,7 +21,7 @@ func NewHandler(keeper Keeper) sdk.Handler {
func handleMsgPlaceBid(ctx sdk.Context, keeper Keeper, msg MsgPlaceBid) sdk.Result {
err := keeper.PlaceBid(ctx, msg.AuctionID, msg.Bidder, msg.Bid, msg.Lot)
err := keeper.PlaceBid(ctx, msg.AuctionID, msg.Bidder, msg.Amount)
if err != nil {
return err.Result()
}

View File

@ -0,0 +1,386 @@
package keeper
import (
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/supply"
"github.com/kava-labs/kava/x/auction/types"
)
// StartSurplusAuction starts a new surplus (forward) auction.
func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin, bidDenom string) (uint64, sdk.Error) {
auction := types.NewSurplusAuction(
seller,
lot,
bidDenom,
ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration))
err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot))
if err != nil {
return 0, err
}
auctionID, err := k.StoreNewAuction(ctx, auction)
if err != nil {
return 0, err
}
return auctionID, nil
}
// StartDebtAuction starts a new debt (reverse) auction.
func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin) (uint64, sdk.Error) {
auction := types.NewDebtAuction(
buyer,
bid,
initialLot,
ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration))
// This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker.
macc := k.supplyKeeper.GetModuleAccount(ctx, buyer)
if !macc.HasPermission(supply.Minter) {
return 0, sdk.ErrInternal("module does not have minting permissions")
}
auctionID, err := k.StoreNewAuction(ctx, auction)
if err != nil {
return 0, err
}
return auctionID, nil
}
// StartCollateralAuction starts a new collateral (2-phase) auction.
func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int) (uint64, sdk.Error) {
weightedAddresses, err := types.NewWeightedAddresses(lotReturnAddrs, lotReturnWeights)
if err != nil {
return 0, err
}
auction := types.NewCollateralAuction(seller, lot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), maxBid, weightedAddresses)
err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot))
if err != nil {
return 0, err
}
auctionID, err := k.StoreNewAuction(ctx, auction)
if err != nil {
return 0, err
}
return auctionID, nil
}
// PlaceBid places a bid on any auction.
func (k Keeper) PlaceBid(ctx sdk.Context, auctionID uint64, bidder sdk.AccAddress, newAmount sdk.Coin) sdk.Error {
auction, found := k.GetAuction(ctx, auctionID)
if !found {
return sdk.ErrInternal("auction doesn't exist")
}
// validation common to all auctions
if ctx.BlockTime().After(auction.GetEndTime()) {
return sdk.ErrInternal("auction has closed")
}
// move coins and return updated auction
var err sdk.Error
var updatedAuction types.Auction
switch a := auction.(type) {
case types.SurplusAuction:
if updatedAuction, err = k.PlaceBidSurplus(ctx, a, bidder, newAmount); err != nil {
return err
}
case types.DebtAuction:
if updatedAuction, err = k.PlaceBidDebt(ctx, a, bidder, newAmount); err != nil {
return err
}
case types.CollateralAuction:
if !a.IsReversePhase() {
updatedAuction, err = k.PlaceForwardBidCollateral(ctx, a, bidder, newAmount)
} else {
updatedAuction, err = k.PlaceReverseBidCollateral(ctx, a, bidder, newAmount)
}
if err != nil {
return err
}
default:
panic(fmt.Sprintf("unrecognized auction type: %T", auction))
}
k.SetAuction(ctx, updatedAuction)
return nil
}
// PlaceBidSurplus places a forward bid on a surplus auction, moving coins and returning the updated auction.
func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.SurplusAuction, sdk.Error) {
// Validate new bid
if bid.Denom != a.Bid.Denom {
return a, sdk.ErrInternal("bid denom doesn't match auction")
}
if !a.Bid.IsLT(bid) {
return a, sdk.ErrInternal("bid not greater than last bid")
}
// 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).
if !bidder.Equals(a.Bidder) && !a.Bid.IsZero() {
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
}
// Increase in bid is burned
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, a.Initiator, sdk.NewCoins(bid.Sub(a.Bid)))
if err != nil {
return a, err
}
err = k.supplyKeeper.BurnCoins(ctx, a.Initiator, sdk.NewCoins(bid.Sub(a.Bid)))
if err != nil {
return a, err
}
// Update Auction
a.Bidder = bidder
a.Bid = bid
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout
return a, nil
}
// PlaceForwardBidCollateral places a forward bid on a collateral auction, moving coins and returning the updated auction.
func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, bid sdk.Coin) (types.CollateralAuction, sdk.Error) {
// Validate new bid
if bid.Denom != a.Bid.Denom {
return a, sdk.ErrInternal("bid denom doesn't match auction")
}
if a.IsReversePhase() {
return a, sdk.ErrInternal("auction is not in forward phase")
}
if !a.Bid.IsLT(bid) {
return a, sdk.ErrInternal("auction in forward phase, new bid not higher than last bid")
}
if a.MaxBid.IsLT(bid) {
return a, sdk.ErrInternal("bid higher than max bid")
}
// 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).
if !bidder.Equals(a.Bidder) && !a.Bid.IsZero() {
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
}
// Increase in bid sent to auction initiator
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, a.Initiator, sdk.NewCoins(bid.Sub(a.Bid)))
if err != nil {
return a, err
}
// Update Auction
a.Bidder = bidder
a.Bid = bid
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout
return a, nil
}
// PlaceReverseBidCollateral places a reverse bid on a collateral auction, moving coins and returning the updated auction.
func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.CollateralAuction, sdk.Error) {
// Validate new bid
if lot.Denom != a.Lot.Denom {
return a, sdk.ErrInternal("lot denom doesn't match auction")
}
if !a.IsReversePhase() {
return a, sdk.ErrInternal("auction not in reverse phase")
}
if lot.IsNegative() {
return a, sdk.ErrInternal("can't bid negative amount")
}
if !lot.IsLT(a.Lot) {
return a, sdk.ErrInternal("auction in reverse phase, new bid not less than previous amount")
}
// New bidder pays back old bidder
// Catch edge cases of a bidder replacing their own bid
if !bidder.Equals(a.Bidder) {
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
}
// Decrease in lot is sent to weighted addresses (normally the CDP depositors)
// TODO paying out rateably to cdp depositors is vulnerable to errors compounding over multiple bids - check this can't be gamed.
lotPayouts, err := splitCoinIntoWeightedBuckets(a.Lot.Sub(lot), a.LotReturns.Weights)
if err != nil {
return a, err
}
for i, payout := range lotPayouts {
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.LotReturns.Addresses[i], sdk.NewCoins(payout))
if err != nil {
return a, err
}
}
// Update Auction
a.Bidder = bidder
a.Lot = lot
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout
return a, nil
}
// PlaceBidDebt places a reverse bid on a debt auction, moving coins and returning the updated auction.
func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.AccAddress, lot sdk.Coin) (types.DebtAuction, sdk.Error) {
// Validate new bid
if lot.Denom != a.Lot.Denom {
return a, sdk.ErrInternal("lot denom doesn't match auction")
}
if lot.IsNegative() {
return a, sdk.ErrInternal("lot less than 0")
}
if !lot.IsLT(a.Lot) {
return a, sdk.ErrInternal("lot not smaller than last lot")
}
// New bidder pays back old bidder
// Catch edge cases of a bidder replacing their own bid
if !bidder.Equals(a.Bidder) {
err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, types.ModuleName, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Bid))
if err != nil {
return a, err
}
}
// Update Auction
a.Bidder = bidder
a.Lot = lot
a.EndTime = earliestTime(ctx.BlockTime().Add(k.GetParams(ctx).BidDuration), a.MaxEndTime) // increment timeout
return a, nil
}
// CloseAuction closes an auction and distributes funds to the highest bidder.
func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) sdk.Error {
auction, found := k.GetAuction(ctx, auctionID)
if !found {
return sdk.ErrInternal("auction doesn't exist")
}
if ctx.BlockTime().Before(auction.GetEndTime()) {
return sdk.ErrInternal(fmt.Sprintf("auction can't be closed as curent block time (%v) is under auction end time (%v)", ctx.BlockTime(), auction.GetEndTime()))
}
// payout to the last bidder
switch auc := auction.(type) {
case types.SurplusAuction:
if err := k.PayoutSurplusAuction(ctx, auc); err != nil {
return err
}
case types.DebtAuction:
if err := k.PayoutDebtAuction(ctx, auc); err != nil {
return err
}
case types.CollateralAuction:
if err := k.PayoutCollateralAuction(ctx, auc); err != nil {
return err
}
default:
panic("unrecognized auction type")
}
k.DeleteAuction(ctx, auctionID)
return nil
}
// PayoutDebtAuction pays out the proceeds for a debt auction, first minting the coins.
func (k Keeper) PayoutDebtAuction(ctx sdk.Context, a types.DebtAuction) sdk.Error {
err := k.supplyKeeper.MintCoins(ctx, a.Initiator, sdk.NewCoins(a.Lot))
if err != nil {
return err
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, a.Initiator, a.Bidder, sdk.NewCoins(a.Lot))
if err != nil {
return err
}
return nil
}
// PayoutSurplusAuction pays out the proceeds for a surplus auction.
func (k Keeper) PayoutSurplusAuction(ctx sdk.Context, a types.SurplusAuction) sdk.Error {
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot))
if err != nil {
return err
}
return nil
}
// PayoutCollateralAuction pays out the proceeds for a collateral auction.
func (k Keeper) PayoutCollateralAuction(ctx sdk.Context, a types.CollateralAuction) sdk.Error {
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot))
if err != nil {
return err
}
return nil
}
// CloseExpiredAuctions finds all auctions that are past (or at) their ending times and closes them, paying out to the highest bidder.
func (k Keeper) CloseExpiredAuctions(ctx sdk.Context) sdk.Error {
var expiredAuctions []uint64
k.IterateAuctionsByTime(ctx, ctx.BlockTime(), func(id uint64) bool {
expiredAuctions = append(expiredAuctions, id)
return false
})
// Note: iteration and auction closing are in separate loops as db should not be modified during iteration // TODO is this correct? gov modifies during iteration
for _, id := range expiredAuctions {
if err := k.CloseAuction(ctx, id); err != nil {
return err
}
}
return nil
}
// earliestTime returns the earliest of two times.
func earliestTime(t1, t2 time.Time) time.Time {
if t1.Before(t2) {
return t1
} else {
return t2 // also returned if times are equal
}
}
// splitCoinIntoWeightedBuckets divides up some amount of coins according to some weights.
func splitCoinIntoWeightedBuckets(coin sdk.Coin, buckets []sdk.Int) ([]sdk.Coin, sdk.Error) {
for _, bucket := range buckets {
if bucket.IsNegative() {
return nil, sdk.ErrInternal("cannot split coin into bucket with negative weight")
}
}
amounts := splitIntIntoWeightedBuckets(coin.Amount, buckets)
result := make([]sdk.Coin, len(amounts))
for i, a := range amounts {
result[i] = sdk.NewCoin(coin.Denom, a)
}
return result, nil
}

View File

@ -0,0 +1,246 @@
package keeper_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/supply"
"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/types"
"github.com/kava-labs/kava/x/liquidator"
)
func TestSurplusAuctionBasic(t *testing.T) {
// Setup
_, addrs := app.GeneratePrivKeyAddressPairs(1)
buyer := addrs[0]
sellerModName := liquidator.ModuleName
sellerAddr := supply.NewModuleAddress(sellerModName)
tApp := app.NewTestApp()
sellerAcc := supply.NewEmptyModuleAccount(sellerModName, supply.Burner) // forward auctions burn proceeds
require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100))))
tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{
auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
sellerAcc,
}),
)
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetAuctionKeeper()
// Create an auction (lot: 20 token1, initialBid: 0 token2)
auctionID, err := keeper.StartSurplusAuction(ctx, sellerModName, c("token1", 20), "token2") // lot, bid denom
require.NoError(t, err)
// Check seller's coins have decreased
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100)))
// PlaceBid (bid: 10 token, lot: same as starting)
require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 10)))
// Check buyer's coins have decreased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90)))
// Check seller's coins have not increased (because proceeds are burned)
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100)))
// increment bid same bidder
err = keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 20))
require.NoError(t, err)
// Close auction at just at auction expiry time
ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration))
require.NoError(t, keeper.CloseAuction(ctx, auctionID))
// Check buyer's coins increased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 80)))
}
func TestDebtAuctionBasic(t *testing.T) {
// Setup
_, addrs := app.GeneratePrivKeyAddressPairs(1)
seller := addrs[0]
buyerModName := liquidator.ModuleName
buyerAddr := supply.NewModuleAddress(buyerModName)
tApp := app.NewTestApp()
tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{
auth.NewBaseAccount(seller, cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
supply.NewEmptyModuleAccount(buyerModName, supply.Minter), // reverse auctions mint payout
}),
)
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetAuctionKeeper()
// Start auction
auctionID, err := keeper.StartDebtAuction(ctx, buyerModName, c("token1", 20), c("token2", 99999)) // buyer, bid, initialLot
require.NoError(t, err)
// Check buyer's coins have not decreased, as lot is minted at the end
tApp.CheckBalance(t, ctx, buyerAddr, nil) // zero coins
// Place a bid
require.NoError(t, keeper.PlaceBid(ctx, 0, seller, c("token2", 10)))
// Check seller's coins have decreased
tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100)))
// Check buyer's coins have increased
tApp.CheckBalance(t, ctx, buyerAddr, cs(c("token1", 20)))
// Close auction at just after auction expiry
ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration))
require.NoError(t, keeper.CloseAuction(ctx, auctionID))
// Check seller's coins increased
tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110)))
}
func TestCollateralAuctionBasic(t *testing.T) {
// Setup
_, addrs := app.GeneratePrivKeyAddressPairs(4)
buyer := addrs[0]
returnAddrs := addrs[1:]
returnWeights := is(30, 20, 10)
sellerModName := liquidator.ModuleName
sellerAddr := supply.NewModuleAddress(sellerModName)
tApp := app.NewTestApp()
sellerAcc := supply.NewEmptyModuleAccount(sellerModName)
require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100))))
tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{
auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
auth.NewBaseAccount(returnAddrs[0], cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
auth.NewBaseAccount(returnAddrs[1], cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
auth.NewBaseAccount(returnAddrs[2], cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
sellerAcc,
}),
)
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetAuctionKeeper()
// Start auction
auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights) // seller, lot, maxBid, otherPerson
require.NoError(t, err)
// Check seller's coins have decreased
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100)))
// Place a forward bid
require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 10)))
// Check bidder's coins have decreased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90)))
// Check seller's coins have increased
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 110)))
// Check return addresses have not received coins
for _, ra := range returnAddrs {
tApp.CheckBalance(t, ctx, ra, cs(c("token1", 100), c("token2", 100)))
}
// Place a reverse bid
require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50))) // first bid up to max bid to switch phases
require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token1", 15)))
// Check bidder's coins have decreased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50)))
// Check seller's coins have increased
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 150)))
// Check return addresses have received coins
tApp.CheckBalance(t, ctx, returnAddrs[0], cs(c("token1", 102), c("token2", 100)))
tApp.CheckBalance(t, ctx, returnAddrs[1], cs(c("token1", 102), c("token2", 100)))
tApp.CheckBalance(t, ctx, returnAddrs[2], cs(c("token1", 101), c("token2", 100)))
// Close auction at just after auction expiry
ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration))
require.NoError(t, keeper.CloseAuction(ctx, auctionID))
// Check buyer's coins increased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 115), c("token2", 50)))
}
func TestStartSurplusAuction(t *testing.T) {
someTime := time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)
type args struct {
seller string
lot sdk.Coin
bidDenom string
}
testCases := []struct {
name string
blockTime time.Time
args args
expectPass bool
}{
{
"normal",
someTime,
args{liquidator.ModuleName, c("stable", 10), "gov"},
true,
},
{
"no module account",
someTime,
args{"nonExistentModule", c("stable", 10), "gov"},
false,
},
{
"not enough coins",
someTime,
args{liquidator.ModuleName, c("stable", 101), "gov"},
false,
},
{
"incorrect denom",
someTime,
args{liquidator.ModuleName, c("notacoin", 10), "gov"},
false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// setup
initialLiquidatorCoins := cs(c("stable", 100))
tApp := app.NewTestApp()
liqAcc := supply.NewEmptyModuleAccount(liquidator.ModuleName, supply.Burner)
require.NoError(t, liqAcc.SetCoins(initialLiquidatorCoins))
tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{liqAcc}),
)
ctx := tApp.NewContext(false, abci.Header{}).WithBlockTime(tc.blockTime)
keeper := tApp.GetAuctionKeeper()
// run function under test
id, err := keeper.StartSurplusAuction(ctx, tc.args.seller, tc.args.lot, tc.args.bidDenom)
// check
sk := tApp.GetSupplyKeeper()
liquidatorCoins := sk.GetModuleAccount(ctx, liquidator.ModuleName).GetCoins()
actualAuc, found := keeper.GetAuction(ctx, id)
if tc.expectPass {
require.NoError(t, err)
// check coins moved
require.Equal(t, initialLiquidatorCoins.Sub(cs(tc.args.lot)), liquidatorCoins)
// 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),
}})
require.Equal(t, expectedAuction, actualAuc)
} else {
require.Error(t, err)
// check coins not moved
require.Equal(t, initialLiquidatorCoins, liquidatorCoins)
// check auction not in store
require.False(t, found)
}
})
}
}

View File

@ -0,0 +1,24 @@
package keeper_test
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/kava-labs/kava/app"
)
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
func i(n int64) sdk.Int { return sdk.NewInt(n) }
func is(ns ...int64) (is []sdk.Int) {
for _, n := range ns {
is = append(is, sdk.NewInt(n))
}
return
}
func NewAuthGenStateFromAccs(accounts authexported.GenesisAccounts) app.GenesisState {
authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts)
return app.GenesisState{auth.ModuleName: auth.ModuleCdc.MustMarshalJSON(authGenesis)}
}

View File

@ -1,314 +1,167 @@
package keeper
import (
"bytes"
"fmt"
"time"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
"github.com/tendermint/tendermint/libs/log"
"github.com/kava-labs/kava/x/auction/types"
)
type Keeper struct {
bankKeeper types.BankKeeper
supplyKeeper types.SupplyKeeper
storeKey sdk.StoreKey
cdc *codec.Codec
paramSubspace subspace.Subspace
// TODO codespace
}
// NewKeeper returns a new auction keeper.
func NewKeeper(cdc *codec.Codec, bankKeeper types.BankKeeper, storeKey sdk.StoreKey, paramstore subspace.Subspace) Keeper {
func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, supplyKeeper types.SupplyKeeper, paramstore subspace.Subspace) Keeper {
return Keeper{
bankKeeper: bankKeeper,
supplyKeeper: supplyKeeper,
storeKey: storeKey,
cdc: cdc,
paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()),
}
}
// TODO these 3 start functions be combined or abstracted away?
// StartForwardAuction starts a normal auction. Known as flap in maker.
func (k Keeper) StartForwardAuction(ctx sdk.Context, seller sdk.AccAddress, lot sdk.Coin, initialBid sdk.Coin) (types.ID, sdk.Error) {
// create auction
auction, initiatorOutput := types.NewForwardAuction(seller, lot, initialBid, types.EndTime(ctx.BlockHeight())+types.DefaultMaxAuctionDuration)
// start the auction
auctionID, err := k.startAuction(ctx, &auction, initiatorOutput)
if err != nil {
return 0, err
}
return auctionID, nil
// Logger returns a module-specific logger.
func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName))
}
// StartReverseAuction starts an auction where sellers compete by offering decreasing prices. Known as flop in maker.
func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer sdk.AccAddress, bid sdk.Coin, initialLot sdk.Coin) (types.ID, sdk.Error) {
// create auction
auction, initiatorOutput := types.NewReverseAuction(buyer, bid, initialLot, types.EndTime(ctx.BlockHeight())+types.DefaultMaxAuctionDuration)
// start the auction
auctionID, err := k.startAuction(ctx, &auction, initiatorOutput)
if err != nil {
return 0, err
}
return auctionID, nil
// SetNextAuctionID stores an ID to be used for the next created auction
func (k Keeper) SetNextAuctionID(ctx sdk.Context, id uint64) {
store := ctx.KVStore(k.storeKey)
store.Set(types.NextAuctionIDKey, types.Uint64ToBytes(id))
}
// StartForwardReverseAuction starts an auction where bidders bid up to a maxBid, then switch to bidding down on price. Known as flip in maker.
func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller sdk.AccAddress, lot sdk.Coin, maxBid sdk.Coin, otherPerson sdk.AccAddress) (types.ID, sdk.Error) {
// create auction
initialBid := sdk.NewInt64Coin(maxBid.Denom, 0) // set the bidding coin denomination from the specified max bid
auction, initiatorOutput := types.NewForwardReverseAuction(seller, lot, initialBid, types.EndTime(ctx.BlockHeight())+types.DefaultMaxAuctionDuration, maxBid, otherPerson)
// start the auction
auctionID, err := k.startAuction(ctx, &auction, initiatorOutput)
if err != nil {
return 0, err
// GetNextAuctionID reads the next available global ID from store
func (k Keeper) GetNextAuctionID(ctx sdk.Context) (uint64, sdk.Error) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.NextAuctionIDKey)
if bz == nil {
return 0, sdk.ErrInternal("initial auction ID hasn't been set")
}
return auctionID, nil
return types.Uint64FromBytes(bz), nil
}
func (k Keeper) startAuction(ctx sdk.Context, auction types.Auction, initiatorOutput types.BankOutput) (types.ID, sdk.Error) {
// get ID
newAuctionID, err := k.getNextAuctionID(ctx)
// IncrementNextAuctionID increments the next auction ID in the store by 1.
func (k Keeper) IncrementNextAuctionID(ctx sdk.Context) sdk.Error {
id, err := k.GetNextAuctionID(ctx)
if err != nil {
return err
}
k.SetNextAuctionID(ctx, id+1)
return nil
}
// StoreNewAuction stores an auction, adding a new ID
func (k Keeper) StoreNewAuction(ctx sdk.Context, auction types.Auction) (uint64, sdk.Error) {
newAuctionID, err := k.GetNextAuctionID(ctx)
if err != nil {
return 0, err
}
// set ID
auction.SetID(newAuctionID)
auction = auction.WithID(newAuctionID)
// subtract coins from initiator
_, err = k.bankKeeper.SubtractCoins(ctx, initiatorOutput.Address, sdk.NewCoins(initiatorOutput.Coin))
if err != nil {
return 0, err
}
// store auction
k.SetAuction(ctx, auction)
k.incrementNextAuctionID(ctx)
err = k.IncrementNextAuctionID(ctx)
if err != nil {
return 0, err
}
return newAuctionID, nil
}
// PlaceBid places a bid on any auction.
func (k Keeper) PlaceBid(ctx sdk.Context, auctionID types.ID, bidder sdk.AccAddress, bid sdk.Coin, lot sdk.Coin) sdk.Error {
// get auction from store
auction, found := k.GetAuction(ctx, auctionID)
if !found {
return sdk.ErrInternal("auction doesn't exist")
}
// place bid
coinOutputs, coinInputs, err := auction.PlaceBid(types.EndTime(ctx.BlockHeight()), bidder, lot, bid) // update auction according to what type of auction it is // TODO should this return updated Auction to be more immutable?
if err != nil {
return err
}
// TODO this will fail if someone tries to update their bid without the full bid amount sitting in their account
// sub outputs
for _, output := range coinOutputs {
_, err = k.bankKeeper.SubtractCoins(ctx, output.Address, sdk.NewCoins(output.Coin)) // TODO handle errors properly here. All coin transfers should be atomic. InputOutputCoins may work
if err != nil {
panic(err)
}
}
// add inputs
for _, input := range coinInputs {
_, err = k.bankKeeper.AddCoins(ctx, input.Address, sdk.NewCoins(input.Coin)) // TODO errors
if err != nil {
panic(err)
}
}
// store updated auction
k.SetAuction(ctx, auction)
return nil
}
// CloseAuction closes an auction and distributes funds to the seller and highest bidder.
// TODO because this is called by the end blocker, it has to be valid for the duration of the EndTime block. Should maybe move this to a begin blocker?
func (k Keeper) CloseAuction(ctx sdk.Context, auctionID types.ID) sdk.Error {
// get the auction from the store
auction, found := k.GetAuction(ctx, auctionID)
if !found {
return sdk.ErrInternal("auction doesn't exist")
}
// error if auction has not reached the end time
if ctx.BlockHeight() < int64(auction.GetEndTime()) { // auctions close at the end of the block with blockheight == EndTime
return sdk.ErrInternal(fmt.Sprintf("auction can't be closed as curent block height (%v) is under auction end time (%v)", ctx.BlockHeight(), auction.GetEndTime()))
}
// payout to the last bidder
coinInput := auction.GetPayout()
_, err := k.bankKeeper.AddCoins(ctx, coinInput.Address, sdk.NewCoins(coinInput.Coin))
if err != nil {
return err
}
// Delete auction from store (and queue)
k.DeleteAuction(ctx, auctionID)
return nil
}
// ---------- Store methods ----------
// Use these to add and remove auction from the store.
// getNextAuctionID gets the next available global AuctionID
func (k Keeper) getNextAuctionID(ctx sdk.Context) (types.ID, sdk.Error) { // TODO don't need error return here
// get next ID from store
store := ctx.KVStore(k.storeKey)
bz := store.Get(k.getNextAuctionIDKey())
if bz == nil {
// if not found, set the id at 0
bz = k.cdc.MustMarshalBinaryLengthPrefixed(types.ID(0))
store.Set(k.getNextAuctionIDKey(), bz)
// TODO Why does the gov module set the id in genesis? :
//return 0, ErrInvalidGenesis(keeper.codespace, "InitialProposalID never set")
}
var auctionID types.ID
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &auctionID)
return auctionID, nil
}
// incrementNextAuctionID increments the global ID in the store by 1
func (k Keeper) incrementNextAuctionID(ctx sdk.Context) sdk.Error {
// get next ID from store
store := ctx.KVStore(k.storeKey)
bz := store.Get(k.getNextAuctionIDKey())
if bz == nil {
panic("initial auctionID never set in genesis")
//return 0, ErrInvalidGenesis(keeper.codespace, "InitialProposalID never set") // TODO is this needed? Why not just set it zero here?
}
var auctionID types.ID
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &auctionID)
// increment the stored next ID
bz = k.cdc.MustMarshalBinaryLengthPrefixed(auctionID + 1)
store.Set(k.getNextAuctionIDKey(), bz)
return nil
}
// SetAuction puts the auction into the database and adds it to the queue
// it overwrites any pre-existing auction with same ID
// SetAuction puts the auction into the store, and updates any indexes.
func (k Keeper) SetAuction(ctx sdk.Context, auction types.Auction) {
// remove the auction from the queue if it is already in there
// remove the auction from the byTime index if it is already in there
existingAuction, found := k.GetAuction(ctx, auction.GetID())
if found {
k.removeFromQueue(ctx, existingAuction.GetEndTime(), existingAuction.GetID())
k.removeFromByTimeIndex(ctx, existingAuction.GetEndTime(), existingAuction.GetID())
}
// store auction
store := ctx.KVStore(k.storeKey)
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix)
bz := k.cdc.MustMarshalBinaryLengthPrefixed(auction)
store.Set(k.getAuctionKey(auction.GetID()), bz)
store.Set(types.GetAuctionKey(auction.GetID()), bz)
// add to the queue
k.InsertIntoQueue(ctx, auction.GetEndTime(), auction.GetID())
k.InsertIntoByTimeIndex(ctx, auction.GetEndTime(), auction.GetID())
}
// getAuction gets an auction from the store by auctionID
func (k Keeper) GetAuction(ctx sdk.Context, auctionID types.ID) (types.Auction, bool) {
// GetAuction gets an auction from the store.
func (k Keeper) GetAuction(ctx sdk.Context, auctionID uint64) (types.Auction, bool) {
var auction types.Auction
store := ctx.KVStore(k.storeKey)
bz := store.Get(k.getAuctionKey(auctionID))
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix)
bz := store.Get(types.GetAuctionKey(auctionID))
if bz == nil {
return auction, false // TODO what is the correct behavior when an auction is not found? gov module follows this pattern of returning a bool
return auction, false
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &auction)
return auction, true
}
// DeleteAuction removes an auction from the store without any validation
func (k Keeper) DeleteAuction(ctx sdk.Context, auctionID types.ID) {
// remove from queue
// DeleteAuction removes an auction from the store, and any indexes.
func (k Keeper) DeleteAuction(ctx sdk.Context, auctionID uint64) {
auction, found := k.GetAuction(ctx, auctionID)
if found {
k.removeFromQueue(ctx, auction.GetEndTime(), auctionID)
k.removeFromByTimeIndex(ctx, auction.GetEndTime(), auctionID)
}
// delete auction
store := ctx.KVStore(k.storeKey)
store.Delete(k.getAuctionKey(auctionID))
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix)
store.Delete(types.GetAuctionKey(auctionID))
}
// ---------- Queue and key methods ----------
// These are lower level function used by the store methods above.
func (k Keeper) getNextAuctionIDKey() []byte {
return []byte("nextAuctionID")
}
func (k Keeper) getAuctionKey(auctionID types.ID) []byte {
return []byte(fmt.Sprintf("auctions:%d", auctionID))
// InsertIntoByTimeIndex adds an auction ID and end time into the byTime index.
func (k Keeper) InsertIntoByTimeIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) { // TODO make private, and find way to make tests work
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix)
store.Set(types.GetAuctionByTimeKey(endTime, auctionID), types.Uint64ToBytes(auctionID))
}
// Inserts a AuctionID into the queue at endTime
func (k Keeper) InsertIntoQueue(ctx sdk.Context, endTime types.EndTime, auctionID types.ID) {
// get the store
store := ctx.KVStore(k.storeKey)
// marshal thing to be inserted
bz := k.cdc.MustMarshalBinaryLengthPrefixed(auctionID)
// store it
store.Set(
getQueueElementKey(endTime, auctionID),
bz,
// removeFromByTimeIndex removes an auction ID and end time from the byTime index.
func (k Keeper) removeFromByTimeIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix)
store.Delete(types.GetAuctionByTimeKey(endTime, auctionID))
}
// IterateAuctionByTime provides an iterator over auctions ordered by auction.EndTime.
// For each auction cb will be callled. If cb returns true the iterator will close and stop.
func (k Keeper) IterateAuctionsByTime(ctx sdk.Context, inclusiveCutoffTime time.Time, cb func(auctionID uint64) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix)
iterator := store.Iterator(
nil, // start at the very start of the prefix store
sdk.PrefixEndBytes(sdk.FormatTimeBytes(inclusiveCutoffTime)), // include any keys with times equal to inclusiveCutoffTime
)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
auctionID := types.Uint64FromBytes(iterator.Value())
if cb(auctionID) {
break
}
}
}
// removes an auctionID from the queue
func (k Keeper) removeFromQueue(ctx sdk.Context, endTime types.EndTime, auctionID types.ID) {
store := ctx.KVStore(k.storeKey)
store.Delete(getQueueElementKey(endTime, auctionID))
}
// IterateAuctions provides an iterator over all stored auctions.
// For each auction, cb will be called. If cb returns true, the iterator will close and stop.
func (k Keeper) IterateAuctions(ctx sdk.Context, cb func(auction types.Auction) (stop bool)) {
iterator := sdk.KVStorePrefixIterator(ctx.KVStore(k.storeKey), types.AuctionKeyPrefix)
// Returns an iterator for all the auctions in the queue that expire by endTime
func (k Keeper) GetQueueIterator(ctx sdk.Context, endTime types.EndTime) sdk.Iterator { // TODO rename to "getAuctionsByExpiry" ?
// get store
store := ctx.KVStore(k.storeKey)
// get an interator
return store.Iterator(
queueKeyPrefix, // start key
sdk.PrefixEndBytes(getQueueElementKeyPrefix(endTime)), // end key (apparently exclusive but tests suggested otherwise)
)
}
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var auction types.Auction
k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &auction)
// GetAuctionIterator returns an iterator over all auctions in the store
func (k Keeper) GetAuctionIterator(ctx sdk.Context) sdk.Iterator {
store := ctx.KVStore(k.storeKey)
return sdk.KVStorePrefixIterator(store, nil)
}
var queueKeyPrefix = []byte("queue")
var keyDelimiter = []byte(":")
// Returns half a key for an auctionID in the queue, it missed the id off the end
func getQueueElementKeyPrefix(endTime types.EndTime) []byte {
return bytes.Join([][]byte{
queueKeyPrefix,
sdk.Uint64ToBigEndian(uint64(endTime)), // TODO check this gives correct ordering
}, keyDelimiter)
}
// Returns the key for an auctionID in the queue
func getQueueElementKey(endTime types.EndTime, auctionID types.ID) []byte {
return bytes.Join([][]byte{
queueKeyPrefix,
sdk.Uint64ToBigEndian(uint64(endTime)), // TODO check this gives correct ordering
sdk.Uint64ToBigEndian(uint64(auctionID)),
}, keyDelimiter)
}
// GetAuctionID returns the id from an input Auction
func (k Keeper) DecodeAuctionID(ctx sdk.Context, idBytes []byte) types.ID {
var auctionID types.ID
k.cdc.MustUnmarshalBinaryLengthPrefixed(idBytes, &auctionID)
return auctionID
}
func (k Keeper) DecodeAuction(ctx sdk.Context, auctionBytes []byte) types.Auction {
var auction types.Auction
k.cdc.MustUnmarshalBinaryBare(auctionBytes, &auction)
return auction
if cb(auction) {
break
}
}
}

View File

@ -2,142 +2,36 @@ package keeper_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"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/keeper"
"github.com/kava-labs/kava/x/auction/types"
)
func TestKeeper_ForwardAuction(t *testing.T) {
// Setup
_, addrs := app.GeneratePrivKeyAddressPairs(2)
seller := addrs[0]
buyer := addrs[1]
tApp := app.NewTestApp()
tApp.InitializeFromGenesisStates(
app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}),
)
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetAuctionKeeper()
// Create an auction (lot: 20 t1, initialBid: 0 t2)
auctionID, err := keeper.StartForwardAuction(ctx, seller, c("token1", 20), c("token2", 0)) // lot, initialBid
require.NoError(t, err)
// Check seller's coins have decreased
tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100)))
// PlaceBid (bid: 10 t2, lot: same as starting)
require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 10), c("token1", 20))) // bid, lot
// Check buyer's coins have decreased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90)))
// Check seller's coins have increased
tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110)))
// Close auction at just after auction expiry
ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration))
require.NoError(t, keeper.CloseAuction(ctx, auctionID))
// Check buyer's coins increased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90)))
}
func TestKeeper_ReverseAuction(t *testing.T) {
// Setup
_, addrs := app.GeneratePrivKeyAddressPairs(2)
seller := addrs[0]
buyer := addrs[1]
tApp := app.NewTestApp()
tApp.InitializeFromGenesisStates(
app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}),
)
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetAuctionKeeper()
// Start auction
auctionID, err := keeper.StartReverseAuction(ctx, buyer, c("token1", 20), c("token2", 99)) // buyer, bid, initialLot
require.NoError(t, err)
// Check buyer's coins have decreased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 1)))
// Place a bid
require.NoError(t, keeper.PlaceBid(ctx, 0, seller, c("token1", 20), c("token2", 10))) // bid, lot
// Check seller's coins have decreased
tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100)))
// Check buyer's coins have increased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90)))
// Close auction at just after auction expiry
ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration))
require.NoError(t, keeper.CloseAuction(ctx, auctionID))
// Check seller's coins increased
tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110)))
}
func TestKeeper_ForwardReverseAuction(t *testing.T) {
// Setup
_, addrs := app.GeneratePrivKeyAddressPairs(3)
seller := addrs[0]
buyer := addrs[1]
recipient := addrs[2]
tApp := app.NewTestApp()
tApp.InitializeFromGenesisStates(
app.NewAuthGenState(addrs, []sdk.Coins{cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100)), cs(c("token1", 100), c("token2", 100))}),
)
ctx := tApp.NewContext(false, abci.Header{})
keeper := tApp.GetAuctionKeeper()
// Start auction
auctionID, err := keeper.StartForwardReverseAuction(ctx, seller, c("token1", 20), c("token2", 50), recipient) // seller, lot, maxBid, otherPerson
require.NoError(t, err)
// Check seller's coins have decreased
tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100)))
// Place a bid
require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50), c("token1", 15))) // bid, lot
// Check bidder's coins have decreased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50)))
// Check seller's coins have increased
tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 150)))
// Check "recipient" has received coins
tApp.CheckBalance(t, ctx, recipient, cs(c("token1", 105), c("token2", 100)))
// Close auction at just after auction expiry
ctx = ctx.WithBlockHeight(int64(types.DefaultMaxBidDuration))
require.NoError(t, keeper.CloseAuction(ctx, auctionID))
// Check buyer's coins increased
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 115), c("token2", 50)))
}
func TestKeeper_SetGetDeleteAuction(t *testing.T) {
func SetGetDeleteAuction(t *testing.T) {
// setup keeper, create auction
_, addrs := app.GeneratePrivKeyAddressPairs(1)
tApp := app.NewTestApp()
keeper := tApp.GetAuctionKeeper()
ctx := tApp.NewContext(true, abci.Header{})
auction, _ := types.NewForwardAuction(addrs[0], c("usdx", 100), c("kava", 0), types.EndTime(1000))
id := types.ID(5)
auction.SetID(id)
someTime := time.Date(43, time.January, 1, 0, 0, 0, 0, time.UTC) // need to specify UTC as tz info is lost on unmarshal
var id uint64 = 5
auction := types.NewSurplusAuction("some_module", c("usdx", 100), "kava", someTime).WithID(id)
// write and read from store
keeper.SetAuction(ctx, &auction)
keeper.SetAuction(ctx, auction)
readAuction, found := keeper.GetAuction(ctx, id)
// check before and after match
require.True(t, found)
require.Equal(t, &auction, readAuction)
// check auction is in queue
iter := keeper.GetQueueIterator(ctx, 100000)
require.Equal(t, 1, len(convertIteratorToSlice(keeper, iter)))
iter.Close()
require.Equal(t, auction, readAuction)
// check auction is in the index
keeper.IterateAuctionsByTime(ctx, auction.GetEndTime(), func(readID uint64) bool {
require.Equal(t, auction.GetID(), readID)
return false
})
// delete auction
keeper.DeleteAuction(ctx, id)
@ -145,53 +39,97 @@ func TestKeeper_SetGetDeleteAuction(t *testing.T) {
// check auction does not exist
_, found = keeper.GetAuction(ctx, id)
require.False(t, found)
// check auction not in queue
iter = keeper.GetQueueIterator(ctx, 100000)
require.Equal(t, 0, len(convertIteratorToSlice(keeper, iter)))
iter.Close()
// check auction not in index
keeper.IterateAuctionsByTime(ctx, time.Unix(999999999, 0), func(readID uint64) bool {
require.Fail(t, "index should be empty", " found auction ID '%s", readID)
return false
})
}
// TODO convert to table driven test with more test cases
func TestKeeper_ExpiredAuctionQueue(t *testing.T) {
func TestIncrementNextAuctionID(t *testing.T) {
// setup keeper
tApp := app.NewTestApp()
keeper := tApp.GetAuctionKeeper()
ctx := tApp.NewContext(true, abci.Header{})
// create an example queue
type queue []struct {
endTime types.EndTime
auctionID types.ID
}
q := queue{{1000, 0}, {1300, 2}, {5200, 1}}
// store id
var id uint64 = 123456
keeper.SetNextAuctionID(ctx, id)
// write and read queue
for _, v := range q {
keeper.InsertIntoQueue(ctx, v.endTime, v.auctionID)
}
iter := keeper.GetQueueIterator(ctx, 1000)
require.NoError(t, keeper.IncrementNextAuctionID(ctx))
// check before and after match
i := 0
for ; iter.Valid(); iter.Next() {
var auctionID types.ID
tApp.Codec().MustUnmarshalBinaryLengthPrefixed(iter.Value(), &auctionID)
require.Equal(t, q[i].auctionID, auctionID)
i++
}
// check id was incremented
readID, err := keeper.GetNextAuctionID(ctx)
require.NoError(t, err)
require.Equal(t, id+1, readID)
}
func convertIteratorToSlice(keeper keeper.Keeper, iterator sdk.Iterator) []types.ID {
var queue []types.ID
for ; iterator.Valid(); iterator.Next() {
var auctionID types.ID
types.ModuleCdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &auctionID)
queue = append(queue, auctionID)
func TestIterateAuctions(t *testing.T) {
// setup
tApp := app.NewTestApp()
tApp.InitializeFromGenesisStates()
keeper := tApp.GetAuctionKeeper()
ctx := tApp.NewContext(true, abci.Header{})
auctions := []types.Auction{
types.NewSurplusAuction("sellerMod", c("denom", 12345678), "anotherdenom", time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)).WithID(0),
types.NewDebtAuction("buyerMod", c("denom", 12345678), c("anotherdenom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)).WithID(1),
types.NewCollateralAuction("sellerMod", c("denom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC), c("anotherdenom", 12345678), types.WeightedAddresses{}).WithID(2),
}
return queue
for _, a := range auctions {
keeper.SetAuction(ctx, a)
}
// run
var readAuctions []types.Auction
keeper.IterateAuctions(ctx, func(a types.Auction) bool {
readAuctions = append(readAuctions, a)
return false
})
// check
require.Equal(t, auctions, readAuctions)
}
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
func TestIterateAuctionsByTime(t *testing.T) {
// setup keeper
tApp := app.NewTestApp()
keeper := tApp.GetAuctionKeeper()
ctx := tApp.NewContext(true, abci.Header{})
// setup byTime index
byTimeIndex := []struct {
endTime time.Time
auctionID uint64
}{
{time.Date(0, time.January, 1, 0, 0, 0, 0, time.UTC), 9999}, // distant past
{time.Date(1998, time.January, 1, 11, 59, 59, 999999999, time.UTC), 1}, // just before cutoff
{time.Date(1998, time.January, 1, 11, 59, 59, 999999999, time.UTC), 2}, //
{time.Date(1998, time.January, 1, 12, 0, 0, 0, time.UTC), 3}, // equal to cutoff
{time.Date(1998, time.January, 1, 12, 0, 0, 0, time.UTC), 4}, //
{time.Date(1998, time.January, 1, 12, 0, 0, 1, time.UTC), 5}, // just after cutoff
{time.Date(1998, time.January, 1, 12, 0, 0, 1, time.UTC), 6}, //
{time.Date(9999, time.January, 1, 0, 0, 0, 0, time.UTC), 0}, // distant future
}
for _, v := range byTimeIndex {
keeper.InsertIntoByTimeIndex(ctx, v.endTime, v.auctionID)
}
// read out values from index up to a cutoff time and check they are as expected
cutoffTime := time.Date(1998, time.January, 1, 12, 0, 0, 0, time.UTC)
var expectedIndex []uint64
for _, v := range byTimeIndex {
if v.endTime.Before(cutoffTime) || v.endTime.Equal(cutoffTime) { // endTime ≤ cutoffTime
expectedIndex = append(expectedIndex, v.auctionID)
}
}
var readIndex []uint64
keeper.IterateAuctionsByTime(ctx, cutoffTime, func(id uint64) bool {
readIndex = append(readIndex, id)
return false
})
require.Equal(t, expectedIndex, readIndex)
}

69
x/auction/keeper/math.go Normal file
View File

@ -0,0 +1,69 @@
package keeper
import (
"sort"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// splitIntIntoWeightedBuckets divides an initial +ve integer among several buckets in proportion to the buckets' weights
// It uses the largest remainder method:
// https://en.wikipedia.org/wiki/Largest_remainder_method
// see also: https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100
func splitIntIntoWeightedBuckets(amount sdk.Int, buckets []sdk.Int) []sdk.Int {
// TODO ideally change algorithm to work with -ve numbers. Limiting to +ve numbers until them
if amount.IsNegative() {
panic("negative amount")
}
for _, bucket := range buckets {
if bucket.IsNegative() {
panic("negative bucket")
}
}
totalWeights := totalInts(buckets...)
// split amount by weights, recording whole number part and remainder
quotients := make([]quoRem, len(buckets))
for i := range buckets {
q := amount.Mul(buckets[i]).Quo(totalWeights)
r := amount.Mul(buckets[i]).Mod(totalWeights)
quotients[i] = quoRem{index: i, quo: q, rem: r}
}
// apportion left over to buckets with the highest remainder (to minimize error)
sort.Slice(quotients, func(i, j int) bool {
return quotients[i].rem.GT(quotients[j].rem) // decreasing remainder order
})
allocated := sdk.ZeroInt()
for _, qr := range quotients {
allocated = allocated.Add(qr.quo)
}
leftToAllocate := amount.Sub(allocated)
results := make([]sdk.Int, len(quotients))
for _, qr := range quotients {
results[qr.index] = qr.quo
if !leftToAllocate.IsZero() {
results[qr.index] = results[qr.index].Add(sdk.OneInt())
leftToAllocate = leftToAllocate.Sub(sdk.OneInt())
}
}
return results
}
type quoRem struct {
index int
quo sdk.Int
rem sdk.Int
}
// totalInts adds together sdk.Ints
func totalInts(is ...sdk.Int) sdk.Int {
total := sdk.ZeroInt()
for _, i := range is {
total = total.Add(i)
}
return total
}

View File

@ -0,0 +1,36 @@
package keeper
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
func TestSplitIntIntoWeightedBuckets(t *testing.T) {
testCases := []struct {
name string
amount sdk.Int
buckets []sdk.Int
want []sdk.Int
}{
{"2split1,1", i(2), is(1, 1), is(1, 1)},
{"100split1,9", i(100), is(1, 9), is(10, 90)},
{"7split1,2", i(7), is(1, 2), is(2, 5)},
{"17split1,1,1", i(17), is(1, 1, 1), is(6, 6, 5)},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := splitIntIntoWeightedBuckets(tc.amount, tc.buckets)
require.Equal(t, tc.want, got)
})
}
}
func i(n int64) sdk.Int { return sdk.NewInt(n) }
func is(ns ...int64) (is []sdk.Int) {
for _, n := range ns {
is = append(is, sdk.NewInt(n))
}
return
}

View File

@ -5,13 +5,11 @@ import (
"github.com/kava-labs/kava/x/auction/types"
)
// SetParams sets the auth module's parameters.
func (k Keeper) SetParams(ctx sdk.Context, params types.AuctionParams) {
func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSubspace.SetParamSet(ctx, &params)
}
// GetParams gets the auth module's parameters.
func (k Keeper) GetParams(ctx sdk.Context) (params types.AuctionParams) {
func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) {
k.paramSubspace.GetParamSet(ctx, &params)
return
}

View File

@ -1,6 +1,7 @@
package keeper
import (
"fmt"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/auction/types"
@ -20,18 +21,14 @@ func NewQuerier(keeper Keeper) sdk.Querier {
}
func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) {
var AuctionsList types.QueryResAuctions
var auctionsList types.QueryResAuctions
iterator := keeper.GetAuctionIterator(ctx)
keeper.IterateAuctions(ctx, func(a types.Auction) bool {
auctionsList = append(auctionsList, fmt.Sprintf("%+v", a)) // TODO formatting
return false
})
for ; iterator.Valid(); iterator.Next() {
var auction types.Auction
keeper.cdc.MustUnmarshalBinaryBare(iterator.Value(), &auction)
AuctionsList = append(AuctionsList, auction.String())
}
bz, err2 := codec.MarshalJSONIndent(keeper.cdc, AuctionsList)
bz, err2 := codec.MarshalJSONIndent(keeper.cdc, auctionsList)
if err2 != nil {
panic("could not marshal result to JSON")
}

View File

@ -21,20 +21,20 @@ var (
_ module.AppModuleBasic = AppModuleBasic{}
)
// AppModuleBasic app module basics object
// AppModuleBasic implements the sdk.AppModuleBasic interface.
type AppModuleBasic struct{}
// Name get module name
// Name returns the module name.
func (AppModuleBasic) Name() string {
return ModuleName
}
// RegisterCodec register module codec
// RegisterCodec registers the module codec.
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
RegisterCodec(cdc)
}
// DefaultGenesis default genesis state
// DefaultGenesis returns the default genesis state.
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
}
@ -64,7 +64,7 @@ func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(StoreKey, cdc)
}
// AppModule app module type
// AppModule implements the sdk.AppModule interface.
type AppModule struct {
AppModuleBasic
keeper Keeper
@ -78,11 +78,6 @@ func NewAppModule(keeper Keeper) AppModule {
}
}
// Name module name
func (AppModule) Name() string {
return ModuleName
}
// RegisterInvariants performs a no-op.
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}

View File

@ -0,0 +1,9 @@
# Concepts
Auctions are broken down into three distinct types, which correspond to three specific functionalities within the CDP system.
* **Surplus Auction:** An auction in which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 they are willing to pay for the lot of c1. After the completion of a surplus auction, the winning bid of c2 is burned, and the bidder receives the lot of c1. As a concrete example, surplus auction are used to sell a fixed amount of USDX stable coins in exchange for increasing bids of KAVA governance tokens. The governance tokens are then burned and the winner receives USDX.
* **Debt Auction:** An auction in which a fixed amount of coins (c1) is bid for a decreasing lot of other coins (c2). Bidders decrement the lot of c2 they are willing to receive for the fixed amount of c1. As a concrete example, debt auctions are used to raise a certain amount of USDX stable coins in exchange for decreasing lots of KAVA governance tokens. The USDX tokens are used to recapitalize the cdp system and the winner receives KAVA.
* **Surplus Reverse Auction:** Are two phase auction is which a fixed lot of coins (c1) is sold for increasing amounts of other coins (c2). Bidders increment the amount of c2 until a specific `maxBid` is reached. Once `maxBid` is reached, a fixed amount of c2 is bid for a decreasing lot of c1. In the second phase, bidders decrement the lot of c1 they are willing to receive for a fixed amount of c2. As a concrete example, collateral auctions are used to sell collateral (ATOM, for example) for up to a `maxBid` amount of USDX. The USDX tokens are used to recapitalize the cdp system and the winner receives the specified lot of ATOM. In the event that the winning lot is smaller than the total lot, the excess ATOM is ratably returned to the original owners of the liquidated CDPs that were collateralized with that ATOM.
Auctions are always initiated by another module, and not directly by users. Auctions start with an expiry, the time at which the auction is guaranteed to end, even if there have been no bidders. After each bid, the auction is extended by a specific amount of time, `BidDuration`. In the case that increasing the auction time by `BidDuration` would cause the auction to go past its expiry, the expiry is chosen as the ending time.

View File

@ -0,0 +1,75 @@
# State
## Parameters and genesis state
`Paramaters` define the rules according to which auctions are run. There is only one active parameter set at any given time. Updates to the parameter set can be made via on-chain parameter update proposals.
```go
// Params governance parameters for auction module
type Params struct {
MaxAuctionDuration time.Duration `json:"max_auction_duration" yaml:"max_auction_duration"` // max length of auction
MaxBidDuration time.Duration `json:"max_bid_duration" yaml:"max_bid_duration"` // additional time added to the auction end time after each bid, capped by the expiry.
}
```
`GenesisState` defines the state that must be persisted when the blockchain stops/restarts in order for normal function of the auction module to resume.
```go
// GenesisState - auction state that must be provided at genesis
type GenesisState struct {
NextAuctionID uint64 `json:"next_auction_id" yaml:"next_auction_id"` // auctionID that will be used for the next created auction
Params Params `json:"auction_params" yaml:"auction_params"` // auction params
Auctions Auctions `json:"genesis_auctions" yaml:"genesis_auctions"` // auctions currently in the store
}
```
## Base types
```go
// Auction is an interface to several types of auction.
type Auction interface {
GetID() uint64
WithID(uint64) Auction
GetEndTime() time.Time
}
// 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.
}
// SurplusAuction is a forward auction that burns what it receives from bids.
// It is normally used to sell off excess pegged asset acquired by the CDP system.
type SurplusAuction struct {
BaseAuction
}
// DebtAuction is a reverse auction that mints what it pays out.
// It is normally used to acquire pegged asset to cover the CDP system's debts that were not covered by selling collateral.
type DebtAuction struct {
BaseAuction
}
// WeightedAddresses is a type for storing some addresses and associated weights.
type WeightedAddresses struct {
Addresses []sdk.AccAddress
Weights []sdk.Int
}
// CollateralAuction is a two phase auction.
// Initially, in forward auction phase, bids can be placed up to a max bid.
// Then it switches to a reverse auction phase, where the initial amount up for auction is bid down.
// Unsold Lot is sent to LotReturns, being divided among the addresses by weight.
// Collateral auctions are normally used to sell off collateral seized from CDPs.
type CollateralAuction struct {
BaseAuction
MaxBid sdk.Coin
LotReturns WeightedAddresses
}
```

View File

@ -0,0 +1,32 @@
# Messages
## Bidding
Users can bid on auctions using the `MsgPlaceBid` message type. All auction types can be bid on using the same message type.
```go
// MsgPlaceBid is the message type used to place a bid on any type of auction.
type MsgPlaceBid struct {
AuctionID uint64
Bidder sdk.AccAddress
Amount sdk.Coin
}
```
**State Modifications:**
* Update bidder if different than previous bidder
* For Surplus auctions:
* Update Bid to msg.Amount
* Return bid coins to previous bidder
* Burn coins equal to the increment in the bid (CurrentBid - PreviousBid)
* For Debt auctions:
* Update Lot amount to msg.Amount
* Return bid coins to previous bidder
* For Collateral auctions:
* Return bid coins to previous bidder
* If in forward phase:
* Update Bid amount to msg.Amount
* If in reverse phase:
* Update Lot amount to msg.Amount
* Extend auction by `BidDuration`, up to `MaxEndTime`

View File

@ -0,0 +1,5 @@
# Events
<!--
TODO: Add events for auction_start, auction_end, auction_bid
-->

View File

@ -0,0 +1,8 @@
# Parameters
The auction module contains the following parameters:
| Key | Type | Example |
| ------------------ | ---------------------- | -----------|
| MaxAuctionDuration | string (time.Duration) | "48h0m0s" |
| BidDuration | string (time.Duration) | "3h0m0s" |

View File

@ -0,0 +1,18 @@
# End Block
At the end of each block, auctions that have reached `EndTime` are closed. The logic to close auctions is as follows:
```go
var expiredAuctions []uint64
k.IterateAuctionsByTime(ctx, ctx.BlockTime(), func(id uint64) bool {
expiredAuctions = append(expiredAuctions, id)
return false
})
for _, id := range expiredAuctions {
err := k.CloseAuction(ctx, id)
if err != nil {
panic(err)
}
}
```

13
x/auction/spec/README.md Normal file
View File

@ -0,0 +1,13 @@
# `auction`
<!-- TOC -->
1. **[Concepts](01_concepts.md)**
2. **[State](02_state.md)**
3. **[Messages](03_messages.md)**
4. **[Events](04_events.md)**
5. **[Params](05_params.md)**
6. **[BeginBlock](06_begin_block.md)**
## Abstract
`x/auction` is an implementation of a Cosmos SDK Module that handles the creation, bidding, and payout of 3 distinct auction types. All auction types implement the `Auction` interface. Each auction type is used at different points during the normal functioning of the CDP system.

View File

@ -2,100 +2,35 @@ package types
import (
"fmt"
"strconv"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/supply"
)
// Auction is an interface to several types of auction.
// Auction is an interface for handling common actions on auctions.
type Auction interface {
GetID() ID
SetID(ID)
PlaceBid(currentBlockHeight EndTime, bidder sdk.AccAddress, lot sdk.Coin, bid sdk.Coin) ([]BankOutput, []BankInput, sdk.Error)
GetEndTime() EndTime // auctions close at the end of the block with blockheight EndTime (ie bids placed in that block are valid)
GetPayout() BankInput
String() string
GetID() uint64
WithID(uint64) Auction
GetEndTime() time.Time
}
// BaseAuction type shared by all Auctions
// BaseAuction is a common type shared by all Auctions.
type BaseAuction struct {
ID ID
Initiator sdk.AccAddress // Person who starts the auction. Giving away Lot (aka seller in a forward auction)
Lot sdk.Coin // Amount of coins up being given by initiator (FA - amount for sale by seller, RA - cost of good by buyer (bid))
Bidder sdk.AccAddress // Person who bids in the auction. Receiver of Lot. (aka buyer in forward auction, seller in RA)
Bid sdk.Coin // Amount of coins being given by the bidder (FA - bid, RA - amount being sold)
EndTime EndTime // Block height at which the auction closes. It closes at the end of this block
MaxEndTime EndTime // 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.
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 type for auction IDs
type ID uint64
// GetID is a getter for auction ID.
func (a BaseAuction) GetID() uint64 { return a.ID }
// NewIDFromString generate new auction ID from a string
func NewIDFromString(s string) (ID, error) {
n, err := strconv.ParseUint(s, 10, 64) // copied from how the gov module rest handler's parse proposal IDs
if err != nil {
return 0, err
}
return ID(n), nil
}
// EndTime type for end time of auctions
type EndTime int64 // TODO rename to Blockheight or don't define custom type
// BankInput the input and output types from the bank module where used here. But they use sdk.Coins instad of sdk.Coin. So it caused a lot of type conversion as auction mainly uses sdk.Coin.
type BankInput struct {
Address sdk.AccAddress
Coin sdk.Coin
}
// BankOutput output type for auction bids
type BankOutput struct {
Address sdk.AccAddress
Coin sdk.Coin
}
// GetID getter for auction ID
func (a BaseAuction) GetID() ID { return a.ID }
// SetID setter for auction ID
func (a *BaseAuction) SetID(id ID) { a.ID = id }
// GetEndTime getter for auction end time
func (a BaseAuction) GetEndTime() EndTime { return a.EndTime }
// GetPayout implements Auction
func (a BaseAuction) GetPayout() BankInput {
return BankInput{a.Bidder, a.Lot}
}
// PlaceBid implements Auction
func (a *BaseAuction) PlaceBid(currentBlockHeight EndTime, bidder sdk.AccAddress, lot sdk.Coin, bid sdk.Coin) ([]BankOutput, []BankInput, sdk.Error) {
// TODO check lot size matches lot?
// check auction has not closed
if currentBlockHeight > a.EndTime {
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("auction has closed")
}
// check bid is greater than last bid
if !a.Bid.IsLT(bid) { // TODO add minimum bid size
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("bid not greater than last bid")
}
// calculate coin movements
outputs := []BankOutput{{bidder, bid}} // new bidder pays bid now
inputs := []BankInput{{a.Bidder, a.Bid}, {a.Initiator, bid.Sub(a.Bid)}} // old bidder is paid back, extra goes to seller
// update auction
a.Bidder = bidder
a.Bid = bid
// increment timeout // TODO into keeper?
a.EndTime = EndTime(min(int64(currentBlockHeight+DefaultMaxBidDuration), int64(a.MaxEndTime))) // TODO is there a better way to structure these types?
return outputs, inputs, nil
}
func (e EndTime) String() string {
return string(e)
}
// GetEndTime is a getter for auction end time.
func (a BaseAuction) GetEndTime() time.Time { return a.EndTime }
func (a BaseAuction) String() string {
return fmt.Sprintf(`Auction %d:
@ -111,118 +46,76 @@ func (a BaseAuction) String() string {
)
}
// NewBaseAuction creates a new base auction
func NewBaseAuction(seller sdk.AccAddress, lot sdk.Coin, initialBid sdk.Coin, EndTime EndTime) BaseAuction {
auction := BaseAuction{
// SurplusAuction is a forward auction that burns what it receives from bids.
// It is normally used to sell off excess pegged asset acquired by the CDP system.
type SurplusAuction struct {
BaseAuction
}
// WithID returns an auction with the ID set.
func (a SurplusAuction) WithID(id uint64) Auction { a.ID = id; return a }
// 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: seller, // send the proceeds from the first bid back to the seller
Bid: initialBid, // set this to zero most of the time
EndTime: EndTime,
MaxEndTime: EndTime,
}
Bidder: nil,
Bid: sdk.NewInt64Coin(bidDenom, 0),
EndTime: endTime,
MaxEndTime: endTime,
}}
return auction
}
// ForwardAuction type for forward auctions
type ForwardAuction struct {
// DebtAuction is a reverse auction that mints what it pays out.
// It is normally used to acquire pegged asset to cover the CDP system's debts that were not covered by selling collateral.
type DebtAuction struct {
BaseAuction
}
// NewForwardAuction creates a new forward auction
func NewForwardAuction(seller sdk.AccAddress, lot sdk.Coin, initialBid sdk.Coin, EndTime EndTime) (ForwardAuction, BankOutput) {
auction := ForwardAuction{BaseAuction{
// WithID returns an auction with the ID set.
func (a DebtAuction) WithID(id uint64) Auction { a.ID = id; return a }
// NewDebtAuction returns a new debt auction.
func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime time.Time) 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{
// no ID
Initiator: seller,
Lot: lot,
Bidder: seller, // send the proceeds from the first bid back to the seller
Bid: initialBid, // set this to zero most of the time
EndTime: EndTime,
MaxEndTime: EndTime,
}}
output := BankOutput{seller, lot}
return auction, output
}
// PlaceBid implements Auction
func (a *ForwardAuction) PlaceBid(currentBlockHeight EndTime, bidder sdk.AccAddress, lot sdk.Coin, bid sdk.Coin) ([]BankOutput, []BankInput, sdk.Error) {
// TODO check lot size matches lot?
// check auction has not closed
if currentBlockHeight > a.EndTime {
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("auction has closed")
}
// check bid is greater than last bid
if !a.Bid.IsLT(bid) { // TODO add minimum bid size
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("bid not greater than last bid")
}
// calculate coin movements
outputs := []BankOutput{{bidder, bid}} // new bidder pays bid now
inputs := []BankInput{{a.Bidder, a.Bid}, {a.Initiator, bid.Sub(a.Bid)}} // old bidder is paid back, extra goes to seller
// update auction
a.Bidder = bidder
a.Bid = bid
// increment timeout // TODO into keeper?
a.EndTime = EndTime(min(int64(currentBlockHeight+DefaultMaxBidDuration), int64(a.MaxEndTime))) // TODO is there a better way to structure these types?
return outputs, inputs, nil
}
// ReverseAuction type for reverse auctions
// TODO when exporting state and initializing a new genesis, we'll need a way to differentiate forward from reverse auctions
type ReverseAuction struct {
BaseAuction
}
// NewReverseAuction creates a new reverse auction
func NewReverseAuction(buyer sdk.AccAddress, bid sdk.Coin, initialLot sdk.Coin, EndTime EndTime) (ReverseAuction, BankOutput) {
auction := ReverseAuction{BaseAuction{
// no ID
Initiator: buyer,
Initiator: buyerModAccName,
Lot: initialLot,
Bidder: buyer, // send proceeds from the first bid to the buyer
Bid: bid, // amount that the buyer it buying - doesn't change over course of auction
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,
}}
output := BankOutput{buyer, initialLot}
return auction, output
return auction
}
// PlaceBid implements Auction
func (a *ReverseAuction) PlaceBid(currentBlockHeight EndTime, bidder sdk.AccAddress, lot sdk.Coin, bid sdk.Coin) ([]BankOutput, []BankInput, sdk.Error) {
// check bid size matches bid?
// check auction has not closed
if currentBlockHeight > a.EndTime {
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("auction has closed")
}
// check bid is less than last bid
if !lot.IsLT(a.Lot) { // TODO add min bid decrements
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("lot not smaller than last lot")
}
// calculate coin movements
outputs := []BankOutput{{bidder, a.Bid}} // new bidder pays bid now
inputs := []BankInput{{a.Bidder, a.Bid}, {a.Initiator, a.Lot.Sub(lot)}} // old bidder is paid back, decrease in price for goes to buyer
// update auction
a.Bidder = bidder
a.Lot = lot
// increment timeout // TODO into keeper?
a.EndTime = EndTime(min(int64(currentBlockHeight+DefaultMaxBidDuration), int64(a.MaxEndTime))) // TODO is there a better way to structure these types?
return outputs, inputs, nil
}
// ForwardReverseAuction type for forward reverse auction
type ForwardReverseAuction struct {
// CollateralAuction is a two phase auction.
// Initially, in forward auction phase, bids can be placed up to a max bid.
// Then it switches to a reverse auction phase, where the initial amount up for auction is bid down.
// Unsold Lot is sent to LotReturns, being divided among the addresses by weight.
// Collateral auctions are normally used to sell off collateral seized from CDPs.
type CollateralAuction struct {
BaseAuction
MaxBid sdk.Coin
OtherPerson sdk.AccAddress // TODO rename, this is normally the original CDP owner
MaxBid sdk.Coin
LotReturns WeightedAddresses
}
func (a ForwardReverseAuction) String() string {
// WithID returns an auction with the ID set.
func (a CollateralAuction) WithID(id uint64) Auction { a.ID = id; return a }
// IsReversePhase returns whether the auction has switched over to reverse phase or not.
// Auction initially start in forward phase.
func (a CollateralAuction) IsReversePhase() bool {
return a.Bid.IsEqual(a.MaxBid)
}
func (a CollateralAuction) String() string {
return fmt.Sprintf(`Auction %d:
Initiator: %s
Lot: %s
@ -231,77 +124,48 @@ func (a ForwardReverseAuction) String() string {
End Time: %s
Max End Time: %s
Max Bid %s
Other Person %s`,
LotReturns %s`,
a.GetID(), a.Initiator, a.Lot,
a.Bidder, a.Bid, a.GetEndTime().String(),
a.MaxEndTime.String(), a.MaxBid, a.OtherPerson,
a.MaxEndTime.String(), a.MaxBid, a.LotReturns,
)
}
// NewForwardReverseAuction creates a new forward reverse auction
func NewForwardReverseAuction(seller sdk.AccAddress, lot sdk.Coin, initialBid sdk.Coin, EndTime EndTime, maxBid sdk.Coin, otherPerson sdk.AccAddress) (ForwardReverseAuction, BankOutput) {
auction := ForwardReverseAuction{
// NewCollateralAuction returns a new collateral auction.
func NewCollateralAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses) CollateralAuction {
auction := CollateralAuction{
BaseAuction: BaseAuction{
// no ID
Initiator: seller,
Lot: lot,
Bidder: seller, // send the proceeds from the first bid back to the seller
Bid: initialBid, // 0 most of the time
Bidder: nil,
Bid: sdk.NewInt64Coin(maxBid.Denom, 0),
EndTime: EndTime,
MaxEndTime: EndTime},
MaxBid: maxBid,
OtherPerson: otherPerson,
MaxBid: maxBid,
LotReturns: lotReturns,
}
output := BankOutput{seller, lot}
return auction, output
return auction
}
// PlaceBid implements auction
func (a *ForwardReverseAuction) PlaceBid(currentBlockHeight EndTime, bidder sdk.AccAddress, lot sdk.Coin, bid sdk.Coin) (outputs []BankOutput, inputs []BankInput, err sdk.Error) {
// check auction has not closed
if currentBlockHeight > a.EndTime {
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("auction has closed")
}
// determine phase of auction
switch {
case a.Bid.IsLT(a.MaxBid) && bid.IsLT(a.MaxBid):
// Forward auction phase
if !a.Bid.IsLT(bid) { // TODO add min bid increments
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("bid not greater than last bid")
}
outputs = []BankOutput{{bidder, bid}} // new bidder pays bid now
inputs = []BankInput{{a.Bidder, a.Bid}, {a.Initiator, bid.Sub(a.Bid)}} // old bidder is paid back, extra goes to seller
case a.Bid.IsLT(a.MaxBid):
// Switch over phase
if !bid.IsEqual(a.MaxBid) { // require bid == a.MaxBid
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("bid greater than the max bid")
}
outputs = []BankOutput{{bidder, bid}} // new bidder pays bid now
inputs = []BankInput{
{a.Bidder, a.Bid}, // old bidder is paid back
{a.Initiator, bid.Sub(a.Bid)}, // extra goes to seller
{a.OtherPerson, a.Lot.Sub(lot)}, //decrease in price for goes to original CDP owner
}
case a.Bid.IsEqual(a.MaxBid):
// Reverse auction phase
if !lot.IsLT(a.Lot) { // TODO add min bid decrements
return []BankOutput{}, []BankInput{}, sdk.ErrInternal("lot not smaller than last lot")
}
outputs = []BankOutput{{bidder, a.Bid}} // new bidder pays bid now
inputs = []BankInput{{a.Bidder, a.Bid}, {a.OtherPerson, a.Lot.Sub(lot)}} // old bidder is paid back, decrease in price for goes to original CDP owner
default:
panic("should never be reached") // TODO
}
// update auction
a.Bidder = bidder
a.Lot = lot
a.Bid = bid
// increment timeout
// TODO use bid duration param
a.EndTime = EndTime(min(int64(currentBlockHeight+DefaultMaxBidDuration), int64(a.MaxEndTime))) // TODO is there a better way to structure these types?
return outputs, inputs, nil
// WeightedAddresses is a type for storing some addresses and associated weights.
type WeightedAddresses struct {
Addresses []sdk.AccAddress
Weights []sdk.Int
}
// NewWeightedAddresses returns a new list addresses with weights.
func NewWeightedAddresses(addrs []sdk.AccAddress, weights []sdk.Int) (WeightedAddresses, sdk.Error) {
if len(addrs) != len(weights) {
return WeightedAddresses{}, sdk.ErrInternal("number of addresses doesn't match number of weights")
}
for _, w := range weights {
if w.IsNegative() {
return WeightedAddresses{}, sdk.ErrInternal("weights contain a negative amount")
}
}
return WeightedAddresses{
Addresses: addrs,
Weights: weights,
}, nil
}

View File

@ -1,403 +0,0 @@
package types
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
// TODO can this be less verbose? Should PlaceBid() be split into smaller functions?
// It would be possible to combine all auction tests into one test runner.
func TestForwardAuction_PlaceBid(t *testing.T) {
seller := sdk.AccAddress([]byte("a_seller"))
buyer1 := sdk.AccAddress([]byte("buyer1"))
buyer2 := sdk.AccAddress([]byte("buyer2"))
end := EndTime(10000)
now := EndTime(10)
type args struct {
currentBlockHeight EndTime
bidder sdk.AccAddress
lot sdk.Coin
bid sdk.Coin
}
tests := []struct {
name string
auction ForwardAuction
args args
expectedOutputs []BankOutput
expectedInputs []BankInput
expectedEndTime EndTime
expectedBidder sdk.AccAddress
expectedBid sdk.Coin
expectpass bool
}{
{
"normal",
ForwardAuction{BaseAuction{
Initiator: seller,
Lot: c("usdx", 100),
Bidder: buyer1,
Bid: c("kava", 6),
EndTime: end,
MaxEndTime: end,
}},
args{now, buyer2, c("usdx", 100), c("kava", 10)},
[]BankOutput{{buyer2, c("kava", 10)}},
[]BankInput{{buyer1, c("kava", 6)}, {seller, c("kava", 4)}},
now + DefaultMaxBidDuration,
buyer2,
c("kava", 10),
true,
},
{
"lowBid",
ForwardAuction{BaseAuction{
Initiator: seller,
Lot: c("usdx", 100),
Bidder: buyer1,
Bid: c("kava", 6),
EndTime: end,
MaxEndTime: end,
}},
args{now, buyer2, c("usdx", 100), c("kava", 5)},
[]BankOutput{},
[]BankInput{},
end,
buyer1,
c("kava", 6),
false,
},
{
"equalBid",
ForwardAuction{BaseAuction{
Initiator: seller,
Lot: c("usdx", 100),
Bidder: buyer1,
Bid: c("kava", 6),
EndTime: end,
MaxEndTime: end,
}},
args{now, buyer2, c("usdx", 100), c("kava", 6)},
[]BankOutput{},
[]BankInput{},
end,
buyer1,
c("kava", 6),
false,
},
{
"timeout",
ForwardAuction{BaseAuction{
Initiator: seller,
Lot: c("usdx", 100),
Bidder: buyer1,
Bid: c("kava", 6),
EndTime: end,
MaxEndTime: end,
}},
args{end + 1, buyer2, c("usdx", 100), c("kava", 10)},
[]BankOutput{},
[]BankInput{},
end,
buyer1,
c("kava", 6),
false,
},
{
"hitMaxEndTime",
ForwardAuction{BaseAuction{
Initiator: seller,
Lot: c("usdx", 100),
Bidder: buyer1,
Bid: c("kava", 6),
EndTime: end,
MaxEndTime: end,
}},
args{end - 1, buyer2, c("usdx", 100), c("kava", 10)},
[]BankOutput{{buyer2, c("kava", 10)}},
[]BankInput{{buyer1, c("kava", 6)}, {seller, c("kava", 4)}},
end, // end time should be capped at MaxEndTime
buyer2,
c("kava", 10),
true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// update auction and return in/outputs
outputs, inputs, err := tc.auction.PlaceBid(tc.args.currentBlockHeight, tc.args.bidder, tc.args.lot, tc.args.bid)
// check for err
if tc.expectpass {
require.Nil(t, err)
} else {
require.NotNil(t, err)
}
// check for correct in/outputs
require.Equal(t, tc.expectedOutputs, outputs)
require.Equal(t, tc.expectedInputs, inputs)
// check for correct EndTime, bidder, bid
require.Equal(t, tc.expectedEndTime, tc.auction.EndTime)
require.Equal(t, tc.expectedBidder, tc.auction.Bidder)
require.Equal(t, tc.expectedBid, tc.auction.Bid)
})
}
}
func TestReverseAuction_PlaceBid(t *testing.T) {
buyer := sdk.AccAddress([]byte("a_buyer"))
seller1 := sdk.AccAddress([]byte("seller1"))
seller2 := sdk.AccAddress([]byte("seller2"))
end := EndTime(10000)
now := EndTime(10)
type args struct {
currentBlockHeight EndTime
bidder sdk.AccAddress
lot sdk.Coin
bid sdk.Coin
}
tests := []struct {
name string
auction ReverseAuction
args args
expectedOutputs []BankOutput
expectedInputs []BankInput
expectedEndTime EndTime
expectedBidder sdk.AccAddress
expectedLot sdk.Coin
expectpass bool
}{
{
"normal",
ReverseAuction{BaseAuction{
Initiator: buyer,
Lot: c("kava", 10),
Bidder: seller1,
Bid: c("usdx", 100),
EndTime: end,
MaxEndTime: end,
}},
args{now, seller2, c("kava", 9), c("usdx", 100)},
[]BankOutput{{seller2, c("usdx", 100)}},
[]BankInput{{seller1, c("usdx", 100)}, {buyer, c("kava", 1)}},
now + DefaultMaxBidDuration,
seller2,
c("kava", 9),
true,
},
{
"highBid",
ReverseAuction{BaseAuction{
Initiator: buyer,
Lot: c("kava", 10),
Bidder: seller1,
Bid: c("usdx", 100),
EndTime: end,
MaxEndTime: end,
}},
args{now, seller2, c("kava", 11), c("usdx", 100)},
[]BankOutput{},
[]BankInput{},
end,
seller1,
c("kava", 10),
false,
},
{
"equalBid",
ReverseAuction{BaseAuction{
Initiator: buyer,
Lot: c("kava", 10),
Bidder: seller1,
Bid: c("usdx", 100),
EndTime: end,
MaxEndTime: end,
}},
args{now, seller2, c("kava", 10), c("usdx", 100)},
[]BankOutput{},
[]BankInput{},
end,
seller1,
c("kava", 10),
false,
},
{
"timeout",
ReverseAuction{BaseAuction{
Initiator: buyer,
Lot: c("kava", 10),
Bidder: seller1,
Bid: c("usdx", 100),
EndTime: end,
MaxEndTime: end,
}},
args{end + 1, seller2, c("kava", 9), c("usdx", 100)},
[]BankOutput{},
[]BankInput{},
end,
seller1,
c("kava", 10),
false,
},
{
"hitMaxEndTime",
ReverseAuction{BaseAuction{
Initiator: buyer,
Lot: c("kava", 10),
Bidder: seller1,
Bid: c("usdx", 100),
EndTime: end,
MaxEndTime: end,
}},
args{end - 1, seller2, c("kava", 9), c("usdx", 100)},
[]BankOutput{{seller2, c("usdx", 100)}},
[]BankInput{{seller1, c("usdx", 100)}, {buyer, c("kava", 1)}},
end, // end time should be capped at MaxEndTime
seller2,
c("kava", 9),
true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// update auction and return in/outputs
outputs, inputs, err := tc.auction.PlaceBid(tc.args.currentBlockHeight, tc.args.bidder, tc.args.lot, tc.args.bid)
// check for err
if tc.expectpass {
require.Nil(t, err)
} else {
require.NotNil(t, err)
}
// check for correct in/outputs
require.Equal(t, tc.expectedOutputs, outputs)
require.Equal(t, tc.expectedInputs, inputs)
// check for correct EndTime, bidder, bid
require.Equal(t, tc.expectedEndTime, tc.auction.EndTime)
require.Equal(t, tc.expectedBidder, tc.auction.Bidder)
require.Equal(t, tc.expectedLot, tc.auction.Lot)
})
}
}
func TestForwardReverseAuction_PlaceBid(t *testing.T) {
cdpOwner := sdk.AccAddress([]byte("a_cdp_owner"))
seller := sdk.AccAddress([]byte("a_seller"))
buyer1 := sdk.AccAddress([]byte("buyer1"))
buyer2 := sdk.AccAddress([]byte("buyer2"))
end := EndTime(10000)
now := EndTime(10)
type args struct {
currentBlockHeight EndTime
bidder sdk.AccAddress
lot sdk.Coin
bid sdk.Coin
}
tests := []struct {
name string
auction ForwardReverseAuction
args args
expectedOutputs []BankOutput
expectedInputs []BankInput
expectedEndTime EndTime
expectedBidder sdk.AccAddress
expectedLot sdk.Coin
expectedBid sdk.Coin
expectpass bool
}{
{
"normalForwardBid",
ForwardReverseAuction{BaseAuction: BaseAuction{
Initiator: seller,
Lot: c("xrp", 100),
Bidder: buyer1,
Bid: c("usdx", 5),
EndTime: end,
MaxEndTime: end},
MaxBid: c("usdx", 10),
OtherPerson: cdpOwner,
},
args{now, buyer2, c("xrp", 100), c("usdx", 6)},
[]BankOutput{{buyer2, c("usdx", 6)}},
[]BankInput{{buyer1, c("usdx", 5)}, {seller, c("usdx", 1)}},
now + DefaultMaxBidDuration,
buyer2,
c("xrp", 100),
c("usdx", 6),
true,
},
{
"normalSwitchOverBid",
ForwardReverseAuction{BaseAuction: BaseAuction{
Initiator: seller,
Lot: c("xrp", 100),
Bidder: buyer1,
Bid: c("usdx", 5),
EndTime: end,
MaxEndTime: end},
MaxBid: c("usdx", 10),
OtherPerson: cdpOwner,
},
args{now, buyer2, c("xrp", 99), c("usdx", 10)},
[]BankOutput{{buyer2, c("usdx", 10)}},
[]BankInput{{buyer1, c("usdx", 5)}, {seller, c("usdx", 5)}, {cdpOwner, c("xrp", 1)}},
now + DefaultMaxBidDuration,
buyer2,
c("xrp", 99),
c("usdx", 10),
true,
},
{
"normalReverseBid",
ForwardReverseAuction{BaseAuction: BaseAuction{
Initiator: seller,
Lot: c("xrp", 99),
Bidder: buyer1,
Bid: c("usdx", 10),
EndTime: end,
MaxEndTime: end},
MaxBid: c("usdx", 10),
OtherPerson: cdpOwner,
},
args{now, buyer2, c("xrp", 90), c("usdx", 10)},
[]BankOutput{{buyer2, c("usdx", 10)}},
[]BankInput{{buyer1, c("usdx", 10)}, {cdpOwner, c("xrp", 9)}},
now + DefaultMaxBidDuration,
buyer2,
c("xrp", 90),
c("usdx", 10),
true,
},
// TODO more test cases
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// update auction and return in/outputs
outputs, inputs, err := tc.auction.PlaceBid(tc.args.currentBlockHeight, tc.args.bidder, tc.args.lot, tc.args.bid)
// check for err
if tc.expectpass {
require.Nil(t, err)
} else {
require.NotNil(t, err)
}
// check for correct in/outputs
require.Equal(t, tc.expectedOutputs, outputs)
require.Equal(t, tc.expectedInputs, inputs)
// check for correct EndTime, bidder, bid
require.Equal(t, tc.expectedEndTime, tc.auction.EndTime)
require.Equal(t, tc.expectedBidder, tc.auction.Bidder)
require.Equal(t, tc.expectedLot, tc.auction.Lot)
require.Equal(t, tc.expectedBid, tc.auction.Bid)
})
}
}
// defined to avoid cluttering test cases with long function name
func c(denom string, amount int64) sdk.Coin {
return sdk.NewInt64Coin(denom, amount)
}

View File

@ -15,9 +15,8 @@ func init() {
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgPlaceBid{}, "auction/MsgPlaceBid", nil)
// Register the Auction interface and concrete types
cdc.RegisterInterface((*Auction)(nil), nil)
cdc.RegisterConcrete(&ForwardAuction{}, "auction/ForwardAuction", nil)
cdc.RegisterConcrete(&ReverseAuction{}, "auction/ReverseAuction", nil)
cdc.RegisterConcrete(&ForwardReverseAuction{}, "auction/ForwardReverseAuction", nil)
cdc.RegisterConcrete(SurplusAuction{}, "auction/SurplusAuction", nil)
cdc.RegisterConcrete(DebtAuction{}, "auction/DebtAuction", nil)
cdc.RegisterConcrete(CollateralAuction{}, "auction/CollateralAuction", nil)
}

View File

@ -2,9 +2,17 @@ package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
)
type BankKeeper interface {
SubtractCoins(sdk.Context, sdk.AccAddress, sdk.Coins) (sdk.Coins, sdk.Error)
AddCoins(sdk.Context, sdk.AccAddress, sdk.Coins) (sdk.Coins, sdk.Error)
// SupplyKeeper defines the expected supply Keeper
type SupplyKeeper interface {
GetModuleAccount(ctx sdk.Context, moduleName string) supplyexported.ModuleAccountI
SendCoinsFromModuleToModule(ctx sdk.Context, sender, recipient string, amt sdk.Coins) sdk.Error
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) sdk.Error
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) sdk.Error
BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error
MintCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error
}

View File

@ -4,43 +4,45 @@ import (
"bytes"
)
// GenesisAuctions type for an array of auctions
type GenesisAuctions []Auction
// Auctions is a slice of auctions.
type Auctions []Auction
// GenesisState - auction state that must be provided at genesis
// GenesisState is auction state that must be provided at chain genesis.
type GenesisState struct {
AuctionParams AuctionParams `json:"auction_params" yaml:"auction_params"`
Auctions GenesisAuctions `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 Auctions `json:"genesis_auctions" yaml:"genesis_auctions"`
}
// NewGenesisState returns a new genesis state object for auctions module
func NewGenesisState(ap AuctionParams, ga GenesisAuctions) GenesisState {
// NewGenesisState returns a new genesis state object for auctions module.
func NewGenesisState(nextID uint64, ap Params, ga Auctions) GenesisState {
return GenesisState{
AuctionParams: ap,
NextAuctionID: nextID,
Params: ap,
Auctions: ga,
}
}
// DefaultGenesisState defines default genesis state for auction module
// DefaultGenesisState returns the default genesis state for auction module.
func DefaultGenesisState() GenesisState {
return NewGenesisState(DefaultAuctionParams(), GenesisAuctions{})
return NewGenesisState(0, DefaultParams(), Auctions{})
}
// Equal checks whether two GenesisState structs are equivalent
// Equal checks whether two GenesisState structs are equivalent.
func (data GenesisState) Equal(data2 GenesisState) bool {
b1 := ModuleCdc.MustMarshalBinaryBare(data)
b2 := ModuleCdc.MustMarshalBinaryBare(data2)
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 {
return data.Equal(GenesisState{})
}
// ValidateGenesis validates genesis inputs. 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 {
if err := data.AuctionParams.Validate(); err != nil {
if err := data.Params.Validate(); err != nil {
return err
}
return nil

View File

@ -1,5 +1,12 @@
package types
import (
"encoding/binary"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
// ModuleName The name that will be used throughout the module
ModuleName = "auction"
@ -13,3 +20,30 @@ const (
// DefaultParamspace default name for parameter store
DefaultParamspace = ModuleName
)
var (
AuctionKeyPrefix = []byte{0x00} // prefix for keys that store auctions
AuctionByTimeKeyPrefix = []byte{0x01} // prefix for keys that are part of the auctionsByTime index
NextAuctionIDKey = []byte{0x02} // key for the next auction id
)
func GetAuctionKey(auctionID uint64) []byte {
return Uint64ToBytes(auctionID)
}
func GetAuctionByTimeKey(endTime time.Time, auctionID uint64) []byte {
return append(sdk.FormatTimeBytes(endTime), Uint64ToBytes(auctionID)...)
}
// Uint64ToBytes converts a uint64 into fixed length bytes for use in store keys.
func Uint64ToBytes(id uint64) []byte {
bz := make([]byte, 8)
binary.BigEndian.PutUint64(bz, uint64(id))
return bz
}
// Uint64FromBytes converts some fixed length bytes back into a uint64.
func Uint64FromBytes(bz []byte) uint64 {
return binary.BigEndian.Uint64(bz)
}

View File

@ -2,42 +2,39 @@ package types
import sdk "github.com/cosmos/cosmos-sdk/types"
// ensure Msg interface compliance at compile time
var _ sdk.Msg = &MsgPlaceBid{}
// MsgPlaceBid is the message type used to place a bid on any type of auction.
type MsgPlaceBid struct {
AuctionID ID
Bidder sdk.AccAddress // This can be a buyer (who increments bid), or a seller (who decrements lot) TODO rename to be clearer?
Bid sdk.Coin
Lot sdk.Coin
AuctionID uint64
Bidder sdk.AccAddress
Amount sdk.Coin // The new bid or lot to be set on the auction.
}
// NewMsgPlaceBid returns a new MsgPlaceBid.
func NewMsgPlaceBid(auctionID ID, bidder sdk.AccAddress, bid sdk.Coin, lot sdk.Coin) MsgPlaceBid {
func NewMsgPlaceBid(auctionID uint64, bidder sdk.AccAddress, amt sdk.Coin) MsgPlaceBid {
return MsgPlaceBid{
AuctionID: auctionID,
Bidder: bidder,
Bid: bid,
Lot: lot,
Amount: amt,
}
}
// Route return the message type used for routing the message.
func (msg MsgPlaceBid) Route() string { return "auction" }
func (msg MsgPlaceBid) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgPlaceBid) Type() string { return "place_bid" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
// ValidateBasic does a simple validation check that doesn't require access to state.
func (msg MsgPlaceBid) ValidateBasic() sdk.Error {
if msg.Bidder.Empty() {
return sdk.ErrInternal("invalid (empty) bidder address")
}
if msg.Bid.Amount.LT(sdk.ZeroInt()) {
return sdk.ErrInternal("invalid (negative) bid amount")
if !msg.Amount.IsValid() {
return sdk.ErrInternal("invalid bid amount")
}
if msg.Lot.Amount.LT(sdk.ZeroInt()) {
return sdk.ErrInternal("invalid (negative) lot amount")
}
// TODO check coin denoms
return nil
}
@ -51,43 +48,3 @@ func (msg MsgPlaceBid) GetSignBytes() []byte {
func (msg MsgPlaceBid) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Bidder}
}
// The CDP system doesn't need Msgs for starting auctions. But they could be added to allow people to create random auctions of their own, and to make this module more general purpose.
// type MsgStartForwardAuction struct {
// Seller sdk.AccAddress
// Amount sdk.Coins
// // TODO add starting bid amount?
// // TODO specify asset denom to be received
// }
// // NewMsgStartAuction returns a new MsgStartAuction.
// func NewMsgStartAuction(seller sdk.AccAddress, amount sdk.Coins, maxBid sdk.Coins) MsgStartAuction {
// return MsgStartAuction{
// Seller: seller,
// Amount: amount,
// MaxBid: maxBid,
// }
// }
// // Route return the message type used for routing the message.
// func (msg MsgStartAuction) Route() string { return "auction" }
// // Type returns a human-readable string for the message, intended for utilization within tags.
// func (msg MsgStartAuction) Type() string { return "start_auction" }
// // ValidateBasic does a simple validation check that doesn't require access to any other information.
// func (msg MsgStartAuction) ValidateBasic() sdk.Error {
// return nil
// }
// // GetSignBytes gets the canonical byte representation of the Msg.
// func (msg MsgStartAuction) GetSignBytes() []byte {
// bz := msgCdc.MustMarshalJSON(msg)
// return sdk.MustSortJSON(bz)
// }
// // GetSigners returns the addresses of signers that must sign.
// func (msg MsgStartAuction) GetSigners() []sdk.AccAddress {
// return []sdk.AccAddress{msg.Seller}
// }

View File

@ -14,19 +14,29 @@ func TestMsgPlaceBid_ValidateBasic(t *testing.T) {
msg MsgPlaceBid
expectPass bool
}{
{"normal", MsgPlaceBid{0, addr, sdk.NewInt64Coin("usdx", 10), sdk.NewInt64Coin("kava", 20)}, true},
{"emptyAddr", MsgPlaceBid{0, sdk.AccAddress{}, sdk.NewInt64Coin("usdx", 10), sdk.NewInt64Coin("kava", 20)}, false},
{"negativeBid", MsgPlaceBid{0, addr, sdk.Coin{"usdx", sdk.NewInt(-10)}, sdk.NewInt64Coin("kava", 20)}, false},
{"negativeLot", MsgPlaceBid{0, addr, sdk.NewInt64Coin("usdx", 10), sdk.Coin{"kava", sdk.NewInt(-20)}}, false},
{"zerocoins", MsgPlaceBid{0, addr, sdk.NewInt64Coin("usdx", 0), sdk.NewInt64Coin("kava", 0)}, true},
{"normal",
NewMsgPlaceBid(0, addr, c("token", 10)),
true},
{"emptyAddr",
NewMsgPlaceBid(0, sdk.AccAddress{}, c("token", 10)),
false},
{"negativeAmount",
NewMsgPlaceBid(0, addr, sdk.Coin{Denom: "token", Amount: sdk.NewInt(-10)}),
false},
{"zeroAmount",
NewMsgPlaceBid(0, addr, c("token", 0)),
true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.expectPass {
require.Nil(t, tc.msg.ValidateBasic())
require.NoError(t, tc.msg.ValidateBasic())
} else {
require.NotNil(t, tc.msg.ValidateBasic())
require.Error(t, tc.msg.ValidateBasic())
}
})
}
}
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }

View File

@ -3,96 +3,89 @@ package types
import (
"bytes"
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
)
// Defaults for auction params
const (
// DefaultMaxAuctionDuration max length of auction, roughly 2 days in blocks
DefaultMaxAuctionDuration EndTime = 2 * 24 * 3600 / 5
// DefaultBidDuration how long an auction gets extended when someone bids, roughly 3 hours in blocks
DefaultMaxBidDuration EndTime = 3 * 3600 / 5
// DefaultStartingAuctionID what the id of the first auction will be
DefaultStartingAuctionID ID = ID(0)
// DefaultMaxAuctionDuration max length of auction
DefaultMaxAuctionDuration time.Duration = 2 * 24 * time.Hour
// DefaultBidDuration how long an auction gets extended when someone bids
DefaultBidDuration time.Duration = 1 * time.Hour
)
// Parameter keys
var (
// ParamStoreKeyAuctionParams Param store key for auction params
KeyAuctionBidDuration = []byte("MaxBidDuration")
KeyAuctionDuration = []byte("MaxAuctionDuration")
KeyAuctionStartingID = []byte("StartingAuctionID")
// ParamStoreKeyParams Param store key for auction params
KeyAuctionBidDuration = []byte("BidDuration")
KeyAuctionDuration = []byte("MaxAuctionDuration")
)
var _ subspace.ParamSet = &AuctionParams{}
var _ subspace.ParamSet = &Params{}
// AuctionParams governance parameters for auction module
type AuctionParams struct {
MaxAuctionDuration EndTime `json:"max_auction_duration" yaml:"max_auction_duration"` // max length of auction, in blocks
MaxBidDuration EndTime `json:"max_bid_duration" yaml:"max_bid_duration"`
StartingAuctionID ID `json:"starting_auction_id" yaml:"starting_auction_id"`
// Params is the governance parameters for the auction module.
type Params struct {
MaxAuctionDuration time.Duration `json:"max_auction_duration" yaml:"max_auction_duration"` // max length of auction
BidDuration time.Duration `json:"bid_duration" yaml:"bid_duration"` // additional time added to the auction end time after each bid, capped by the expiry.
}
// NewAuctionParams creates a new AuctionParams object
func NewAuctionParams(maxAuctionDuration EndTime, bidDuration EndTime, startingID ID) AuctionParams {
return AuctionParams{
// NewParams returns a new Params object.
func NewParams(maxAuctionDuration time.Duration, bidDuration time.Duration) Params {
return Params{
MaxAuctionDuration: maxAuctionDuration,
MaxBidDuration: bidDuration,
StartingAuctionID: startingID,
BidDuration: bidDuration,
}
}
// DefaultAuctionParams default parameters for auctions
func DefaultAuctionParams() AuctionParams {
return NewAuctionParams(
// DefaultParams returns the default parameters for auctions.
func DefaultParams() Params {
return NewParams(
DefaultMaxAuctionDuration,
DefaultMaxBidDuration,
DefaultStartingAuctionID,
DefaultBidDuration,
)
}
// ParamKeyTable Key declaration for parameters
func ParamKeyTable() subspace.KeyTable {
return subspace.NewKeyTable().RegisterParamSet(&AuctionParams{})
return subspace.NewKeyTable().RegisterParamSet(&Params{})
}
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
// pairs of auth module's parameters.
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs.
// nolint
func (ap *AuctionParams) ParamSetPairs() subspace.ParamSetPairs {
func (p *Params) ParamSetPairs() subspace.ParamSetPairs {
return subspace.ParamSetPairs{
{KeyAuctionBidDuration, &ap.MaxBidDuration},
{KeyAuctionDuration, &ap.MaxAuctionDuration},
{KeyAuctionStartingID, &ap.StartingAuctionID},
{KeyAuctionBidDuration, &p.BidDuration},
{KeyAuctionDuration, &p.MaxAuctionDuration},
}
}
// Equal returns a boolean determining if two AuctionParams types are identical.
func (ap AuctionParams) Equal(ap2 AuctionParams) bool {
bz1 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&ap)
bz2 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&ap2)
// Equal returns a boolean determining if two Params types are identical.
func (p Params) Equal(p2 Params) bool {
bz1 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&p)
bz2 := ModuleCdc.MustMarshalBinaryLengthPrefixed(&p2)
return bytes.Equal(bz1, bz2)
}
// String implements stringer interface
func (ap AuctionParams) String() string {
func (p Params) String() string {
return fmt.Sprintf(`Auction Params:
Max Auction Duration: %s
Max Bid Duration: %s
Starting Auction ID: %v`, ap.MaxAuctionDuration, ap.MaxBidDuration, ap.StartingAuctionID)
Bid Duration: %s`, p.MaxAuctionDuration, p.BidDuration)
}
// Validate checks that the parameters have valid values.
func (ap AuctionParams) Validate() error {
if ap.MaxAuctionDuration <= EndTime(0) {
return fmt.Errorf("max auction duration should be positive, is %s", ap.MaxAuctionDuration)
func (p Params) Validate() error {
if p.BidDuration < 0 {
return sdk.ErrInternal("bid duration cannot be negative")
}
if ap.MaxBidDuration <= EndTime(0) {
return fmt.Errorf("bid duration should be positive, is %s", ap.MaxBidDuration)
if p.MaxAuctionDuration < 0 {
return sdk.ErrInternal("max auction duration cannot be negative")
}
if ap.StartingAuctionID <= ID(0) {
return fmt.Errorf("starting auction ID should be positive, is %v", ap.StartingAuctionID)
if p.BidDuration > p.MaxAuctionDuration {
return sdk.ErrInternal("bid duration param cannot be larger than max auction duration")
}
return nil
}

View File

@ -0,0 +1,38 @@
package types
import (
"github.com/stretchr/testify/require"
"testing"
"time"
)
func TestParams_Validate(t *testing.T) {
type fields struct {
}
testCases := []struct {
name string
MaxAuctionDuration time.Duration
BidDuration time.Duration
expectErr bool
}{
{"normal", 24 * time.Hour, 1 * time.Hour, false},
{"negativeBid", 24 * time.Hour, -1 * time.Hour, true},
{"negativeAuction", -24 * time.Hour, 1 * time.Hour, true},
{"bid>auction", 1 * time.Hour, 24 * time.Hour, true},
{"zeros", 0, 0, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
p := Params{
MaxAuctionDuration: tc.MaxAuctionDuration,
BidDuration: tc.BidDuration,
}
err := p.Validate()
if tc.expectErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@ -1,9 +0,0 @@
package types
// Go doesn't have a built in min function for integers :(
func min(a, b int64) int64 {
if a < b {
return a
}
return b
}

View File

@ -5,7 +5,6 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/liquidator/types"
)
@ -33,7 +32,7 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, paramstore subspace.Subs
// SeizeAndStartCollateralAuction pulls collateral out of a CDP and sells it in an auction for stable coin. Excess collateral goes to the original CDP owner.
// Known as Cat.bite in maker
// result: stable coin is transferred to module account, collateral is transferred from module account to buyer, (and any excess collateral is transferred to original CDP owner)
func (k Keeper) SeizeAndStartCollateralAuction(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string) (auction.ID, sdk.Error) {
func (k Keeper) SeizeAndStartCollateralAuction(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string) (uint64, sdk.Error) {
// Get CDP
cdp, found := k.cdpKeeper.GetCDP(ctx, owner, collateralDenom)
if !found {
@ -73,7 +72,7 @@ func (k Keeper) SeizeAndStartCollateralAuction(ctx sdk.Context, owner sdk.AccAdd
// StartDebtAuction sells off minted gov coin to raise set amounts of stable coin.
// Known as Vow.flop in maker
// result: minted gov coin moved to highest bidder, stable coin moved to moduleAccount
func (k Keeper) StartDebtAuction(ctx sdk.Context) (auction.ID, sdk.Error) {
func (k Keeper) StartDebtAuction(ctx sdk.Context) (uint64, sdk.Error) {
// Ensure amount of seized stable coin is 0 (ie Joy = 0)
stableCoins := k.bankKeeper.GetCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress()).AmountOf(k.cdpKeeper.GetStableDenom())
@ -107,7 +106,7 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context) (auction.ID, sdk.Error) {
// StartSurplusAuction sells off excess stable coin in exchange for gov coin, which is burned
// Known as Vow.flap in maker
// result: stable coin removed from module account (eventually to buyer), gov coin transferred to module account
// func (k Keeper) StartSurplusAuction(ctx sdk.Context) (auction.ID, sdk.Error) {
// func (k Keeper) StartSurplusAuction(ctx sdk.Context) (uint64, sdk.Error) {
// // TODO ensure seized debt is 0

View File

@ -3,7 +3,6 @@ package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/cdp"
)
@ -26,7 +25,7 @@ type BankKeeper interface {
// AuctionKeeper expected interface for the auction keeper
type AuctionKeeper interface {
StartForwardAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin) (auction.ID, sdk.Error)
StartReverseAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin) (auction.ID, sdk.Error)
StartForwardReverseAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin, sdk.AccAddress) (auction.ID, sdk.Error)
StartForwardAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin) (uint64, sdk.Error)
StartReverseAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin) (uint64, sdk.Error)
StartForwardReverseAuction(sdk.Context, sdk.AccAddress, sdk.Coin, sdk.Coin, sdk.AccAddress) (uint64, sdk.Error)
}