diff --git a/app/app.go b/app/app.go index b92d7a95..3d4daa72 100644 --- a/app/app.go +++ b/app/app.go @@ -271,9 +271,10 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, // During begin block slashing happens after distr.BeginBlocker so that // there is nothing left over in the validator fee pool, so as to keep the // CanWithdrawInvariant invariant. - app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, cdp.ModuleName) + // Auction.BeginBlocker will close out expired auctions and pay debt back to cdp. So it should be run before cdp.BeginBlocker which cancels out debt with stable and starts more auctions. + app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, auction.ModuleName, cdp.ModuleName) - app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName, auction.ModuleName) + app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName) // Note: genutils must occur after staking so that pools are properly // initialized with tokens from genesis accounts. diff --git a/x/auction/abci.go b/x/auction/abci.go index 402c5c12..f1b4cfad 100644 --- a/x/auction/abci.go +++ b/x/auction/abci.go @@ -4,8 +4,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// EndBlocker runs at the end of every block. -func EndBlocker(ctx sdk.Context, k Keeper) { +// BeginBlocker runs at the start of every block. +func BeginBlocker(ctx sdk.Context, k Keeper) { err := k.CloseExpiredAuctions(ctx) if err != nil { panic(err) diff --git a/x/auction/abci_test.go b/x/auction/abci_test.go index 7f69cab6..c7274b7b 100644 --- a/x/auction/abci_test.go +++ b/x/auction/abci_test.go @@ -15,7 +15,7 @@ import ( "github.com/kava-labs/kava/x/cdp" ) -func TestKeeper_EndBlocker(t *testing.T) { +func TestKeeper_BeginBlocker(t *testing.T) { // Setup _, addrs := app.GeneratePrivKeyAddressPairs(2) buyer := addrs[0] @@ -36,13 +36,14 @@ func TestKeeper_EndBlocker(t *testing.T) { ctx := tApp.NewContext(true, abci.Header{}) keeper := tApp.GetAuctionKeeper() + // Start an auction and place a bid auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights, c("debt", 40)) require.NoError(t, err) require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 30))) - // Run the endblocker, simulating a block time 1ns before auction expiry + // Run the beginblocker, simulating a block time 1ns before auction expiry preExpiryTime := ctx.BlockTime().Add(auction.DefaultBidDuration - 1) - auction.EndBlocker(ctx.WithBlockTime(preExpiryTime), keeper) + auction.BeginBlocker(ctx.WithBlockTime(preExpiryTime), keeper) // Check auction has not been closed yet _, found := keeper.GetAuction(ctx, auctionID) @@ -50,7 +51,7 @@ func TestKeeper_EndBlocker(t *testing.T) { // Run the endblocker, simulating a block time equal to auction expiry expiryTime := ctx.BlockTime().Add(auction.DefaultBidDuration) - auction.EndBlocker(ctx.WithBlockTime(expiryTime), keeper) + auction.BeginBlocker(ctx.WithBlockTime(expiryTime), keeper) // Check auction has been closed _, found = keeper.GetAuction(ctx, auctionID) diff --git a/x/auction/alias.go b/x/auction/alias.go index c4109000..1aed46a7 100644 --- a/x/auction/alias.go +++ b/x/auction/alias.go @@ -1,8 +1,8 @@ // nolint // autogenerated code using github.com/rigelrozanski/multitool // aliases generated for the following subdirectories: -// ALIASGEN: github.com/kava-labs/kava/x/auction/types/ -// ALIASGEN: github.com/kava-labs/kava/x/auction/keeper/ +// ALIASGEN: github.com/kava-labs/kava/x/auction/keeper +// ALIASGEN: github.com/kava-labs/kava/x/auction/types package auction import ( @@ -13,6 +13,7 @@ import ( const ( DefaultCodespace = types.DefaultCodespace CodeInvalidInitialAuctionID = types.CodeInvalidInitialAuctionID + CodeInvalidModulePermissions = types.CodeInvalidModulePermissions CodeUnrecognizedAuctionType = types.CodeUnrecognizedAuctionType CodeAuctionNotFound = types.CodeAuctionNotFound CodeAuctionHasNotExpired = types.CodeAuctionHasNotExpired @@ -21,60 +22,101 @@ const ( CodeInvalidLotDenom = types.CodeInvalidLotDenom CodeBidTooSmall = types.CodeBidTooSmall CodeBidTooLarge = types.CodeBidTooLarge + CodeLotTooSmall = types.CodeLotTooSmall CodeLotTooLarge = types.CodeLotTooLarge CodeCollateralAuctionIsInReversePhase = types.CodeCollateralAuctionIsInReversePhase CodeCollateralAuctionIsInForwardPhase = types.CodeCollateralAuctionIsInForwardPhase + EventTypeAuctionStart = types.EventTypeAuctionStart + EventTypeAuctionBid = types.EventTypeAuctionBid + EventTypeAuctionClose = types.EventTypeAuctionClose + AttributeValueCategory = types.AttributeValueCategory + AttributeKeyAuctionID = types.AttributeKeyAuctionID + AttributeKeyAuctionType = types.AttributeKeyAuctionType + AttributeKeyBidder = types.AttributeKeyBidder + AttributeKeyBidDenom = types.AttributeKeyBidDenom + AttributeKeyLotDenom = types.AttributeKeyLotDenom + AttributeKeyBidAmount = types.AttributeKeyBidAmount + AttributeKeyLotAmount = types.AttributeKeyLotAmount + AttributeKeyEndTime = types.AttributeKeyEndTime + DefaultNextAuctionID = types.DefaultNextAuctionID ModuleName = types.ModuleName StoreKey = types.StoreKey RouterKey = types.RouterKey DefaultParamspace = types.DefaultParamspace + QuerierRoute = types.QuerierRoute DefaultMaxAuctionDuration = types.DefaultMaxAuctionDuration DefaultBidDuration = types.DefaultBidDuration QueryGetAuction = types.QueryGetAuction - DefaultNextAuctionID = types.DefaultNextAuctionID + QueryGetAuctions = types.QueryGetAuctions + QueryGetParams = types.QueryGetParams ) var ( // functions aliases - NewSurplusAuction = types.NewSurplusAuction - NewDebtAuction = types.NewDebtAuction - NewCollateralAuction = types.NewCollateralAuction - NewWeightedAddresses = types.NewWeightedAddresses - RegisterCodec = types.RegisterCodec - NewGenesisState = types.NewGenesisState - DefaultGenesisState = types.DefaultGenesisState - GetAuctionKey = types.GetAuctionKey - GetAuctionByTimeKey = types.GetAuctionByTimeKey - Uint64FromBytes = types.Uint64FromBytes - Uint64ToBytes = types.Uint64ToBytes - NewMsgPlaceBid = types.NewMsgPlaceBid - NewParams = types.NewParams - DefaultParams = types.DefaultParams - ParamKeyTable = types.ParamKeyTable - NewKeeper = keeper.NewKeeper - NewQuerier = keeper.NewQuerier + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier + NewSurplusAuction = types.NewSurplusAuction + NewDebtAuction = types.NewDebtAuction + NewCollateralAuction = types.NewCollateralAuction + NewWeightedAddresses = types.NewWeightedAddresses + RegisterCodec = types.RegisterCodec + ErrInvalidInitialAuctionID = types.ErrInvalidInitialAuctionID + ErrInvalidModulePermissions = types.ErrInvalidModulePermissions + ErrUnrecognizedAuctionType = types.ErrUnrecognizedAuctionType + ErrAuctionNotFound = types.ErrAuctionNotFound + ErrAuctionHasNotExpired = types.ErrAuctionHasNotExpired + ErrAuctionHasExpired = types.ErrAuctionHasExpired + ErrInvalidBidDenom = types.ErrInvalidBidDenom + ErrInvalidLotDenom = types.ErrInvalidLotDenom + ErrBidTooSmall = types.ErrBidTooSmall + ErrBidTooLarge = types.ErrBidTooLarge + ErrLotTooSmall = types.ErrLotTooSmall + ErrLotTooLarge = types.ErrLotTooLarge + ErrCollateralAuctionIsInReversePhase = types.ErrCollateralAuctionIsInReversePhase + ErrCollateralAuctionIsInForwardPhase = types.ErrCollateralAuctionIsInForwardPhase + NewGenesisState = types.NewGenesisState + DefaultGenesisState = types.DefaultGenesisState + GetAuctionKey = types.GetAuctionKey + GetAuctionByTimeKey = types.GetAuctionByTimeKey + Uint64ToBytes = types.Uint64ToBytes + Uint64FromBytes = types.Uint64FromBytes + NewMsgPlaceBid = types.NewMsgPlaceBid + NewParams = types.NewParams + DefaultParams = types.DefaultParams + ParamKeyTable = types.ParamKeyTable + NewQueryAllAuctionParams = types.NewQueryAllAuctionParams + NewAuctionWithPhase = types.NewAuctionWithPhase // variable aliases + DistantFuture = types.DistantFuture ModuleCdc = types.ModuleCdc AuctionKeyPrefix = types.AuctionKeyPrefix AuctionByTimeKeyPrefix = types.AuctionByTimeKeyPrefix NextAuctionIDKey = types.NextAuctionIDKey - KeyAuctionBidDuration = types.KeyAuctionBidDuration - KeyAuctionDuration = types.KeyAuctionDuration + DefaultIncrement = types.DefaultIncrement + KeyBidDuration = types.KeyBidDuration + KeyMaxAuctionDuration = types.KeyMaxAuctionDuration + KeyIncrementSurplus = types.KeyIncrementSurplus + KeyIncrementDebt = types.KeyIncrementDebt + KeyIncrementCollateral = types.KeyIncrementCollateral ) type ( - Auction = types.Auction - BaseAuction = types.BaseAuction - SurplusAuction = types.SurplusAuction - DebtAuction = types.DebtAuction - CollateralAuction = types.CollateralAuction - WeightedAddresses = types.WeightedAddresses - SupplyKeeper = types.SupplyKeeper - GenesisAuctions = types.GenesisAuctions - GenesisAuction = types.GenesisAuction - GenesisState = types.GenesisState - MsgPlaceBid = types.MsgPlaceBid - Params = types.Params - Keeper = keeper.Keeper + Keeper = keeper.Keeper + Auction = types.Auction + Auctions = types.Auctions + BaseAuction = types.BaseAuction + SurplusAuction = types.SurplusAuction + DebtAuction = types.DebtAuction + CollateralAuction = types.CollateralAuction + WeightedAddresses = types.WeightedAddresses + SupplyKeeper = types.SupplyKeeper + GenesisAuction = types.GenesisAuction + GenesisAuctions = types.GenesisAuctions + GenesisState = types.GenesisState + MsgPlaceBid = types.MsgPlaceBid + Params = types.Params + QueryAuctionParams = types.QueryAuctionParams + QueryAllAuctionParams = types.QueryAllAuctionParams + AuctionWithPhase = types.AuctionWithPhase ) diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index e192a503..025c3df1 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -168,8 +168,14 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder if bid.Denom != a.Bid.Denom { return a, types.ErrInvalidBidDenom(k.codespace, bid.Denom, a.Bid.Denom) } - if !a.Bid.IsLT(bid) { - return a, types.ErrBidTooSmall(k.codespace, bid, a.Bid) + minNewBidAmt := a.Bid.Amount.Add( // new bids must be some % greater than old bid, and at least 1 larger to avoid replacing an old bid at no cost + sdk.MaxInt( + sdk.NewInt(1), + sdk.NewDecFromInt(a.Bid.Amount).Mul(k.GetParams(ctx).IncrementSurplus).RoundInt(), + ), + ) + if bid.Amount.LT(minNewBidAmt) { + return a, types.ErrBidTooSmall(k.codespace, bid, sdk.NewCoin(a.Bid.Denom, minNewBidAmt)) } // New bidder pays back old bidder @@ -225,8 +231,15 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc if a.IsReversePhase() { return a, types.ErrCollateralAuctionIsInReversePhase(k.codespace, a.ID) } - if !a.Bid.IsLT(bid) { - return a, types.ErrBidTooSmall(k.codespace, bid, a.Bid) + minNewBidAmt := a.Bid.Amount.Add( // new bids must be some % greater than old bid, and at least 1 larger to avoid replacing an old bid at no cost + sdk.MaxInt( + sdk.NewInt(1), + sdk.NewDecFromInt(a.Bid.Amount).Mul(k.GetParams(ctx).IncrementCollateral).RoundInt(), + ), + ) + minNewBidAmt = sdk.MinInt(minNewBidAmt, a.MaxBid.Amount) // allow new bids to hit MaxBid even though it may be less than the increment % + if bid.Amount.LT(minNewBidAmt) { + return a, types.ErrBidTooSmall(k.codespace, bid, sdk.NewCoin(a.Bid.Denom, minNewBidAmt)) } if a.MaxBid.IsLT(bid) { return a, types.ErrBidTooLarge(k.codespace, bid, a.MaxBid) @@ -294,8 +307,17 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc if !a.IsReversePhase() { return a, types.ErrCollateralAuctionIsInForwardPhase(k.codespace, a.ID) } - if !lot.IsLT(a.Lot) { - return a, types.ErrLotTooLarge(k.codespace, lot, a.Lot) + maxNewLotAmt := a.Lot.Amount.Sub( // new lot must be some % less than old lot, and at least 1 smaller to avoid replacing an old bid at no cost + sdk.MaxInt( + sdk.NewInt(1), + sdk.NewDecFromInt(a.Lot.Amount).Mul(k.GetParams(ctx).IncrementCollateral).RoundInt(), + ), + ) + if lot.Amount.GT(maxNewLotAmt) { + return a, types.ErrLotTooLarge(k.codespace, lot, sdk.NewCoin(a.Lot.Denom, maxNewLotAmt)) + } + if lot.IsNegative() { + return a, types.ErrLotTooSmall(k.codespace, lot, sdk.NewCoin(a.Lot.Denom, sdk.ZeroInt())) } // New bidder pays back old bidder @@ -351,8 +373,17 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac if lot.Denom != a.Lot.Denom { return a, types.ErrInvalidLotDenom(k.codespace, lot.Denom, a.Lot.Denom) } - if !lot.IsLT(a.Lot) { - return a, types.ErrLotTooLarge(k.codespace, lot, a.Lot) + maxNewLotAmt := a.Lot.Amount.Sub( // new lot must be some % less than old lot, and at least 1 smaller to avoid replacing an old bid at no cost + sdk.MaxInt( + sdk.NewInt(1), + sdk.NewDecFromInt(a.Lot.Amount).Mul(k.GetParams(ctx).IncrementDebt).RoundInt(), + ), + ) + if lot.Amount.GT(maxNewLotAmt) { + return a, types.ErrLotTooLarge(k.codespace, lot, sdk.NewCoin(a.Lot.Denom, maxNewLotAmt)) + } + if lot.IsNegative() { + return a, types.ErrLotTooSmall(k.codespace, lot, sdk.NewCoin(a.Lot.Denom, sdk.ZeroInt())) } // New bidder pays back old bidder diff --git a/x/auction/keeper/bidding_test.go b/x/auction/keeper/bidding_test.go index b4f3ed1f..40d2d164 100644 --- a/x/auction/keeper/bidding_test.go +++ b/x/auction/keeper/bidding_test.go @@ -19,11 +19,10 @@ import ( type AuctionType int const ( - Invalid AuctionType = 0 - Surplus AuctionType = 1 - Debt AuctionType = 2 - CollateralPhase1 AuctionType = 3 - CollateralPhase2 AuctionType = 4 + Invalid AuctionType = 0 + Surplus AuctionType = 1 + Debt AuctionType = 2 + Collateral AuctionType = 3 ) func TestAuctionBidding(t *testing.T) { @@ -47,14 +46,14 @@ func TestAuctionBidding(t *testing.T) { } type bidArgs struct { - bidder sdk.AccAddress - amount sdk.Coin - secondBidder sdk.AccAddress + bidder sdk.AccAddress + amount sdk.Coin } tests := []struct { name string auctionArgs auctionArgs + setupBids []bidArgs bidArgs bidArgs expectedError sdk.CodeType expectedEndTime time.Time @@ -65,17 +64,30 @@ func TestAuctionBidding(t *testing.T) { { "basic: auction doesn't exist", auctionArgs{Surplus, "", c("token1", 1), c("token2", 1), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, - bidArgs{buyer, c("token2", 10), nil}, + nil, + bidArgs{buyer, c("token2", 10)}, types.CodeAuctionNotFound, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 10), false, }, + { + "basic: closed auction", + auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, + nil, + bidArgs{buyer, c("token2", 10)}, + types.CodeAuctionHasExpired, + types.DistantFuture, + nil, + c("token2", 0), + false, + }, { "surplus: normal", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, - bidArgs{buyer, c("token2", 10), nil}, + nil, + bidArgs{buyer, c("token2", 10)}, sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), buyer, @@ -85,7 +97,8 @@ func TestAuctionBidding(t *testing.T) { { "surplus: second bidder", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, - bidArgs{buyer, c("token2", 10), secondBuyer}, + []bidArgs{{buyer, c("token2", 10)}}, + bidArgs{secondBuyer, c("token2", 11)}, sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), secondBuyer, @@ -95,27 +108,52 @@ func TestAuctionBidding(t *testing.T) { { "surplus: invalid bid denom", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, - bidArgs{buyer, c("badtoken", 10), nil}, + nil, + bidArgs{buyer, c("badtoken", 10)}, types.CodeInvalidBidDenom, + types.DistantFuture, + nil, // surplus auctions are created with initial bidder as a nil address + c("token2", 0), + false, + }, + { + "surplus: invalid bid (less than)", + auctionArgs{Surplus, modName, c("token1", 100), c("token2", 0), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, + []bidArgs{{buyer, c("token2", 100)}}, + bidArgs{buyer, c("token2", 99)}, + types.CodeBidTooSmall, someTime.Add(types.DefaultBidDuration), buyer, - c("token2", 10), + c("token2", 100), false, }, { "surplus: invalid bid (equal)", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 0), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, - bidArgs{buyer, c("token2", 0), nil}, + nil, + bidArgs{buyer, c("token2", 0)}, // min bid is technically 0 at default 5%, but it's capped at 1 + types.CodeBidTooSmall, + types.DistantFuture, + nil, // surplus auctions are created with initial bidder as a nil address + c("token2", 0), + false, + }, + { + "surplus: invalid bid (less than min increment)", + auctionArgs{Surplus, modName, c("token1", 100), c("token2", 0), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, + []bidArgs{{buyer, c("token2", 100)}}, + bidArgs{buyer, c("token2", 104)}, // min bid is 105 at default 5% types.CodeBidTooSmall, someTime.Add(types.DefaultBidDuration), buyer, - c("token2", 10), + c("token2", 100), false, }, { "debt: normal", - auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot - bidArgs{buyer, c("token1", 10), nil}, + auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot + nil, + bidArgs{buyer, c("token1", 10)}, sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), buyer, @@ -124,8 +162,9 @@ func TestAuctionBidding(t *testing.T) { }, { "debt: second bidder", - auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot - bidArgs{buyer, c("token1", 10), secondBuyer}, + auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot + []bidArgs{{buyer, c("token1", 10)}}, + bidArgs{secondBuyer, c("token1", 9)}, sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), secondBuyer, @@ -134,28 +173,53 @@ func TestAuctionBidding(t *testing.T) { }, { "debt: invalid lot denom", - auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot - bidArgs{buyer, c("badtoken", 10), nil}, + auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot + nil, + bidArgs{buyer, c("badtoken", 10)}, types.CodeInvalidLotDenom, - someTime.Add(types.DefaultBidDuration), - buyer, - c("token1", 20), + types.DistantFuture, + supply.NewModuleAddress(modName), + c("token2", 100), false, }, { "debt: invalid lot size (larger)", - auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, - bidArgs{buyer, c("token1", 21), nil}, + auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}}, + nil, + bidArgs{buyer, c("token1", 21)}, types.CodeLotTooLarge, - someTime.Add(types.DefaultBidDuration), - buyer, - c("token1", 20), + types.DistantFuture, + supply.NewModuleAddress(modName), + c("token2", 100), + false, + }, + { + "debt: invalid lot size (equal)", + auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}}, + nil, + bidArgs{buyer, c("token1", 20)}, + types.CodeLotTooLarge, + types.DistantFuture, + supply.NewModuleAddress(modName), + c("token2", 100), + false, + }, + { + "debt: invalid lot size (larger than min increment)", + auctionArgs{Debt, modName, c("token1", 60), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}}, + nil, + bidArgs{buyer, c("token1", 58)}, // max lot at default 5% is 57 + types.CodeLotTooLarge, + types.DistantFuture, + supply.NewModuleAddress(modName), + c("token2", 100), false, }, { "collateral [forward]: normal", - auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid - bidArgs{buyer, c("token2", 10), nil}, + auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + nil, + bidArgs{buyer, c("token2", 10)}, sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), buyer, @@ -164,8 +228,9 @@ func TestAuctionBidding(t *testing.T) { }, { "collateral [forward]: second bidder", - auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid - bidArgs{buyer, c("token2", 10), secondBuyer}, + auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + []bidArgs{{buyer, c("token2", 10)}}, + bidArgs{secondBuyer, c("token2", 11)}, sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), secondBuyer, @@ -174,18 +239,20 @@ func TestAuctionBidding(t *testing.T) { }, { "collateral [forward]: invalid bid denom", - auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid - bidArgs{buyer, c("badtoken", 10), nil}, + auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + nil, + bidArgs{buyer, c("badtoken", 10)}, types.CodeInvalidBidDenom, - someTime.Add(types.DefaultBidDuration), - buyer, - c("token2", 10), + types.DistantFuture, + nil, + c("token2", 0), false, }, { "collateral [forward]: invalid bid size (smaller)", - auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid - bidArgs{buyer, c("token2", 0), nil}, // lot, bid + auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + []bidArgs{{buyer, c("token2", 10)}}, + bidArgs{buyer, c("token2", 9)}, types.CodeBidTooSmall, someTime.Add(types.DefaultBidDuration), buyer, @@ -193,19 +260,54 @@ func TestAuctionBidding(t *testing.T) { false, }, { - "collateral [forward]: invalid bid size (greater than max)", - auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid - bidArgs{buyer, c("token2", 101), nil}, // lot, bid - types.CodeBidTooLarge, + "collateral [forward]: invalid bid size (equal)", + auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + nil, + bidArgs{buyer, c("token2", 0)}, + types.CodeBidTooSmall, + types.DistantFuture, + nil, + c("token2", 0), + false, + }, + { + "collateral [forward]: invalid bid size (less than min increment)", + auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + []bidArgs{{buyer, c("token2", 50)}}, + bidArgs{buyer, c("token2", 51)}, + types.CodeBidTooSmall, someTime.Add(types.DefaultBidDuration), buyer, - c("token2", 10), + c("token2", 50), + false, + }, + { + "collateral [forward]: less than min increment but equal to maxBid", + auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + []bidArgs{{buyer, c("token2", 99)}}, + bidArgs{buyer, c("token2", 100)}, // min bid at default 5% is 104 + sdk.CodeType(0), + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 100), + true, + }, + { + "collateral [forward]: invalid bid size (greater than max)", + auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + nil, + bidArgs{buyer, c("token2", 101)}, + types.CodeBidTooLarge, + types.DistantFuture, + nil, + c("token2", 0), false, }, { "collateral [reverse]: normal", - auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid - bidArgs{buyer, c("token1", 15), nil}, + auctionArgs{Collateral, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + []bidArgs{{buyer, c("token2", 50)}}, // put auction into reverse phase + bidArgs{buyer, c("token1", 15)}, sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), buyer, @@ -214,8 +316,9 @@ func TestAuctionBidding(t *testing.T) { }, { "collateral [reverse]: second bidder", - auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid - bidArgs{buyer, c("token1", 15), secondBuyer}, + auctionArgs{Collateral, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + []bidArgs{{buyer, c("token2", 50)}, {buyer, c("token1", 15)}}, // put auction into reverse phase, and add a reverse phase bid + bidArgs{secondBuyer, c("token1", 14)}, sdk.CodeType(0), someTime.Add(types.DefaultBidDuration), secondBuyer, @@ -224,28 +327,20 @@ func TestAuctionBidding(t *testing.T) { }, { "collateral [reverse]: invalid lot denom", - auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid - bidArgs{buyer, c("badtoken", 15), nil}, + auctionArgs{Collateral, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + []bidArgs{{buyer, c("token2", 50)}}, // put auction into reverse phase + bidArgs{buyer, c("badtoken", 15)}, types.CodeInvalidLotDenom, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 50), false, }, - { - "collateral [reverse]: invalid lot size (equal)", - auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid - bidArgs{buyer, c("token1", 20), nil}, - types.CodeLotTooLarge, - someTime.Add(types.DefaultBidDuration), - buyer, - c("token2", 50), - false, - }, { "collateral [reverse]: invalid lot size (greater)", - auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid - bidArgs{buyer, c("token1", 21), nil}, + auctionArgs{Collateral, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + []bidArgs{{buyer, c("token2", 50)}}, // put auction into reverse phase + bidArgs{buyer, c("token1", 21)}, types.CodeLotTooLarge, someTime.Add(types.DefaultBidDuration), buyer, @@ -253,13 +348,25 @@ func TestAuctionBidding(t *testing.T) { false, }, { - "basic: closed auction", - auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, - bidArgs{buyer, c("token2", 10), nil}, - types.CodeAuctionHasExpired, + "collateral [reverse]: invalid lot size (equal)", + auctionArgs{Collateral, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + []bidArgs{{buyer, c("token2", 50)}}, // put auction into reverse phase + bidArgs{buyer, c("token1", 20)}, + types.CodeLotTooLarge, someTime.Add(types.DefaultBidDuration), buyer, - c("token2", 10), + c("token2", 50), + false, + }, + { + "collateral [reverse]: invalid lot size (larger than min increment)", + auctionArgs{Collateral, modName, c("token1", 60), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + []bidArgs{{buyer, c("token2", 50)}}, // put auction into reverse phase + bidArgs{buyer, c("token1", 58)}, // max lot at default 5% is 57 + types.CodeLotTooLarge, + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 50), false, }, } @@ -283,6 +390,7 @@ func TestAuctionBidding(t *testing.T) { ) ctx := tApp.NewContext(false, abci.Header{}) keeper := tApp.GetAuctionKeeper() + bank := tApp.GetBankKeeper() // Start Auction var id uint64 @@ -292,56 +400,84 @@ func TestAuctionBidding(t *testing.T) { id, _ = keeper.StartSurplusAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid.Denom) case Debt: id, _ = keeper.StartDebtAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.bid, tc.auctionArgs.lot, tc.auctionArgs.debt) - case CollateralPhase1, CollateralPhase2: + case Collateral: id, _ = keeper.StartCollateralAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid, tc.auctionArgs.addresses, tc.auctionArgs.weights, tc.auctionArgs.debt) // seller, lot, maxBid, otherPerson - // Move CollateralAuction to debt phase by placing max bid - if tc.auctionArgs.auctionType == CollateralPhase2 { - err = keeper.PlaceBid(ctx, id, tc.bidArgs.bidder, tc.auctionArgs.bid) - require.NoError(t, err) - } default: t.Fail() } + // Place setup bids + for _, b := range tc.setupBids { + require.NoError(t, keeper.PlaceBid(ctx, id, b.bidder, b.amount)) + } // Close the auction early to test late bidding (if applicable) if strings.Contains(tc.name, "closed") { ctx = ctx.WithBlockTime(types.DistantFuture.Add(1)) } + // Store some state for use in checks + oldAuction, found := keeper.GetAuction(ctx, id) + var oldBidder sdk.AccAddress + if found { + oldBidder = oldAuction.GetBidder() + } + oldBidderOldCoins := bank.GetCoins(ctx, oldBidder) + newBidderOldCoins := bank.GetCoins(ctx, tc.bidArgs.bidder) + // Place bid on auction err = keeper.PlaceBid(ctx, id, tc.bidArgs.bidder, tc.bidArgs.amount) - // Place second bid from new bidder - if tc.bidArgs.secondBidder != nil { - // Set bid increase/decrease based on auction type, phase - var secondBid sdk.Coin - switch tc.auctionArgs.auctionType { - case Surplus, CollateralPhase1: - secondBid = tc.bidArgs.amount.Add(c(tc.bidArgs.amount.Denom, 1)) - case Debt, CollateralPhase2: - secondBid = tc.bidArgs.amount.Sub(c(tc.bidArgs.amount.Denom, 1)) - default: - t.Fail() - } - // Place the second bid - err2 := keeper.PlaceBid(ctx, id, tc.bidArgs.secondBidder, secondBid) - require.NoError(t, err2) - } - // Check success/failure if tc.expectpass { require.Nil(t, err) - // Get auction from store - auction, found := keeper.GetAuction(ctx, id) + // Check auction was found + newAuction, found := keeper.GetAuction(ctx, id) require.True(t, found) // Check auction values - require.Equal(t, modName, auction.GetInitiator()) - require.Equal(t, tc.expectedBidder, auction.GetBidder()) - require.Equal(t, tc.expectedBid, auction.GetBid()) - require.Equal(t, tc.expectedEndTime, auction.GetEndTime()) + require.Equal(t, modName, newAuction.GetInitiator()) + require.Equal(t, tc.expectedBidder, newAuction.GetBidder()) + require.Equal(t, tc.expectedBid, newAuction.GetBid()) + require.Equal(t, tc.expectedEndTime, newAuction.GetEndTime()) + + // Check coins have moved between bidder and previous bidder + bidAmt := tc.bidArgs.amount + switch tc.auctionArgs.auctionType { + case Debt: + bidAmt = oldAuction.GetBid() + case Collateral: + collatAuction, _ := oldAuction.(types.CollateralAuction) + if collatAuction.IsReversePhase() { + bidAmt = oldAuction.GetBid() + } + } + if oldBidder.Equals(tc.bidArgs.bidder) { // same bidder + require.Equal(t, newBidderOldCoins.Sub(cs(bidAmt.Sub(oldAuction.GetBid()))), bank.GetCoins(ctx, tc.bidArgs.bidder)) + } else { + require.Equal(t, cs(newBidderOldCoins.Sub(cs(bidAmt))...), bank.GetCoins(ctx, tc.bidArgs.bidder)) // wrapping in cs() to avoid comparing nil and empty coins + if oldBidder.Equals(supply.NewModuleAddress(oldAuction.GetInitiator())) { // handle checking debt coins for case debt auction has had no bids placed yet TODO make this less confusing + require.Equal(t, cs(oldBidderOldCoins.Add(cs(oldAuction.GetBid()))...).Add(cs(c("debt", oldAuction.GetBid().Amount.Int64()))), bank.GetCoins(ctx, oldBidder)) + } else { + require.Equal(t, cs(oldBidderOldCoins.Add(cs(oldAuction.GetBid()))...), bank.GetCoins(ctx, oldBidder)) + } + } + } else { // Check expected error code type + require.NotNil(t, err, "PlaceBid did not return an error") // catch nil values before they cause a panic below require.Equal(t, tc.expectedError, err.Result().Code) + + // Check auction values + newAuction, found := keeper.GetAuction(ctx, id) + if found { + require.Equal(t, modName, newAuction.GetInitiator()) + require.Equal(t, tc.expectedBidder, newAuction.GetBidder()) + require.Equal(t, tc.expectedBid, newAuction.GetBid()) + require.Equal(t, tc.expectedEndTime, newAuction.GetEndTime()) + } + + // Check coins have not moved + require.Equal(t, newBidderOldCoins, bank.GetCoins(ctx, tc.bidArgs.bidder)) + require.Equal(t, oldBidderOldCoins, bank.GetCoins(ctx, oldBidder)) } }) } diff --git a/x/auction/module.go b/x/auction/module.go index 07f5a953..f34b19f5 100644 --- a/x/auction/module.go +++ b/x/auction/module.go @@ -149,10 +149,11 @@ func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { } // BeginBlock module begin-block -func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} +func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { + BeginBlocker(ctx, am.keeper) +} // EndBlock module end-block -func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { - EndBlocker(ctx, am.keeper) +func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { return []abci.ValidatorUpdate{} } diff --git a/x/auction/spec/02_state.md b/x/auction/spec/02_state.md index 306665f9..a85a8cc3 100644 --- a/x/auction/spec/02_state.md +++ b/x/auction/spec/02_state.md @@ -9,6 +9,9 @@ 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. + IncrementSurplus sdk.Dec `json:"increment_surplus" yaml:"increment_surplus"` // percentage change (of auc.Bid) required for a new bid on a surplus auction + IncrementDebt sdk.Dec `json:"increment_debt" yaml:"increment_debt"` // percentage change (of auc.Lot) required for a new bid on a debt auction + IncrementCollateral sdk.Dec `json:"increment_collateral" yaml:"increment_collateral"` // percentage change (of auc.Bid or auc.Lot) required for a new bid on a collateral auction } ``` diff --git a/x/auction/spec/04_events.md b/x/auction/spec/04_events.md index 39ec4053..36232b2f 100644 --- a/x/auction/spec/04_events.md +++ b/x/auction/spec/04_events.md @@ -25,7 +25,7 @@ The `x/auction` module emits the following events: | message | module | auction | | message | sender | {sender address} | -## EndBlock +## BeginBlock | Type | Attribute Key | Attribute Value | |---------------|---------------|-----------------| diff --git a/x/auction/spec/05_params.md b/x/auction/spec/05_params.md index 9a108303..c5ce5c52 100644 --- a/x/auction/spec/05_params.md +++ b/x/auction/spec/05_params.md @@ -2,7 +2,10 @@ The auction module contains the following parameters: -| Key | Type | Example | -| ------------------ | ---------------------- | -----------| -| MaxAuctionDuration | string (time.Duration) | "48h0m0s" | -| BidDuration | string (time.Duration) | "3h0m0s" | +| Key | Type | Example | Description | +|---------------------|------------------------|------------------------|---------------------------------------------------------------------------------------| +| MaxAuctionDuration | string (time.Duration) | "48h0m0s" | | +| BidDuration | string (time.Duration) | "3h0m0s" | | +| IncrementSurplus | string (dec) | "0.050000000000000000" | percentage change in bid required for a new bid on a surplus auction | +| IncrementDebt | string (dec) | "0.050000000000000000" | percentage change in lot required for a new bid on a debt auction | +| IncrementCollateral | string (dec) | "0.050000000000000000" | percentage change in either bid or lot required for a new bid on a collateral auction | diff --git a/x/auction/spec/06_end_block.md b/x/auction/spec/06_begin_block.md similarity index 68% rename from x/auction/spec/06_end_block.md rename to x/auction/spec/06_begin_block.md index 43694df9..c673159a 100644 --- a/x/auction/spec/06_end_block.md +++ b/x/auction/spec/06_begin_block.md @@ -1,6 +1,6 @@ -# End Block +# Begin Block -At the end of each block, auctions that have reached `EndTime` are closed. The logic to close auctions is as follows: +At the start of each block, auctions that have reached `EndTime` are closed. The logic to close auctions is as follows: ```go var expiredAuctions []uint64 diff --git a/x/auction/spec/README.md b/x/auction/spec/README.md index 8c2ee2b5..32ae71d1 100644 --- a/x/auction/spec/README.md +++ b/x/auction/spec/README.md @@ -6,7 +6,7 @@ 3. **[Messages](03_messages.md)** 4. **[Events](04_events.md)** 5. **[Params](05_params.md)** -6. **[EndBlock](06_end_block.md)** +6. **[BeginBlock](06_begin_block.md)** ## Abstract diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 87694c31..6a84f4bb 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -61,7 +61,7 @@ func (a BaseAuction) GetBid() sdk.Coin { return a.Bid } // GetEndTime is a getter for auction end time. func (a BaseAuction) GetEndTime() time.Time { return a.EndTime } -// GetType returns theauction type. Used to identify auctions in event attributes. +// GetType returns the auction type. Used to identify auctions in event attributes. func (a BaseAuction) GetType() string { return "base" } // Validate verifies that the auction end time is before max end time diff --git a/x/auction/types/errors.go b/x/auction/types/errors.go index d281f3aa..85e6b718 100644 --- a/x/auction/types/errors.go +++ b/x/auction/types/errors.go @@ -21,9 +21,10 @@ const ( CodeInvalidLotDenom sdk.CodeType = 8 CodeBidTooSmall sdk.CodeType = 9 CodeBidTooLarge sdk.CodeType = 10 - CodeLotTooLarge sdk.CodeType = 11 - CodeCollateralAuctionIsInReversePhase sdk.CodeType = 12 - CodeCollateralAuctionIsInForwardPhase sdk.CodeType = 13 + CodeLotTooSmall sdk.CodeType = 11 + CodeLotTooLarge sdk.CodeType = 12 + CodeCollateralAuctionIsInReversePhase sdk.CodeType = 13 + CodeCollateralAuctionIsInForwardPhase sdk.CodeType = 14 ) // ErrInvalidInitialAuctionID error for when the initial auction ID hasn't been set @@ -66,9 +67,9 @@ func ErrInvalidLotDenom(codespace sdk.CodespaceType, lotDenom string, auctionLot return sdk.NewError(codespace, CodeInvalidLotDenom, fmt.Sprintf("lot denom %s doesn't match auction lot denom %s", lotDenom, auctionLotDenom)) } -// ErrBidTooSmall error for when bid is not greater than auction's last bid -func ErrBidTooSmall(codespace sdk.CodespaceType, bid sdk.Coin, lastBid sdk.Coin) sdk.Error { - return sdk.NewError(codespace, CodeBidTooSmall, fmt.Sprintf("bid %s is not greater than auction's last bid %s", bid.String(), lastBid.String())) +// ErrBidTooSmall error for when bid is not greater than auction's min bid amount +func ErrBidTooSmall(codespace sdk.CodespaceType, bid sdk.Coin, minBid sdk.Coin) sdk.Error { + return sdk.NewError(codespace, CodeBidTooSmall, fmt.Sprintf("bid %s is not greater than auction's min new bid amount %s", bid.String(), minBid.String())) } // ErrBidTooLarge error for when bid is larger than auction's maximum allowed bid @@ -76,9 +77,14 @@ func ErrBidTooLarge(codespace sdk.CodespaceType, bid sdk.Coin, maxBid sdk.Coin) return sdk.NewError(codespace, CodeBidTooLarge, fmt.Sprintf("bid %s is greater than auction's max bid %s", bid.String(), maxBid.String())) } -// ErrLotTooLarge error for when lot is not smaller than auction's last lot -func ErrLotTooLarge(codespace sdk.CodespaceType, lot sdk.Coin, lastLot sdk.Coin) sdk.Error { - return sdk.NewError(codespace, CodeLotTooLarge, fmt.Sprintf("lot %s is not less than auction's last lot %s", lot.String(), lastLot.String())) +// ErrLotToosmall error for when lot is less than zero +func ErrLotTooSmall(codespace sdk.CodespaceType, lot sdk.Coin, minLot sdk.Coin) sdk.Error { + return sdk.NewError(codespace, CodeLotTooSmall, fmt.Sprintf("lot %s is not greater than auction's min new lot amount %s", lot.String(), minLot.String())) +} + +// ErrLotTooLarge error for when lot is not smaller than auction's max new lot amount +func ErrLotTooLarge(codespace sdk.CodespaceType, lot sdk.Coin, maxLot sdk.Coin) sdk.Error { + return sdk.NewError(codespace, CodeLotTooLarge, fmt.Sprintf("lot %s is greater than auction's max new lot amount %s", lot.String(), maxLot.String())) } // ErrCollateralAuctionIsInReversePhase error for when attempting to place a forward bid on a collateral auction in reverse phase diff --git a/x/auction/types/params.go b/x/auction/types/params.go index cbea5b11..74f2037a 100644 --- a/x/auction/types/params.go +++ b/x/auction/types/params.go @@ -17,26 +17,36 @@ const ( DefaultBidDuration time.Duration = 1 * time.Hour ) -// Parameter keys var ( + // DefaultIncrement is the smallest percent change a new bid must have from the old one + DefaultIncrement sdk.Dec = sdk.MustNewDecFromStr("0.05") // ParamStoreKeyParams Param store key for auction params - KeyAuctionBidDuration = []byte("BidDuration") - KeyAuctionDuration = []byte("MaxAuctionDuration") + KeyBidDuration = []byte("BidDuration") + KeyMaxAuctionDuration = []byte("MaxAuctionDuration") + KeyIncrementSurplus = []byte("IncrementSurplus") + KeyIncrementDebt = []byte("IncrementDebt") + KeyIncrementCollateral = []byte("IncrementCollateral") ) var _ subspace.ParamSet = &Params{} // 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. + 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. + IncrementSurplus sdk.Dec `json:"increment_surplus" yaml:"increment_surplus"` // percentage change (of auc.Bid) required for a new bid on a surplus auction + IncrementDebt sdk.Dec `json:"increment_debt" yaml:"increment_debt"` // percentage change (of auc.Lot) required for a new bid on a debt auction + IncrementCollateral sdk.Dec `json:"increment_collateral" yaml:"increment_collateral"` // percentage change (of auc.Bid or auc.Lot) required for a new bid on a collateral auction } // NewParams returns a new Params object. -func NewParams(maxAuctionDuration time.Duration, bidDuration time.Duration) Params { +func NewParams(maxAuctionDuration, bidDuration time.Duration, incrementSurplus, incrementDebt, incrementCollateral sdk.Dec) Params { return Params{ - MaxAuctionDuration: maxAuctionDuration, - BidDuration: bidDuration, + MaxAuctionDuration: maxAuctionDuration, + BidDuration: bidDuration, + IncrementSurplus: incrementSurplus, + IncrementDebt: incrementDebt, + IncrementCollateral: incrementCollateral, } } @@ -45,6 +55,9 @@ func DefaultParams() Params { return NewParams( DefaultMaxAuctionDuration, DefaultBidDuration, + DefaultIncrement, + DefaultIncrement, + DefaultIncrement, ) } @@ -56,8 +69,11 @@ func ParamKeyTable() subspace.KeyTable { // ParamSetPairs implements the ParamSet interface and returns all the key/value pairs. func (p *Params) ParamSetPairs() subspace.ParamSetPairs { return subspace.ParamSetPairs{ - {Key: KeyAuctionBidDuration, Value: &p.BidDuration}, - {Key: KeyAuctionDuration, Value: &p.MaxAuctionDuration}, + {Key: KeyBidDuration, Value: &p.BidDuration}, + {Key: KeyMaxAuctionDuration, Value: &p.MaxAuctionDuration}, + {Key: KeyIncrementSurplus, Value: &p.IncrementSurplus}, + {Key: KeyIncrementDebt, Value: &p.IncrementDebt}, + {Key: KeyIncrementCollateral, Value: &p.IncrementCollateral}, } } @@ -72,7 +88,11 @@ func (p Params) Equal(p2 Params) bool { func (p Params) String() string { return fmt.Sprintf(`Auction Params: Max Auction Duration: %s - Bid Duration: %s`, p.MaxAuctionDuration, p.BidDuration) + Bid Duration: %s + Increment Surplus: %s + Increment Debt: %s + Increment Collateral: %s`, + p.MaxAuctionDuration, p.BidDuration, p.IncrementSurplus, p.IncrementDebt, p.IncrementCollateral) } // Validate checks that the parameters have valid values. @@ -86,5 +106,17 @@ func (p Params) Validate() error { if p.BidDuration > p.MaxAuctionDuration { return sdk.ErrInternal("bid duration param cannot be larger than max auction duration") } + if p.IncrementSurplus == (sdk.Dec{}) || p.IncrementDebt == (sdk.Dec{}) || p.IncrementCollateral == (sdk.Dec{}) { + return sdk.ErrInternal("auction increment values cannot be nil") + } + if p.IncrementSurplus.IsNegative() { + return sdk.ErrInternal("surplus auction increment cannot be less than zero") + } + if p.IncrementDebt.IsNegative() { + return sdk.ErrInternal("debt auction increment cannot be less than zero") + } + if p.IncrementCollateral.IsNegative() { + return sdk.ErrInternal("collateral auction increment cannot be less than zero") + } return nil } diff --git a/x/auction/types/params_test.go b/x/auction/types/params_test.go index 0f642875..253a180a 100644 --- a/x/auction/types/params_test.go +++ b/x/auction/types/params_test.go @@ -1,33 +1,101 @@ package types import ( - "github.com/stretchr/testify/require" "testing" "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" ) func TestParams_Validate(t *testing.T) { type fields struct { } testCases := []struct { - name string - MaxAuctionDuration time.Duration - BidDuration time.Duration - expectErr bool + name string + Params + 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}, + { + "normal", + DefaultParams(), + false, + }, + { + "negativeBid", + Params{ + MaxAuctionDuration: 24 * time.Hour, + BidDuration: -1 * time.Hour, + IncrementSurplus: d("0.05"), + IncrementDebt: d("0.05"), + IncrementCollateral: d("0.05"), + }, + true, + }, + { + "negativeAuction", + Params{ + MaxAuctionDuration: -24 * time.Hour, + BidDuration: 1 * time.Hour, + IncrementSurplus: d("0.05"), + IncrementDebt: d("0.05"), + IncrementCollateral: d("0.05"), + }, + true, + }, + { + "bid>auction", + Params{ + MaxAuctionDuration: 1 * time.Hour, + BidDuration: 24 * time.Hour, + IncrementSurplus: d("0.05"), + IncrementDebt: d("0.05"), + IncrementCollateral: d("0.05"), + }, + true, + }, + { + "negative increment surplus", + Params{ + MaxAuctionDuration: 24 * time.Hour, + BidDuration: 1 * time.Hour, + IncrementSurplus: d("-0.05"), + IncrementDebt: d("0.05"), + IncrementCollateral: d("0.05"), + }, + true, + }, + { + "negative increment debt", + Params{ + MaxAuctionDuration: 24 * time.Hour, + BidDuration: 1 * time.Hour, + IncrementSurplus: d("0.05"), + IncrementDebt: d("-0.05"), + IncrementCollateral: d("0.05"), + }, + true, + }, + { + "negative increment collateral", + Params{ + MaxAuctionDuration: 24 * time.Hour, + BidDuration: 1 * time.Hour, + IncrementSurplus: d("0.05"), + IncrementDebt: d("0.05"), + IncrementCollateral: d("-0.05"), + }, + true, + }, + { + "zero value", + Params{}, + true, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - p := Params{ - MaxAuctionDuration: tc.MaxAuctionDuration, - BidDuration: tc.BidDuration, - } - err := p.Validate() + err := tc.Params.Validate() if tc.expectErr { require.Error(t, err) } else { @@ -36,3 +104,5 @@ func TestParams_Validate(t *testing.T) { }) } } + +func d(amount string) sdk.Dec { return sdk.MustNewDecFromStr(amount) }