mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-24 15:25:18 +00:00
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:
parent
c5db0ff680
commit
e1c11d411a
26
app/app.go
26
app/app.go
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
err := k.CloseExpiredAuctions(ctx)
|
||||
if err != nil {
|
||||
panic(err) // TODO how should errors be handled here?
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
@ -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)}
|
||||
}
|
||||
|
@ -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
|
||||
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
|
||||
NewAuctionParams = types.NewAuctionParams
|
||||
DefaultAuctionParams = types.DefaultAuctionParams
|
||||
NewParams = types.NewParams
|
||||
DefaultParams = types.DefaultParams
|
||||
ParamKeyTable = types.ParamKeyTable
|
||||
NewKeeper = keeper.NewKeeper
|
||||
NewQuerier = keeper.NewQuerier
|
||||
|
||||
// variable aliases
|
||||
ModuleCdc = types.ModuleCdc
|
||||
AuctionKeyPrefix = types.AuctionKeyPrefix
|
||||
AuctionByTimeKeyPrefix = types.AuctionByTimeKeyPrefix
|
||||
NextAuctionIDKey = types.NextAuctionIDKey
|
||||
KeyAuctionBidDuration = types.KeyAuctionBidDuration
|
||||
KeyAuctionDuration = types.KeyAuctionDuration
|
||||
KeyAuctionStartingID = types.KeyAuctionStartingID
|
||||
)
|
||||
|
||||
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
|
||||
SurplusAuction = types.SurplusAuction
|
||||
DebtAuction = types.DebtAuction
|
||||
CollateralAuction = types.CollateralAuction
|
||||
WeightedAddresses = types.WeightedAddresses
|
||||
SupplyKeeper = types.SupplyKeeper
|
||||
Auctions = types.Auctions
|
||||
GenesisState = types.GenesisState
|
||||
MsgPlaceBid = types.MsgPlaceBid
|
||||
AuctionParams = types.AuctionParams
|
||||
Params = types.Params
|
||||
QueryResAuctions = types.QueryResAuctions
|
||||
Keeper = keeper.Keeper
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
386
x/auction/keeper/auctions.go
Normal file
386
x/auction/keeper/auctions.go
Normal 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
|
||||
}
|
246
x/auction/keeper/auctions_test.go
Normal file
246
x/auction/keeper/auctions_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
24
x/auction/keeper/integration_test.go
Normal file
24
x/auction/keeper/integration_test.go
Normal 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)}
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
defer iterator.Close()
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
var auction types.Auction
|
||||
k.cdc.MustUnmarshalBinaryBare(auctionBytes, &auction)
|
||||
return auction
|
||||
k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &auction)
|
||||
|
||||
if cb(auction) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
69
x/auction/keeper/math.go
Normal 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
|
||||
}
|
36
x/auction/keeper/math_test.go
Normal file
36
x/auction/keeper/math_test.go
Normal 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
|
||||
}
|
@ -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, ¶ms)
|
||||
}
|
||||
|
||||
// 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, ¶ms)
|
||||
return
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
@ -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) {}
|
||||
|
||||
|
9
x/auction/spec/01_concepts.md
Normal file
9
x/auction/spec/01_concepts.md
Normal 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.
|
75
x/auction/spec/02_state.md
Normal file
75
x/auction/spec/02_state.md
Normal 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
|
||||
}
|
||||
```
|
32
x/auction/spec/03_messages.md
Normal file
32
x/auction/spec/03_messages.md
Normal 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`
|
5
x/auction/spec/04_events.md
Normal file
5
x/auction/spec/04_events.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Events
|
||||
|
||||
<!--
|
||||
TODO: Add events for auction_start, auction_end, auction_bid
|
||||
-->
|
8
x/auction/spec/05_params.md
Normal file
8
x/auction/spec/05_params.md
Normal 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" |
|
18
x/auction/spec/06_end_block.md
Normal file
18
x/auction/spec/06_end_block.md
Normal 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
13
x/auction/spec/README.md
Normal 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.
|
@ -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
|
||||
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,
|
||||
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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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}
|
||||
// }
|
||||
|
@ -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) }
|
||||
|
@ -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")
|
||||
// ParamStoreKeyParams Param store key for auction params
|
||||
KeyAuctionBidDuration = []byte("BidDuration")
|
||||
KeyAuctionDuration = []byte("MaxAuctionDuration")
|
||||
KeyAuctionStartingID = []byte("StartingAuctionID")
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
38
x/auction/types/params_test.go
Normal file
38
x/auction/types/params_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user