x/auction: audit revisions (#497)

Auction audit revisions
This commit is contained in:
Federico Kunze 2020-05-13 09:31:36 -04:00 committed by GitHub
commit 6dedc1520a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 329 additions and 322 deletions

View File

@ -1,109 +1,105 @@
package auction package auction
// DO NOT EDIT - generated by aliasgen tool (github.com/rhuairahrighairidh/aliasgen)
import ( import (
"github.com/kava-labs/kava/x/auction/keeper" "github.com/kava-labs/kava/x/auction/keeper"
"github.com/kava-labs/kava/x/auction/types" "github.com/kava-labs/kava/x/auction/types"
) )
// nolint
// autogenerated code using github.com/rigelrozanski/multitool
// aliases generated for the following subdirectories:
// ALIASGEN: github.com/kava-labs/kava/x/auction/keeper
// ALIASGEN: github.com/kava-labs/kava/x/auction/types
const ( const (
EventTypeAuctionStart = types.EventTypeAuctionStart
EventTypeAuctionBid = types.EventTypeAuctionBid
EventTypeAuctionClose = types.EventTypeAuctionClose
AttributeValueCategory = types.AttributeValueCategory
AttributeKeyAuctionID = types.AttributeKeyAuctionID AttributeKeyAuctionID = types.AttributeKeyAuctionID
AttributeKeyAuctionType = types.AttributeKeyAuctionType AttributeKeyAuctionType = types.AttributeKeyAuctionType
AttributeKeyBid = types.AttributeKeyBid
AttributeKeyBidder = types.AttributeKeyBidder AttributeKeyBidder = types.AttributeKeyBidder
AttributeKeyBidDenom = types.AttributeKeyBidDenom AttributeKeyCloseBlock = types.AttributeKeyCloseBlock
AttributeKeyLotDenom = types.AttributeKeyLotDenom
AttributeKeyBidAmount = types.AttributeKeyBidAmount
AttributeKeyLotAmount = types.AttributeKeyLotAmount
AttributeKeyEndTime = types.AttributeKeyEndTime AttributeKeyEndTime = types.AttributeKeyEndTime
DefaultNextAuctionID = types.DefaultNextAuctionID AttributeKeyLot = types.AttributeKeyLot
ModuleName = types.ModuleName AttributeKeyMaxBid = types.AttributeKeyMaxBid
StoreKey = types.StoreKey AttributeValueCategory = types.AttributeValueCategory
RouterKey = types.RouterKey
DefaultParamspace = types.DefaultParamspace
QuerierRoute = types.QuerierRoute
DefaultMaxAuctionDuration = types.DefaultMaxAuctionDuration
DefaultBidDuration = types.DefaultBidDuration DefaultBidDuration = types.DefaultBidDuration
DefaultMaxAuctionDuration = types.DefaultMaxAuctionDuration
DefaultNextAuctionID = types.DefaultNextAuctionID
DefaultParamspace = types.DefaultParamspace
EventTypeAuctionBid = types.EventTypeAuctionBid
EventTypeAuctionClose = types.EventTypeAuctionClose
EventTypeAuctionStart = types.EventTypeAuctionStart
ModuleName = types.ModuleName
QuerierRoute = types.QuerierRoute
QueryGetAuction = types.QueryGetAuction QueryGetAuction = types.QueryGetAuction
QueryGetAuctions = types.QueryGetAuctions QueryGetAuctions = types.QueryGetAuctions
QueryGetParams = types.QueryGetParams QueryGetParams = types.QueryGetParams
RouterKey = types.RouterKey
StoreKey = types.StoreKey
) )
var ( var (
// functions aliases // function aliases
NewKeeper = keeper.NewKeeper ModuleAccountInvariants = keeper.ModuleAccountInvariants
NewQuerier = keeper.NewQuerier NewKeeper = keeper.NewKeeper
RegisterInvariants = keeper.RegisterInvariants NewQuerier = keeper.NewQuerier
NewSurplusAuction = types.NewSurplusAuction RegisterInvariants = keeper.RegisterInvariants
NewDebtAuction = types.NewDebtAuction ValidAuctionInvariant = keeper.ValidAuctionInvariant
NewCollateralAuction = types.NewCollateralAuction ValidIndexInvariant = keeper.ValidIndexInvariant
NewWeightedAddresses = types.NewWeightedAddresses DefaultGenesisState = types.DefaultGenesisState
RegisterCodec = types.RegisterCodec DefaultParams = types.DefaultParams
ErrInvalidInitialAuctionID = types.ErrInvalidInitialAuctionID GetAuctionByTimeKey = types.GetAuctionByTimeKey
ErrInvalidModulePermissions = types.ErrInvalidModulePermissions GetAuctionKey = types.GetAuctionKey
ErrUnrecognizedAuctionType = types.ErrUnrecognizedAuctionType NewAuctionWithPhase = types.NewAuctionWithPhase
ErrAuctionNotFound = types.ErrAuctionNotFound NewCollateralAuction = types.NewCollateralAuction
ErrAuctionHasNotExpired = types.ErrAuctionHasNotExpired NewDebtAuction = types.NewDebtAuction
ErrAuctionHasExpired = types.ErrAuctionHasExpired NewGenesisState = types.NewGenesisState
ErrInvalidBidDenom = types.ErrInvalidBidDenom NewMsgPlaceBid = types.NewMsgPlaceBid
ErrInvalidLotDenom = types.ErrInvalidLotDenom NewParams = types.NewParams
ErrBidTooSmall = types.ErrBidTooSmall NewQueryAllAuctionParams = types.NewQueryAllAuctionParams
ErrBidTooLarge = types.ErrBidTooLarge NewSurplusAuction = types.NewSurplusAuction
ErrLotTooSmall = types.ErrLotTooSmall NewWeightedAddresses = types.NewWeightedAddresses
ErrLotTooLarge = types.ErrLotTooLarge ParamKeyTable = types.ParamKeyTable
ErrCollateralAuctionIsInReversePhase = types.ErrCollateralAuctionIsInReversePhase RegisterCodec = types.RegisterCodec
ErrCollateralAuctionIsInForwardPhase = types.ErrCollateralAuctionIsInForwardPhase Uint64FromBytes = types.Uint64FromBytes
NewGenesisState = types.NewGenesisState Uint64ToBytes = types.Uint64ToBytes
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 // variable aliases
DistantFuture = types.DistantFuture AuctionByTimeKeyPrefix = types.AuctionByTimeKeyPrefix
ModuleCdc = types.ModuleCdc AuctionKeyPrefix = types.AuctionKeyPrefix
AuctionKeyPrefix = types.AuctionKeyPrefix DefaultIncrement = types.DefaultIncrement
AuctionByTimeKeyPrefix = types.AuctionByTimeKeyPrefix DistantFuture = types.DistantFuture
NextAuctionIDKey = types.NextAuctionIDKey ErrAuctionHasExpired = types.ErrAuctionHasExpired
DefaultIncrement = types.DefaultIncrement ErrAuctionHasNotExpired = types.ErrAuctionHasNotExpired
KeyBidDuration = types.KeyBidDuration ErrAuctionNotFound = types.ErrAuctionNotFound
KeyMaxAuctionDuration = types.KeyMaxAuctionDuration ErrBidTooLarge = types.ErrBidTooLarge
KeyIncrementSurplus = types.KeyIncrementSurplus ErrBidTooSmall = types.ErrBidTooSmall
KeyIncrementDebt = types.KeyIncrementDebt ErrInvalidBidDenom = types.ErrInvalidBidDenom
KeyIncrementCollateral = types.KeyIncrementCollateral ErrInvalidInitialAuctionID = types.ErrInvalidInitialAuctionID
ErrInvalidLotDenom = types.ErrInvalidLotDenom
ErrLotTooLarge = types.ErrLotTooLarge
ErrLotTooSmall = types.ErrLotTooSmall
ErrUnrecognizedAuctionType = types.ErrUnrecognizedAuctionType
KeyBidDuration = types.KeyBidDuration
KeyIncrementCollateral = types.KeyIncrementCollateral
KeyIncrementDebt = types.KeyIncrementDebt
KeyIncrementSurplus = types.KeyIncrementSurplus
KeyMaxAuctionDuration = types.KeyMaxAuctionDuration
ModuleCdc = types.ModuleCdc
NextAuctionIDKey = types.NextAuctionIDKey
) )
type ( type (
Keeper = keeper.Keeper Keeper = keeper.Keeper
Auction = types.Auction Auction = types.Auction
AuctionWithPhase = types.AuctionWithPhase
Auctions = types.Auctions Auctions = types.Auctions
BaseAuction = types.BaseAuction BaseAuction = types.BaseAuction
SurplusAuction = types.SurplusAuction
DebtAuction = types.DebtAuction
CollateralAuction = types.CollateralAuction CollateralAuction = types.CollateralAuction
WeightedAddresses = types.WeightedAddresses DebtAuction = types.DebtAuction
SupplyKeeper = types.SupplyKeeper
GenesisAuction = types.GenesisAuction GenesisAuction = types.GenesisAuction
GenesisAuctions = types.GenesisAuctions GenesisAuctions = types.GenesisAuctions
GenesisState = types.GenesisState GenesisState = types.GenesisState
MsgPlaceBid = types.MsgPlaceBid MsgPlaceBid = types.MsgPlaceBid
Params = types.Params Params = types.Params
QueryAuctionParams = types.QueryAuctionParams
QueryAllAuctionParams = types.QueryAllAuctionParams QueryAllAuctionParams = types.QueryAllAuctionParams
AuctionWithPhase = types.AuctionWithPhase QueryAuctionParams = types.QueryAuctionParams
SupplyKeeper = types.SupplyKeeper
SurplusAuction = types.SurplusAuction
WeightedAddresses = types.WeightedAddresses
) )

View File

@ -9,17 +9,20 @@ import (
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app" "github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/auction"
) )
var _, testAddrs = app.GeneratePrivKeyAddressPairs(2)
var testTime = time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) var testTime = time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
var testAuction = auction.NewCollateralAuction( var testAuction = auction.NewCollateralAuction(
"seller", "seller",
c("lotdenom", 10), c("lotdenom", 10),
testTime, testTime,
c("biddenom", 1000), c("biddenom", 1000),
auction.WeightedAddresses{}, auction.WeightedAddresses{Addresses: testAddrs, Weights: []sdk.Int{sdk.OneInt(), sdk.OneInt()}},
c("debt", 1000), c("debt", 1000),
).WithID(3).(auction.GenesisAuction) ).WithID(3).(auction.GenesisAuction)

View File

@ -20,6 +20,7 @@ func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin
types.DistantFuture, types.DistantFuture,
) )
// NOTE: for the duration of the auction the auction module account holds the lot
err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot))
if err != nil { if err != nil {
return 0, err return 0, err
@ -35,8 +36,8 @@ func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin
types.EventTypeAuctionStart, types.EventTypeAuctionStart,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())), sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()), sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()),
sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom), sdk.NewAttribute(types.AttributeKeyBid, auction.Bid.String()),
sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom), sdk.NewAttribute(types.AttributeKeyLot, auction.Lot.String()),
), ),
) )
return auctionID, nil return auctionID, nil
@ -50,14 +51,16 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in
bid, bid,
initialLot, initialLot,
types.DistantFuture, types.DistantFuture,
debt) debt,
)
// This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker. // This auction type mints coins at close. Need to check module account has minting privileges to avoid potential err in endblocker.
macc := k.supplyKeeper.GetModuleAccount(ctx, buyer) macc := k.supplyKeeper.GetModuleAccount(ctx, buyer)
if !macc.HasPermission(supply.Minter) { if !macc.HasPermission(supply.Minter) {
return 0, sdkerrors.Wrap(types.ErrInvalidModulePermissions, supply.Minter) panic(fmt.Errorf("module '%s' does not have '%s' permission", buyer, supply.Minter))
} }
// NOTE: for the duration of the auction the auction module account holds the debt
err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, buyer, types.ModuleName, sdk.NewCoins(debt)) err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, buyer, types.ModuleName, sdk.NewCoins(debt))
if err != nil { if err != nil {
return 0, err return 0, err
@ -73,8 +76,8 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in
types.EventTypeAuctionStart, types.EventTypeAuctionStart,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())), sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()), sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()),
sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom), sdk.NewAttribute(types.AttributeKeyBid, auction.Bid.String()),
sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom), sdk.NewAttribute(types.AttributeKeyLot, auction.Lot.String()),
), ),
) )
return auctionID, nil return auctionID, nil
@ -98,6 +101,7 @@ func (k Keeper) StartCollateralAuction(
debt, debt,
) )
// NOTE: for the duration of the auction the auction module account holds the debt and the lot
err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot))
if err != nil { if err != nil {
return 0, err return 0, err
@ -117,8 +121,9 @@ func (k Keeper) StartCollateralAuction(
types.EventTypeAuctionStart, types.EventTypeAuctionStart,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())), sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()), sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()),
sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom), sdk.NewAttribute(types.AttributeKeyBid, auction.Bid.String()),
sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom), sdk.NewAttribute(types.AttributeKeyLot, auction.Lot.String()),
sdk.NewAttribute(types.AttributeKeyMaxBid, auction.MaxBid.String()),
), ),
) )
return auctionID, nil return auctionID, nil
@ -218,7 +223,7 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder
types.EventTypeAuctionBid, types.EventTypeAuctionBid,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)), sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()), sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
sdk.NewAttribute(types.AttributeKeyBidAmount, a.Bid.Amount.String()), sdk.NewAttribute(types.AttributeKeyBid, a.Bid.String()),
sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())), sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())),
), ),
) )
@ -233,7 +238,7 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc
return a, sdkerrors.Wrapf(types.ErrInvalidBidDenom, "%s ≠ %s", bid.Denom, a.Bid.Denom) return a, sdkerrors.Wrapf(types.ErrInvalidBidDenom, "%s ≠ %s", bid.Denom, a.Bid.Denom)
} }
if a.IsReversePhase() { if a.IsReversePhase() {
return a, sdkerrors.Wrapf(types.ErrCollateralAuctionIsInReversePhase, "%d", a.ID) panic("cannot place forward bid on auction in reverse phase")
} }
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 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.MaxInt(
@ -294,7 +299,7 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc
types.EventTypeAuctionBid, types.EventTypeAuctionBid,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)), sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()), sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
sdk.NewAttribute(types.AttributeKeyBidAmount, a.Bid.Amount.String()), sdk.NewAttribute(types.AttributeKeyBid, a.Bid.String()),
sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())), sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())),
), ),
) )
@ -309,7 +314,7 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc
return a, sdkerrors.Wrapf(types.ErrInvalidLotDenom, lot.Denom, a.Lot.Denom) return a, sdkerrors.Wrapf(types.ErrInvalidLotDenom, lot.Denom, a.Lot.Denom)
} }
if !a.IsReversePhase() { if !a.IsReversePhase() {
return a, sdkerrors.Wrapf(types.ErrCollateralAuctionIsInForwardPhase, "%d", a.ID) panic("cannot place reverse bid on auction in forward phase")
} }
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 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.MaxInt(
@ -336,13 +341,18 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc
return a, err return a, err
} }
} }
// Decrease in lot is sent to weighted addresses (normally the CDP depositors) // 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. // Note: splitting an integer amount across weighted buckets results in small errors.
lotPayouts, err := splitCoinIntoWeightedBuckets(a.Lot.Sub(lot), a.LotReturns.Weights) lotPayouts, err := splitCoinIntoWeightedBuckets(a.Lot.Sub(lot), a.LotReturns.Weights)
if err != nil { if err != nil {
return a, err return a, err
} }
for i, payout := range lotPayouts { for i, payout := range lotPayouts {
// if the payout amount is 0, don't send 0 coins
if !payout.IsPositive() {
continue
}
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.LotReturns.Addresses[i], sdk.NewCoins(payout)) err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.LotReturns.Addresses[i], sdk.NewCoins(payout))
if err != nil { if err != nil {
return a, err return a, err
@ -363,7 +373,7 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc
types.EventTypeAuctionBid, types.EventTypeAuctionBid,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)), sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()), sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
sdk.NewAttribute(types.AttributeKeyLotAmount, a.Lot.Amount.String()), sdk.NewAttribute(types.AttributeKeyLot, a.Lot.String()),
sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())), sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())),
), ),
) )
@ -429,7 +439,7 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac
types.EventTypeAuctionBid, types.EventTypeAuctionBid,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)), sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()), sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
sdk.NewAttribute(types.AttributeKeyLotAmount, a.Lot.Amount.String()), sdk.NewAttribute(types.AttributeKeyLot, a.Lot.String()),
sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())), sdk.NewAttribute(types.AttributeKeyEndTime, fmt.Sprintf("%d", a.EndTime.Unix())),
), ),
) )
@ -473,6 +483,7 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) error {
sdk.NewEvent( sdk.NewEvent(
types.EventTypeAuctionClose, types.EventTypeAuctionClose,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())), sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
sdk.NewAttribute(types.AttributeKeyCloseBlock, fmt.Sprintf("%d", ctx.BlockHeight())),
), ),
) )
return nil return nil
@ -480,14 +491,17 @@ func (k Keeper) CloseAuction(ctx sdk.Context, auctionID uint64) error {
// PayoutDebtAuction pays out the proceeds for a debt auction, first minting the coins. // PayoutDebtAuction pays out the proceeds for a debt auction, first minting the coins.
func (k Keeper) PayoutDebtAuction(ctx sdk.Context, a types.DebtAuction) error { func (k Keeper) PayoutDebtAuction(ctx sdk.Context, a types.DebtAuction) error {
// create the coins that are needed to pay off the debt
err := k.supplyKeeper.MintCoins(ctx, a.Initiator, sdk.NewCoins(a.Lot)) err := k.supplyKeeper.MintCoins(ctx, a.Initiator, sdk.NewCoins(a.Lot))
if err != nil { if err != nil {
return err panic(fmt.Errorf("could not mint coins: %w", err))
} }
// send the new coins from the initiator module to the bidder
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, a.Initiator, a.Bidder, sdk.NewCoins(a.Lot)) err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, a.Initiator, a.Bidder, sdk.NewCoins(a.Lot))
if err != nil { if err != nil {
return err return err
} }
// if there is remaining debt, return it to the calling module to manage
if a.CorrespondingDebt.IsPositive() { if a.CorrespondingDebt.IsPositive() {
err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(a.CorrespondingDebt)) err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(a.CorrespondingDebt))
if err != nil { if err != nil {
@ -499,6 +513,7 @@ func (k Keeper) PayoutDebtAuction(ctx sdk.Context, a types.DebtAuction) error {
// PayoutSurplusAuction pays out the proceeds for a surplus auction. // PayoutSurplusAuction pays out the proceeds for a surplus auction.
func (k Keeper) PayoutSurplusAuction(ctx sdk.Context, a types.SurplusAuction) error { func (k Keeper) PayoutSurplusAuction(ctx sdk.Context, a types.SurplusAuction) error {
// Send the tokens from the auction module account where they are being managed to the bidder who won the auction
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot)) err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot))
if err != nil { if err != nil {
return err return err
@ -508,10 +523,13 @@ func (k Keeper) PayoutSurplusAuction(ctx sdk.Context, a types.SurplusAuction) er
// PayoutCollateralAuction pays out the proceeds for a collateral auction. // PayoutCollateralAuction pays out the proceeds for a collateral auction.
func (k Keeper) PayoutCollateralAuction(ctx sdk.Context, a types.CollateralAuction) error { func (k Keeper) PayoutCollateralAuction(ctx sdk.Context, a types.CollateralAuction) error {
// Send the tokens from the auction module account where they are being managed to the bidder who won the auction
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot)) err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.Bidder, sdk.NewCoins(a.Lot))
if err != nil { if err != nil {
return err return err
} }
// if there is remaining debt after the auction, send it back to the initiating module for management
if a.CorrespondingDebt.IsPositive() { if a.CorrespondingDebt.IsPositive() {
err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(a.CorrespondingDebt)) err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(a.CorrespondingDebt))
if err != nil { if err != nil {
@ -547,11 +565,6 @@ func earliestTime(t1, t2 time.Time) time.Time {
// splitCoinIntoWeightedBuckets divides up some amount of coins according to some weights. // splitCoinIntoWeightedBuckets divides up some amount of coins according to some weights.
func splitCoinIntoWeightedBuckets(coin sdk.Coin, buckets []sdk.Int) ([]sdk.Coin, error) { func splitCoinIntoWeightedBuckets(coin sdk.Coin, buckets []sdk.Int) ([]sdk.Coin, error) {
for _, bucket := range buckets {
if bucket.IsNegative() {
return nil, fmt.Errorf("cannot split %s into bucket with negative weight (%s)", coin.String(), bucket.String())
}
}
amounts := splitIntIntoWeightedBuckets(coin.Amount, buckets) amounts := splitIntIntoWeightedBuckets(coin.Amount, buckets)
result := make([]sdk.Coin, len(amounts)) result := make([]sdk.Coin, len(amounts))
for i, a := range amounts { for i, a := range amounts {

View File

@ -128,7 +128,7 @@ func (k Keeper) DeleteAuction(ctx sdk.Context, auctionID uint64) {
} }
// InsertIntoByTimeIndex adds an auction ID and end time into the byTime index. // 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 func (k Keeper) InsertIntoByTimeIndex(ctx sdk.Context, endTime time.Time, auctionID uint64) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix) store := prefix.NewStore(ctx.KVStore(k.storeKey), types.AuctionByTimeKeyPrefix)
store.Set(types.GetAuctionByTimeKey(endTime, auctionID), types.Uint64ToBytes(auctionID)) store.Set(types.GetAuctionByTimeKey(endTime, auctionID), types.Uint64ToBytes(auctionID))
} }

View File

@ -7,41 +7,52 @@ import (
) )
// splitIntIntoWeightedBuckets divides an initial +ve integer among several buckets in proportion to the buckets' weights // splitIntIntoWeightedBuckets divides an initial +ve integer among several buckets in proportion to the buckets' weights
// It uses the largest remainder method: // It uses the largest remainder method: https://en.wikipedia.org/wiki/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
// 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 { 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 // Limit input to +ve numbers as algorithm hasn't been scoped to work with -ve numbers.
if amount.IsNegative() { if amount.IsNegative() {
panic("negative amount") panic("negative amount")
} }
if len(buckets) < 1 {
panic("no buckets")
}
for _, bucket := range buckets { for _, bucket := range buckets {
if bucket.IsNegative() { if bucket.IsNegative() {
panic("negative bucket") panic("negative bucket")
} }
} }
totalWeights := totalInts(buckets...) // 1) Split the amount by weights, recording whole number part and remainder
totalWeights := totalInts(buckets...)
if !totalWeights.IsPositive() {
panic("total weights must sum to > 0")
}
// split amount by weights, recording whole number part and remainder
quotients := make([]quoRem, len(buckets)) quotients := make([]quoRem, len(buckets))
for i := range buckets { for i := range buckets {
// amount * ( weight/total_weight )
q := amount.Mul(buckets[i]).Quo(totalWeights) q := amount.Mul(buckets[i]).Quo(totalWeights)
r := amount.Mul(buckets[i]).Mod(totalWeights) r := amount.Mul(buckets[i]).Mod(totalWeights)
quotients[i] = quoRem{index: i, quo: q, rem: r} quotients[i] = quoRem{index: i, quo: q, rem: r}
} }
// apportion left over to buckets with the highest remainder (to minimize error) // 2) Calculate total left over from remainders, and apportion it to buckets with the highest remainder (to minimize error)
// sort by decreasing remainder order
sort.Slice(quotients, func(i, j int) bool { sort.Slice(quotients, func(i, j int) bool {
return quotients[i].rem.GT(quotients[j].rem) // decreasing remainder order return quotients[i].rem.GT(quotients[j].rem)
}) })
// calculate total left over from remainders
allocated := sdk.ZeroInt() allocated := sdk.ZeroInt()
for _, qr := range quotients { for _, qr := range quotients {
allocated = allocated.Add(qr.quo) allocated = allocated.Add(qr.quo)
} }
leftToAllocate := amount.Sub(allocated) leftToAllocate := amount.Sub(allocated)
// apportion according to largest remainder
results := make([]sdk.Int, len(quotients)) results := make([]sdk.Int, len(quotients))
for _, qr := range quotients { for _, qr := range quotients {
results[qr.index] = qr.quo results[qr.index] = qr.quo

View File

@ -10,19 +10,97 @@ import (
func TestSplitIntIntoWeightedBuckets(t *testing.T) { func TestSplitIntIntoWeightedBuckets(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
amount sdk.Int amount sdk.Int
buckets []sdk.Int buckets []sdk.Int
want []sdk.Int want []sdk.Int
expectPanic bool
}{ }{
{"2split1,1", i(2), is(1, 1), is(1, 1)}, {
{"100split1,9", i(100), is(1, 9), is(10, 90)}, name: "0split0",
{"7split1,2", i(7), is(1, 2), is(2, 5)}, amount: i(0),
{"17split1,1,1", i(17), is(1, 1, 1), is(6, 6, 5)}, buckets: is(0),
expectPanic: true,
},
{
name: "5splitnil",
amount: i(5),
buckets: is(),
expectPanic: true,
},
{
name: "-2split1,1",
amount: i(-2),
buckets: is(1, 1),
expectPanic: true,
},
{
name: "2split1,-1",
amount: i(2),
buckets: is(1, -1),
expectPanic: true,
},
{
name: "0split0,0,0,1",
amount: i(0),
buckets: is(0, 0, 0, 1),
want: is(0, 0, 0, 0),
},
{
name: "2split1,1",
amount: i(2),
buckets: is(1, 1),
want: is(1, 1),
},
{
name: "100split1,9",
amount: i(100),
buckets: is(1, 9),
want: is(10, 90),
},
{
name: "100split9,1",
amount: i(100),
buckets: is(9, 1),
want: is(90, 10),
},
{
name: "7split1,2",
amount: i(7),
buckets: is(1, 2),
want: is(2, 5),
},
{
name: "17split1,1,1",
amount: i(17),
buckets: is(1, 1, 1),
want: is(6, 6, 5),
},
{
name: "10split1000000,1",
amount: i(10),
buckets: is(1000000, 1),
want: is(10, 0),
},
{
name: "334733353split730777,31547",
amount: i(334733353),
buckets: is(730777, 31547),
want: is(320881194, 13852159),
},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
got := splitIntIntoWeightedBuckets(tc.amount, tc.buckets) var got []sdk.Int
run := func() {
got = splitIntIntoWeightedBuckets(tc.amount, tc.buckets)
}
if tc.expectPanic {
require.Panics(t, run)
} else {
require.NotPanics(t, run)
}
require.Equal(t, tc.want, got) require.Equal(t, tc.want, got)
}) })
} }

View File

@ -4,12 +4,13 @@ The `x/auction` module emits the following events:
## Triggered By Other Modules ## Triggered By Other Modules
| Type | Attribute Key | Attribute Value | | Type | Attribute Key | Attribute Value |
|---------------|---------------|---------------------| |---------------|---------------|-----------------|
| auction_start | auction_id | {auction ID} | | auction_start | auction_id | {auction ID} |
| auction_start | auction_type | {auction type} | | auction_start | auction_type | {auction type} |
| auction_start | lot_denom | {auction lot denom} | | auction_start | lot | {coin amount} |
| auction_start | bid_denom | {auction bid denom} | | auction_start | bid | {coin amount} |
| auction_start | max_bid | {coin amount} |
## Handlers ## Handlers
@ -19,8 +20,8 @@ The `x/auction` module emits the following events:
|-------------|---------------|--------------------| |-------------|---------------|--------------------|
| auction_bid | auction_id | {auction ID} | | auction_bid | auction_id | {auction ID} |
| auction_bid | bidder | {latest bidder} | | auction_bid | bidder | {latest bidder} |
| auction_bid | bid_amount | {coin amount} | | auction_bid | bid | {coin amount} |
| auction_bid | lot_amount | {coin amount} | | auction_bid | lot | {coin amount} |
| auction_bid | end_time | {auction end time} | | auction_bid | end_time | {auction end time} |
| message | module | auction | | message | module | auction |
| message | sender | {sender address} | | message | sender | {sender address} |
@ -30,3 +31,4 @@ The `x/auction` module emits the following events:
| Type | Attribute Key | Attribute Value | | Type | Attribute Key | Attribute Value |
|---------------|---------------|-----------------| |---------------|---------------|-----------------|
| auction_close | auction_id | {auction ID} | | auction_close | auction_id | {auction ID} |
| auction_close | close_block | {block height} |

View File

@ -287,15 +287,23 @@ func NewWeightedAddresses(addrs []sdk.AccAddress, weights []sdk.Int) (WeightedAd
return wa, nil return wa, nil
} }
// Validate checks for that the weights are not negative and that lengths match. // Validate checks for that the weights are not negative, not all zero, and the lengths match.
func (wa WeightedAddresses) Validate() error { func (wa WeightedAddresses) Validate() error {
if len(wa.Weights) < 1 {
return fmt.Errorf("must be at least 1 weighted address")
}
if len(wa.Addresses) != len(wa.Weights) { if len(wa.Addresses) != len(wa.Weights) {
return fmt.Errorf("number of addresses doesn't match number of weights, %d ≠ %d", len(wa.Addresses), len(wa.Weights)) return fmt.Errorf("number of addresses doesn't match number of weights, %d ≠ %d", len(wa.Addresses), len(wa.Weights))
} }
totalWeight := sdk.ZeroInt()
for _, w := range wa.Weights { for _, w := range wa.Weights {
if w.IsNegative() { if w.IsNegative() {
return fmt.Errorf("weights contain a negative amount: %s", w) return fmt.Errorf("weights contain a negative amount: %s", w)
} }
totalWeight = totalWeight.Add(w)
}
if !totalWeight.IsPositive() {
return fmt.Errorf("total weight must be positive")
} }
return nil return nil
} }

View File

@ -1,6 +1,7 @@
package types package types
import ( import (
"fmt"
"testing" "testing"
"time" "time"
@ -24,48 +25,65 @@ const (
TestAccAddress2 = "kava1pdfav2cjhry9k79nu6r8kgknnjtq6a7rcr0qlr" TestAccAddress2 = "kava1pdfav2cjhry9k79nu6r8kgknnjtq6a7rcr0qlr"
) )
func d(amount string) sdk.Dec { return sdk.MustNewDecFromStr(amount) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
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 TestNewWeightedAddresses(t *testing.T) { func TestNewWeightedAddresses(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
addresses []sdk.AccAddress addresses []sdk.AccAddress
weights []sdk.Int weights []sdk.Int
expectpass bool expectedErr error
}{ }{
{ {
"normal", name: "normal",
[]sdk.AccAddress{ addresses: []sdk.AccAddress{
sdk.AccAddress([]byte(TestAccAddress1)), sdk.AccAddress([]byte(TestAccAddress1)),
sdk.AccAddress([]byte(TestAccAddress2)), sdk.AccAddress([]byte(TestAccAddress2)),
}, },
[]sdk.Int{ weights: is(6, 8),
sdk.NewInt(6), expectedErr: nil,
sdk.NewInt(8),
},
true,
}, },
{ {
"mismatched", name: "mismatched",
[]sdk.AccAddress{ addresses: []sdk.AccAddress{
sdk.AccAddress([]byte(TestAccAddress1)), sdk.AccAddress([]byte(TestAccAddress1)),
sdk.AccAddress([]byte(TestAccAddress2)), sdk.AccAddress([]byte(TestAccAddress2)),
}, },
[]sdk.Int{ weights: is(6),
sdk.NewInt(6), expectedErr: fmt.Errorf("number of addresses doesn't match number of weights, %d ≠ %d", 2, 1),
},
false,
}, },
{ {
"negativeWeight", name: "negativeWeight",
[]sdk.AccAddress{ addresses: []sdk.AccAddress{
sdk.AccAddress([]byte(TestAccAddress1)), sdk.AccAddress([]byte(TestAccAddress1)),
sdk.AccAddress([]byte(TestAccAddress2)), sdk.AccAddress([]byte(TestAccAddress2)),
}, },
[]sdk.Int{ weights: is(6, -8),
sdk.NewInt(6), expectedErr: fmt.Errorf("weights contain a negative amount: %s", i(-8)),
sdk.NewInt(-8), },
{
name: "zero total weights",
addresses: []sdk.AccAddress{
sdk.AccAddress([]byte(TestAccAddress1)),
sdk.AccAddress([]byte(TestAccAddress2)),
}, },
false, weights: is(0, 0),
expectedErr: fmt.Errorf("total weight must be positive"),
},
{
name: "no weights",
addresses: nil,
weights: nil,
expectedErr: fmt.Errorf("must be at least 1 weighted address"),
}, },
} }
@ -75,27 +93,16 @@ func TestNewWeightedAddresses(t *testing.T) {
// Attempt to instantiate new WeightedAddresses // Attempt to instantiate new WeightedAddresses
weightedAddresses, err := NewWeightedAddresses(tc.addresses, tc.weights) weightedAddresses, err := NewWeightedAddresses(tc.addresses, tc.weights)
if tc.expectpass { if tc.expectedErr != nil {
// Confirm there is no error // Confirm the error
require.Nil(t, err) require.EqualError(t, err, tc.expectedErr.Error())
} else {
require.NoError(t, err)
// Check addresses, weights // Check addresses, weights
require.Equal(t, tc.addresses, weightedAddresses.Addresses) require.Equal(t, tc.addresses, weightedAddresses.Addresses)
require.Equal(t, tc.weights, weightedAddresses.Weights) require.Equal(t, tc.weights, weightedAddresses.Weights)
} else {
// Confirm that there is an error
require.NotNil(t, err)
switch tc.name {
case "mismatched":
require.Contains(t, err.Error(), "number of addresses doesn't match number of weights")
case "negativeWeight":
require.Contains(t, err.Error(), "weights contain a negative amount")
default:
// Unexpected error state
t.Fail()
}
} }
}) })
} }
} }

View File

@ -7,30 +7,24 @@ import sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
var ( var (
// ErrInvalidInitialAuctionID error for when the initial auction ID hasn't been set // ErrInvalidInitialAuctionID error for when the initial auction ID hasn't been set
ErrInvalidInitialAuctionID = sdkerrors.Register(ModuleName, 2, "initial auction ID hasn't been set") ErrInvalidInitialAuctionID = sdkerrors.Register(ModuleName, 2, "initial auction ID hasn't been set")
// ErrInvalidModulePermissions error for when module doesn't have valid permissions
ErrInvalidModulePermissions = sdkerrors.Register(ModuleName, 3, "module does not have required permission")
// ErrUnrecognizedAuctionType error for unrecognized auction type // ErrUnrecognizedAuctionType error for unrecognized auction type
ErrUnrecognizedAuctionType = sdkerrors.Register(ModuleName, 4, "unrecognized auction type") ErrUnrecognizedAuctionType = sdkerrors.Register(ModuleName, 3, "unrecognized auction type")
// ErrAuctionNotFound error for when an auction is not found // ErrAuctionNotFound error for when an auction is not found
ErrAuctionNotFound = sdkerrors.Register(ModuleName, 5, "auction not found") ErrAuctionNotFound = sdkerrors.Register(ModuleName, 4, "auction not found")
// ErrAuctionHasNotExpired error for attempting to close an auction that has not passed its end time // ErrAuctionHasNotExpired error for attempting to close an auction that has not passed its end time
ErrAuctionHasNotExpired = sdkerrors.Register(ModuleName, 6, "auction can't be closed as curent block time has not passed auction end time") ErrAuctionHasNotExpired = sdkerrors.Register(ModuleName, 5, "auction can't be closed as curent block time has not passed auction end time")
// ErrAuctionHasExpired error for when an auction is closed and unavailable for bidding // ErrAuctionHasExpired error for when an auction is closed and unavailable for bidding
ErrAuctionHasExpired = sdkerrors.Register(ModuleName, 7, "auction has closed") ErrAuctionHasExpired = sdkerrors.Register(ModuleName, 6, "auction has closed")
// ErrInvalidBidDenom error for when bid denom doesn't match auction bid denom // ErrInvalidBidDenom error for when bid denom doesn't match auction bid denom
ErrInvalidBidDenom = sdkerrors.Register(ModuleName, 8, "bid denom doesn't match auction bid denom") ErrInvalidBidDenom = sdkerrors.Register(ModuleName, 7, "bid denom doesn't match auction bid denom")
// ErrInvalidLotDenom error for when lot denom doesn't match auction lot denom // ErrInvalidLotDenom error for when lot denom doesn't match auction lot denom
ErrInvalidLotDenom = sdkerrors.Register(ModuleName, 9, "lot denom doesn't match auction lot denom") ErrInvalidLotDenom = sdkerrors.Register(ModuleName, 8, "lot denom doesn't match auction lot denom")
// ErrBidTooSmall error for when bid is not greater than auction's min bid amount // ErrBidTooSmall error for when bid is not greater than auction's min bid amount
ErrBidTooSmall = sdkerrors.Register(ModuleName, 10, "bid is not greater than auction's min new bid amount") ErrBidTooSmall = sdkerrors.Register(ModuleName, 9, "bid is not greater than auction's min new bid amount")
// ErrBidTooLarge error for when bid is larger than auction's maximum allowed bid // ErrBidTooLarge error for when bid is larger than auction's maximum allowed bid
ErrBidTooLarge = sdkerrors.Register(ModuleName, 11, "bid is greater than auction's max bid") ErrBidTooLarge = sdkerrors.Register(ModuleName, 10, "bid is greater than auction's max bid")
// ErrLotTooSmall error for when lot is less than zero // ErrLotTooSmall error for when lot is less than zero
ErrLotTooSmall = sdkerrors.Register(ModuleName, 12, "lot is not greater than auction's min new lot amount") ErrLotTooSmall = sdkerrors.Register(ModuleName, 11, "lot is not greater than auction's min new lot amount")
// ErrLotTooLarge error for when lot is not smaller than auction's max new lot amount // ErrLotTooLarge error for when lot is not smaller than auction's max new lot amount
ErrLotTooLarge = sdkerrors.Register(ModuleName, 13, "lot is greater than auction's max new lot amount") ErrLotTooLarge = sdkerrors.Register(ModuleName, 12, "lot is greater than auction's max new lot amount")
// ErrCollateralAuctionIsInReversePhase error for when attempting to place a forward bid on a collateral auction in reverse phase
ErrCollateralAuctionIsInReversePhase = sdkerrors.Register(ModuleName, 14, "invalid bid: auction is in reverse phase")
// ErrCollateralAuctionIsInForwardPhase error for when attempting to place a reverse bid on a collateral auction in forward phase
ErrCollateralAuctionIsInForwardPhase = sdkerrors.Register(ModuleName, 15, "invalid bid: auction is in forward phase")
) )

View File

@ -1,6 +1,6 @@
package types package types
// Events for auction module // Events for the module
const ( const (
EventTypeAuctionStart = "auction_start" EventTypeAuctionStart = "auction_start"
EventTypeAuctionBid = "auction_bid" EventTypeAuctionBid = "auction_bid"
@ -10,9 +10,9 @@ const (
AttributeKeyAuctionID = "auction_id" AttributeKeyAuctionID = "auction_id"
AttributeKeyAuctionType = "auction_type" AttributeKeyAuctionType = "auction_type"
AttributeKeyBidder = "bidder" AttributeKeyBidder = "bidder"
AttributeKeyBidDenom = "bid_denom" AttributeKeyLot = "lot"
AttributeKeyLotDenom = "lot_denom" AttributeKeyMaxBid = "max_bid"
AttributeKeyBidAmount = "bid_amount" AttributeKeyBid = "bid"
AttributeKeyLotAmount = "lot_amount"
AttributeKeyEndTime = "end_time" AttributeKeyEndTime = "end_time"
AttributeKeyCloseBlock = "close_block"
) )

View File

@ -76,7 +76,7 @@ func (gs GenesisState) Validate() error {
ids[a.GetID()] = true ids[a.GetID()] = true
if a.GetID() >= gs.NextAuctionID { if a.GetID() >= gs.NextAuctionID {
return fmt.Errorf("found auction ID >= the nextAuctionID (%d >= %d)", a.GetID(), gs.NextAuctionID) return fmt.Errorf("found auction ID ≥ the nextAuctionID (%d ≥ %d)", a.GetID(), gs.NextAuctionID)
} }
} }
return nil return nil

View File

@ -39,5 +39,3 @@ func TestMsgPlaceBid_ValidateBasic(t *testing.T) {
}) })
} }
} }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }

View File

@ -5,8 +5,6 @@ import (
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
sdk "github.com/cosmos/cosmos-sdk/types"
) )
func TestParams_Validate(t *testing.T) { func TestParams_Validate(t *testing.T) {
@ -105,5 +103,3 @@ func TestParams_Validate(t *testing.T) {
}) })
} }
} }
func d(amount string) sdk.Dec { return sdk.MustNewDecFromStr(amount) }

View File

@ -11,98 +11,15 @@ const (
dump = 100 dump = 100
) )
type partialDeposit struct {
Depositor sdk.AccAddress
Amount sdk.Coin
DebtShare sdk.Int
}
func newPartialDeposit(depositor sdk.AccAddress, amount sdk.Coin, ds sdk.Int) partialDeposit {
return partialDeposit{
Depositor: depositor,
Amount: amount,
DebtShare: ds,
}
}
type partialDeposits []partialDeposit
func (pd partialDeposits) SumCollateral() (sum sdk.Int) {
sum = sdk.ZeroInt()
for _, d := range pd {
sum = sum.Add(d.Amount.Amount)
}
return
}
func (pd partialDeposits) SumDebt() (sum sdk.Int) {
sum = sdk.ZeroInt()
for _, d := range pd {
sum = sum.Add(d.DebtShare)
}
return
}
// AuctionCollateral creates auctions from the input deposits which attempt to raise the corresponding amount of debt // AuctionCollateral creates auctions from the input deposits which attempt to raise the corresponding amount of debt
func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt sdk.Int, bidDenom string) error { func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt sdk.Int, bidDenom string) error {
auctionSize := k.getAuctionSize(ctx, deposits[0].Amount.Denom)
partialAuctionDeposits := partialDeposits{}
totalCollateral := deposits.SumCollateral()
for totalCollateral.GT(sdk.ZeroInt()) {
for i, dep := range deposits {
if dep.Amount.IsZero() {
continue
}
collateralAmount := dep.Amount.Amount
collateralDenom := dep.Amount.Denom
// create auctions from individual deposits that are larger than the auction size
debtChange, collateralChange, err := k.CreateAuctionsFromDeposit(ctx, dep, debt, totalCollateral, auctionSize, bidDenom)
if err != nil {
return err
}
debt = debt.Sub(debtChange)
totalCollateral = totalCollateral.Sub(collateralChange)
dep.Amount = sdk.NewCoin(collateralDenom, collateralAmount.Sub(collateralChange))
collateralAmount = collateralAmount.Sub(collateralChange)
// if there is leftover collateral that is less than a lot
if !dep.Amount.IsZero() {
// figure out how much debt this deposit accounts for
// (depositCollateral / totalCollateral) * totalDebtFromCDP
debtCoveredByDeposit := (collateralAmount.Quo(totalCollateral)).Mul(debt)
// if adding this deposit to the other partial deposits is less than a lot
if (partialAuctionDeposits.SumCollateral().Add(collateralAmount)).LT(auctionSize) {
// append the deposit to the partial deposits and zero out the deposit
pd := newPartialDeposit(dep.Depositor, dep.Amount, debtCoveredByDeposit)
partialAuctionDeposits = append(partialAuctionDeposits, pd)
dep.Amount = sdk.NewCoin(collateralDenom, sdk.ZeroInt())
} else {
// if the sum of partial deposits now makes a lot
partialCollateral := sdk.NewCoin(collateralDenom, auctionSize.Sub(partialAuctionDeposits.SumCollateral()))
partialAmount := partialCollateral.Amount
partialDebt := (partialAmount.Quo(collateralAmount)).Mul(debtCoveredByDeposit)
// create a partial deposit from the deposit auctionSize := k.getAuctionSize(ctx, deposits[0].Amount.Denom)
partialDep := newPartialDeposit(dep.Depositor, partialCollateral, partialDebt) totalCollateral := deposits.SumCollateral()
// append it to the partial deposits for _, deposit := range deposits {
partialAuctionDeposits = append(partialAuctionDeposits, partialDep)
// create an auction from the partial deposits debtCoveredByDeposit := (sdk.NewDecFromInt(deposit.Amount.Amount).Quo(sdk.NewDecFromInt(totalCollateral))).Mul(sdk.NewDecFromInt(debt)).RoundInt()
debtChange, collateralChange, err := k.CreateAuctionFromPartialDeposits(ctx, partialAuctionDeposits, debt, totalCollateral, auctionSize, bidDenom) err := k.CreateAuctionsFromDeposit(ctx, deposit.Amount, deposit.Depositor, debtCoveredByDeposit, auctionSize, bidDenom)
if err != nil {
return err
}
debt = debt.Sub(debtChange)
totalCollateral = totalCollateral.Sub(collateralChange)
// reset partial deposits and update the deposit amount
partialAuctionDeposits = partialDeposits{}
dep.Amount = sdk.NewCoin(collateralDenom, collateralAmount.Sub(partialAmount))
}
}
deposits[i] = dep
totalCollateral = deposits.SumCollateral()
}
}
if partialAuctionDeposits.SumCollateral().GT(sdk.ZeroInt()) {
_, _, err := k.CreateAuctionFromPartialDeposits(ctx, partialAuctionDeposits, debt, totalCollateral, partialAuctionDeposits.SumCollateral(), bidDenom)
if err != nil { if err != nil {
return err return err
} }
@ -110,54 +27,38 @@ func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt
return nil return nil
} }
// CreateAuctionsFromDeposit creates auctions from the input deposit until there is less than auctionSize left on the deposit // CreateAuctionsFromDeposit creates auctions from the input deposit
func (k Keeper) CreateAuctionsFromDeposit( func (k Keeper) CreateAuctionsFromDeposit(
ctx sdk.Context, dep types.Deposit, debt sdk.Int, totalCollateral sdk.Int, auctionSize sdk.Int, ctx sdk.Context, collateral sdk.Coin, returnAddr sdk.AccAddress, debt, auctionSize sdk.Int,
principalDenom string) (debtChange sdk.Int, collateralChange sdk.Int, err error) { principalDenom string) (err error) {
debtChange = sdk.ZeroInt()
collateralChange = sdk.ZeroInt() amountToAuction := collateral.Amount
depositAmount := dep.Amount.Amount totalCollateralAmount := collateral.Amount
depositDenom := dep.Amount.Denom remainingDebt := debt
for depositAmount.GTE(auctionSize) { if !amountToAuction.IsPositive() {
// figure out how much debt is covered by one lots worth of collateral return nil
depositDebtAmount := (sdk.NewDecFromInt(auctionSize).Quo(sdk.NewDecFromInt(totalCollateral))).Mul(sdk.NewDecFromInt(debt)).RoundInt() }
penalty := k.ApplyLiquidationPenalty(ctx, depositDenom, depositDebtAmount) for amountToAuction.GT(auctionSize) {
// start an auction for one lot, attempting to raise depositDebtAmount plus the liquidation penalty debtCoveredByAuction := (sdk.NewDecFromInt(auctionSize).Quo(sdk.NewDecFromInt(totalCollateralAmount))).Mul(sdk.NewDecFromInt(debt)).RoundInt()
penalty := k.ApplyLiquidationPenalty(ctx, collateral.Denom, debtCoveredByAuction)
_, err := k.auctionKeeper.StartCollateralAuction( _, err := k.auctionKeeper.StartCollateralAuction(
ctx, types.LiquidatorMacc, sdk.NewCoin(depositDenom, auctionSize), sdk.NewCoin(principalDenom, depositDebtAmount.Add(penalty)), []sdk.AccAddress{dep.Depositor}, ctx, types.LiquidatorMacc, sdk.NewCoin(collateral.Denom, auctionSize), sdk.NewCoin(principalDenom, debtCoveredByAuction.Add(penalty)), []sdk.AccAddress{returnAddr},
[]sdk.Int{auctionSize}, sdk.NewCoin(k.GetDebtDenom(ctx), depositDebtAmount)) []sdk.Int{auctionSize}, sdk.NewCoin(k.GetDebtDenom(ctx), debtCoveredByAuction))
if err != nil { if err != nil {
return sdk.ZeroInt(), sdk.ZeroInt(), err return err
} }
depositAmount = depositAmount.Sub(auctionSize) amountToAuction = amountToAuction.Sub(auctionSize)
totalCollateral = totalCollateral.Sub(auctionSize) remainingDebt = remainingDebt.Sub(debtCoveredByAuction)
debt = debt.Sub(depositDebtAmount)
// subtract one lot's worth of debt from the total debt covered by this deposit
debtChange = debtChange.Add(depositDebtAmount)
collateralChange = collateralChange.Add(auctionSize)
} }
return debtChange, collateralChange, nil penalty := k.ApplyLiquidationPenalty(ctx, collateral.Denom, remainingDebt)
} _, err = k.auctionKeeper.StartCollateralAuction(
ctx, types.LiquidatorMacc, sdk.NewCoin(collateral.Denom, amountToAuction), sdk.NewCoin(principalDenom, remainingDebt.Add(penalty)), []sdk.AccAddress{returnAddr},
// CreateAuctionFromPartialDeposits creates an auction from the input partial deposits []sdk.Int{amountToAuction}, sdk.NewCoin(k.GetDebtDenom(ctx), remainingDebt))
func (k Keeper) CreateAuctionFromPartialDeposits(ctx sdk.Context, partialDeps partialDeposits, debt sdk.Int, collateral sdk.Int, auctionSize sdk.Int, bidDenom string) (debtChange, collateralChange sdk.Int, err error) {
returnAddrs := []sdk.AccAddress{}
returnWeights := []sdk.Int{}
depositDenom := partialDeps[0].Amount.Denom
for _, pd := range partialDeps {
returnAddrs = append(returnAddrs, pd.Depositor)
returnWeights = append(returnWeights, pd.DebtShare)
}
penalty := k.ApplyLiquidationPenalty(ctx, depositDenom, partialDeps.SumDebt())
_, err = k.auctionKeeper.StartCollateralAuction(ctx, types.LiquidatorMacc, sdk.NewCoin(partialDeps[0].Amount.Denom, auctionSize), sdk.NewCoin(bidDenom, partialDeps.SumDebt().Add(penalty)), returnAddrs, returnWeights, sdk.NewCoin(k.GetDebtDenom(ctx), partialDeps.SumDebt()))
if err != nil { if err != nil {
return sdk.ZeroInt(), sdk.ZeroInt(), err return err
} }
debtChange = partialDeps.SumDebt()
collateralChange = partialDeps.SumCollateral() return nil
return debtChange, collateralChange, nil
} }
// NetSurplusAndDebt burns surplus and debt coins equal to the minimum of surplus and debt balances held by the liquidator module account // NetSurplusAndDebt burns surplus and debt coins equal to the minimum of surplus and debt balances held by the liquidator module account

View File

@ -13,8 +13,8 @@ import (
// 1. updates the fees for the input cdp, // 1. updates the fees for the input cdp,
// 2. sends collateral for all deposits from the cdp module to the liquidator module account // 2. sends collateral for all deposits from the cdp module to the liquidator module account
// 3. Applies the liquidation penalty and mints the corresponding amount of debt coins in the cdp module // 3. Applies the liquidation penalty and mints the corresponding amount of debt coins in the cdp module
// 3. moves debt coins from the cdp module to the liquidator module account, // 4. moves debt coins from the cdp module to the liquidator module account,
// 4. decrements the total amount of principal outstanding for that collateral type // 5. decrements the total amount of principal outstanding for that collateral type
// (this is the equivalent of saying that fees are no longer accumulated by a cdp once it gets liquidated) // (this is the equivalent of saying that fees are no longer accumulated by a cdp once it gets liquidated)
func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) error { func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) error {
// Calculate the previous collateral ratio // Calculate the previous collateral ratio