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
// DO NOT EDIT - generated by aliasgen tool (github.com/rhuairahrighairidh/aliasgen)
import (
"github.com/kava-labs/kava/x/auction/keeper"
"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 (
EventTypeAuctionStart = types.EventTypeAuctionStart
EventTypeAuctionBid = types.EventTypeAuctionBid
EventTypeAuctionClose = types.EventTypeAuctionClose
AttributeValueCategory = types.AttributeValueCategory
AttributeKeyAuctionID = types.AttributeKeyAuctionID
AttributeKeyAuctionType = types.AttributeKeyAuctionType
AttributeKeyBid = types.AttributeKeyBid
AttributeKeyBidder = types.AttributeKeyBidder
AttributeKeyBidDenom = types.AttributeKeyBidDenom
AttributeKeyLotDenom = types.AttributeKeyLotDenom
AttributeKeyBidAmount = types.AttributeKeyBidAmount
AttributeKeyLotAmount = types.AttributeKeyLotAmount
AttributeKeyCloseBlock = types.AttributeKeyCloseBlock
AttributeKeyEndTime = types.AttributeKeyEndTime
DefaultNextAuctionID = types.DefaultNextAuctionID
ModuleName = types.ModuleName
StoreKey = types.StoreKey
RouterKey = types.RouterKey
DefaultParamspace = types.DefaultParamspace
QuerierRoute = types.QuerierRoute
DefaultMaxAuctionDuration = types.DefaultMaxAuctionDuration
AttributeKeyLot = types.AttributeKeyLot
AttributeKeyMaxBid = types.AttributeKeyMaxBid
AttributeValueCategory = types.AttributeValueCategory
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
QueryGetAuctions = types.QueryGetAuctions
QueryGetParams = types.QueryGetParams
RouterKey = types.RouterKey
StoreKey = types.StoreKey
)
var (
// functions aliases
// function aliases
ModuleAccountInvariants = keeper.ModuleAccountInvariants
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
RegisterInvariants = keeper.RegisterInvariants
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
ValidAuctionInvariant = keeper.ValidAuctionInvariant
ValidIndexInvariant = keeper.ValidIndexInvariant
DefaultGenesisState = types.DefaultGenesisState
GetAuctionKey = types.GetAuctionKey
DefaultParams = types.DefaultParams
GetAuctionByTimeKey = types.GetAuctionByTimeKey
Uint64ToBytes = types.Uint64ToBytes
Uint64FromBytes = types.Uint64FromBytes
GetAuctionKey = types.GetAuctionKey
NewAuctionWithPhase = types.NewAuctionWithPhase
NewCollateralAuction = types.NewCollateralAuction
NewDebtAuction = types.NewDebtAuction
NewGenesisState = types.NewGenesisState
NewMsgPlaceBid = types.NewMsgPlaceBid
NewParams = types.NewParams
DefaultParams = types.DefaultParams
ParamKeyTable = types.ParamKeyTable
NewQueryAllAuctionParams = types.NewQueryAllAuctionParams
NewAuctionWithPhase = types.NewAuctionWithPhase
NewSurplusAuction = types.NewSurplusAuction
NewWeightedAddresses = types.NewWeightedAddresses
ParamKeyTable = types.ParamKeyTable
RegisterCodec = types.RegisterCodec
Uint64FromBytes = types.Uint64FromBytes
Uint64ToBytes = types.Uint64ToBytes
// variable aliases
DistantFuture = types.DistantFuture
ModuleCdc = types.ModuleCdc
AuctionKeyPrefix = types.AuctionKeyPrefix
AuctionByTimeKeyPrefix = types.AuctionByTimeKeyPrefix
NextAuctionIDKey = types.NextAuctionIDKey
AuctionKeyPrefix = types.AuctionKeyPrefix
DefaultIncrement = types.DefaultIncrement
DistantFuture = types.DistantFuture
ErrAuctionHasExpired = types.ErrAuctionHasExpired
ErrAuctionHasNotExpired = types.ErrAuctionHasNotExpired
ErrAuctionNotFound = types.ErrAuctionNotFound
ErrBidTooLarge = types.ErrBidTooLarge
ErrBidTooSmall = types.ErrBidTooSmall
ErrInvalidBidDenom = types.ErrInvalidBidDenom
ErrInvalidInitialAuctionID = types.ErrInvalidInitialAuctionID
ErrInvalidLotDenom = types.ErrInvalidLotDenom
ErrLotTooLarge = types.ErrLotTooLarge
ErrLotTooSmall = types.ErrLotTooSmall
ErrUnrecognizedAuctionType = types.ErrUnrecognizedAuctionType
KeyBidDuration = types.KeyBidDuration
KeyMaxAuctionDuration = types.KeyMaxAuctionDuration
KeyIncrementSurplus = types.KeyIncrementSurplus
KeyIncrementDebt = types.KeyIncrementDebt
KeyIncrementCollateral = types.KeyIncrementCollateral
KeyIncrementDebt = types.KeyIncrementDebt
KeyIncrementSurplus = types.KeyIncrementSurplus
KeyMaxAuctionDuration = types.KeyMaxAuctionDuration
ModuleCdc = types.ModuleCdc
NextAuctionIDKey = types.NextAuctionIDKey
)
type (
Keeper = keeper.Keeper
Auction = types.Auction
AuctionWithPhase = types.AuctionWithPhase
Auctions = types.Auctions
BaseAuction = types.BaseAuction
SurplusAuction = types.SurplusAuction
DebtAuction = types.DebtAuction
CollateralAuction = types.CollateralAuction
WeightedAddresses = types.WeightedAddresses
SupplyKeeper = types.SupplyKeeper
DebtAuction = types.DebtAuction
GenesisAuction = types.GenesisAuction
GenesisAuctions = types.GenesisAuctions
GenesisState = types.GenesisState
MsgPlaceBid = types.MsgPlaceBid
Params = types.Params
QueryAuctionParams = types.QueryAuctionParams
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"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"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 testAuction = auction.NewCollateralAuction(
"seller",
c("lotdenom", 10),
testTime,
c("biddenom", 1000),
auction.WeightedAddresses{},
auction.WeightedAddresses{Addresses: testAddrs, Weights: []sdk.Int{sdk.OneInt(), sdk.OneInt()}},
c("debt", 1000),
).WithID(3).(auction.GenesisAuction)

View File

@ -20,6 +20,7 @@ func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin
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))
if err != nil {
return 0, err
@ -35,8 +36,8 @@ func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin
types.EventTypeAuctionStart,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()),
sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom),
sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom),
sdk.NewAttribute(types.AttributeKeyBid, auction.Bid.String()),
sdk.NewAttribute(types.AttributeKeyLot, auction.Lot.String()),
),
)
return auctionID, nil
@ -50,14 +51,16 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in
bid,
initialLot,
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.
macc := k.supplyKeeper.GetModuleAccount(ctx, buyer)
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))
if err != nil {
return 0, err
@ -73,8 +76,8 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in
types.EventTypeAuctionStart,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()),
sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom),
sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom),
sdk.NewAttribute(types.AttributeKeyBid, auction.Bid.String()),
sdk.NewAttribute(types.AttributeKeyLot, auction.Lot.String()),
),
)
return auctionID, nil
@ -98,6 +101,7 @@ func (k Keeper) StartCollateralAuction(
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))
if err != nil {
return 0, err
@ -117,8 +121,9 @@ func (k Keeper) StartCollateralAuction(
types.EventTypeAuctionStart,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
sdk.NewAttribute(types.AttributeKeyAuctionType, auction.GetType()),
sdk.NewAttribute(types.AttributeKeyBidDenom, auction.Bid.Denom),
sdk.NewAttribute(types.AttributeKeyLotDenom, auction.Lot.Denom),
sdk.NewAttribute(types.AttributeKeyBid, auction.Bid.String()),
sdk.NewAttribute(types.AttributeKeyLot, auction.Lot.String()),
sdk.NewAttribute(types.AttributeKeyMaxBid, auction.MaxBid.String()),
),
)
return auctionID, nil
@ -218,7 +223,7 @@ func (k Keeper) PlaceBidSurplus(ctx sdk.Context, a types.SurplusAuction, bidder
types.EventTypeAuctionBid,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
sdk.NewAttribute(types.AttributeKeyBidAmount, a.Bid.Amount.String()),
sdk.NewAttribute(types.AttributeKeyBid, a.Bid.String()),
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)
}
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
sdk.MaxInt(
@ -294,7 +299,7 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc
types.EventTypeAuctionBid,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
sdk.NewAttribute(types.AttributeKeyBidAmount, a.Bid.Amount.String()),
sdk.NewAttribute(types.AttributeKeyBid, a.Bid.String()),
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)
}
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
sdk.MaxInt(
@ -336,13 +341,18 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc
return a, err
}
}
// Decrease in lot is sent to weighted addresses (normally the CDP depositors)
// TODO paying out rateably to cdp depositors is vulnerable to errors compounding over multiple bids - check this can't be gamed.
// Note: splitting an integer amount across weighted buckets results in small errors.
lotPayouts, err := splitCoinIntoWeightedBuckets(a.Lot.Sub(lot), a.LotReturns.Weights)
if err != nil {
return a, err
}
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))
if err != nil {
return a, err
@ -363,7 +373,7 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc
types.EventTypeAuctionBid,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
sdk.NewAttribute(types.AttributeKeyLotAmount, a.Lot.Amount.String()),
sdk.NewAttribute(types.AttributeKeyLot, a.Lot.String()),
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,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", a.ID)),
sdk.NewAttribute(types.AttributeKeyBidder, a.Bidder.String()),
sdk.NewAttribute(types.AttributeKeyLotAmount, a.Lot.Amount.String()),
sdk.NewAttribute(types.AttributeKeyLot, a.Lot.String()),
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(
types.EventTypeAuctionClose,
sdk.NewAttribute(types.AttributeKeyAuctionID, fmt.Sprintf("%d", auction.GetID())),
sdk.NewAttribute(types.AttributeKeyCloseBlock, fmt.Sprintf("%d", ctx.BlockHeight())),
),
)
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.
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))
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))
if err != nil {
return err
}
// if there is remaining debt, return it to the calling module to manage
if a.CorrespondingDebt.IsPositive() {
err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(a.CorrespondingDebt))
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.
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))
if err != nil {
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.
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))
if err != nil {
return err
}
// if there is remaining debt after the auction, send it back to the initiating module for management
if a.CorrespondingDebt.IsPositive() {
err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(a.CorrespondingDebt))
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.
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)
result := make([]sdk.Coin, len(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.
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.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
// It uses the largest remainder method:
// https://en.wikipedia.org/wiki/Largest_remainder_method
// see also: https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100
// It uses the largest remainder method: https://en.wikipedia.org/wiki/Largest_remainder_method
// See also: https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100
func splitIntIntoWeightedBuckets(amount sdk.Int, buckets []sdk.Int) []sdk.Int {
// TODO ideally change algorithm to work with -ve numbers. Limiting to +ve numbers until them
// Limit input to +ve numbers as algorithm hasn't been scoped to work with -ve numbers.
if amount.IsNegative() {
panic("negative amount")
}
if len(buckets) < 1 {
panic("no buckets")
}
for _, bucket := range buckets {
if bucket.IsNegative() {
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))
for i := range buckets {
// amount * ( weight/total_weight )
q := amount.Mul(buckets[i]).Quo(totalWeights)
r := amount.Mul(buckets[i]).Mod(totalWeights)
quotients[i] = quoRem{index: i, quo: q, rem: r}
}
// apportion left over to buckets with the highest remainder (to minimize error)
// 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 {
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()
for _, qr := range quotients {
allocated = allocated.Add(qr.quo)
}
leftToAllocate := amount.Sub(allocated)
// apportion according to largest remainder
results := make([]sdk.Int, len(quotients))
for _, qr := range quotients {
results[qr.index] = qr.quo

View File

@ -14,15 +14,93 @@ func TestSplitIntIntoWeightedBuckets(t *testing.T) {
amount sdk.Int
buckets []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)},
{"7split1,2", i(7), is(1, 2), is(2, 5)},
{"17split1,1,1", i(17), is(1, 1, 1), is(6, 6, 5)},
{
name: "0split0",
amount: i(0),
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 {
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)
})
}

View File

@ -5,11 +5,12 @@ The `x/auction` module emits the following events:
## Triggered By Other Modules
| Type | Attribute Key | Attribute Value |
|---------------|---------------|---------------------|
|---------------|---------------|-----------------|
| auction_start | auction_id | {auction ID} |
| auction_start | auction_type | {auction type} |
| auction_start | lot_denom | {auction lot denom} |
| auction_start | bid_denom | {auction bid denom} |
| auction_start | lot | {coin amount} |
| auction_start | bid | {coin amount} |
| auction_start | max_bid | {coin amount} |
## Handlers
@ -19,8 +20,8 @@ The `x/auction` module emits the following events:
|-------------|---------------|--------------------|
| auction_bid | auction_id | {auction ID} |
| auction_bid | bidder | {latest bidder} |
| auction_bid | bid_amount | {coin amount} |
| auction_bid | lot_amount | {coin amount} |
| auction_bid | bid | {coin amount} |
| auction_bid | lot | {coin amount} |
| auction_bid | end_time | {auction end time} |
| message | module | auction |
| message | sender | {sender address} |
@ -30,3 +31,4 @@ The `x/auction` module emits the following events:
| Type | Attribute Key | Attribute Value |
|---------------|---------------|-----------------|
| 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
}
// 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 {
if len(wa.Weights) < 1 {
return fmt.Errorf("must be at least 1 weighted address")
}
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))
}
totalWeight := sdk.ZeroInt()
for _, w := range wa.Weights {
if w.IsNegative() {
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
}

View File

@ -1,6 +1,7 @@
package types
import (
"fmt"
"testing"
"time"
@ -24,48 +25,65 @@ const (
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) {
tests := []struct {
name string
addresses []sdk.AccAddress
weights []sdk.Int
expectpass bool
expectedErr error
}{
{
"normal",
[]sdk.AccAddress{
name: "normal",
addresses: []sdk.AccAddress{
sdk.AccAddress([]byte(TestAccAddress1)),
sdk.AccAddress([]byte(TestAccAddress2)),
},
[]sdk.Int{
sdk.NewInt(6),
sdk.NewInt(8),
},
true,
weights: is(6, 8),
expectedErr: nil,
},
{
"mismatched",
[]sdk.AccAddress{
name: "mismatched",
addresses: []sdk.AccAddress{
sdk.AccAddress([]byte(TestAccAddress1)),
sdk.AccAddress([]byte(TestAccAddress2)),
},
[]sdk.Int{
sdk.NewInt(6),
},
false,
weights: is(6),
expectedErr: fmt.Errorf("number of addresses doesn't match number of weights, %d ≠ %d", 2, 1),
},
{
"negativeWeight",
[]sdk.AccAddress{
name: "negativeWeight",
addresses: []sdk.AccAddress{
sdk.AccAddress([]byte(TestAccAddress1)),
sdk.AccAddress([]byte(TestAccAddress2)),
},
[]sdk.Int{
sdk.NewInt(6),
sdk.NewInt(-8),
weights: is(6, -8),
expectedErr: fmt.Errorf("weights contain a negative amount: %s", i(-8)),
},
false,
{
name: "zero total weights",
addresses: []sdk.AccAddress{
sdk.AccAddress([]byte(TestAccAddress1)),
sdk.AccAddress([]byte(TestAccAddress2)),
},
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
weightedAddresses, err := NewWeightedAddresses(tc.addresses, tc.weights)
if tc.expectpass {
// Confirm there is no error
require.Nil(t, err)
if tc.expectedErr != nil {
// Confirm the error
require.EqualError(t, err, tc.expectedErr.Error())
} else {
require.NoError(t, err)
// Check addresses, weights
require.Equal(t, tc.addresses, weightedAddresses.Addresses)
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 (
// ErrInvalidInitialAuctionID error for when the 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = sdkerrors.Register(ModuleName, 13, "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")
ErrLotTooLarge = sdkerrors.Register(ModuleName, 12, "lot is greater than auction's max new lot amount")
)

View File

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

View File

@ -76,7 +76,7 @@ func (gs GenesisState) Validate() error {
ids[a.GetID()] = true
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

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"
"github.com/stretchr/testify/require"
sdk "github.com/cosmos/cosmos-sdk/types"
)
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
)
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
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
partialDep := newPartialDeposit(dep.Depositor, partialCollateral, partialDebt)
// append it to the partial deposits
partialAuctionDeposits = append(partialAuctionDeposits, partialDep)
// create an auction from the partial deposits
debtChange, collateralChange, err := k.CreateAuctionFromPartialDeposits(ctx, partialAuctionDeposits, debt, totalCollateral, 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)
auctionSize := k.getAuctionSize(ctx, deposits[0].Amount.Denom)
totalCollateral := deposits.SumCollateral()
for _, deposit := range deposits {
debtCoveredByDeposit := (sdk.NewDecFromInt(deposit.Amount.Amount).Quo(sdk.NewDecFromInt(totalCollateral))).Mul(sdk.NewDecFromInt(debt)).RoundInt()
err := k.CreateAuctionsFromDeposit(ctx, deposit.Amount, deposit.Depositor, debtCoveredByDeposit, auctionSize, bidDenom)
if err != nil {
return err
}
@ -110,54 +27,38 @@ func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt
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(
ctx sdk.Context, dep types.Deposit, debt sdk.Int, totalCollateral sdk.Int, auctionSize sdk.Int,
principalDenom string) (debtChange sdk.Int, collateralChange sdk.Int, err error) {
debtChange = sdk.ZeroInt()
collateralChange = sdk.ZeroInt()
depositAmount := dep.Amount.Amount
depositDenom := dep.Amount.Denom
for depositAmount.GTE(auctionSize) {
// figure out how much debt is covered by one lots worth of collateral
depositDebtAmount := (sdk.NewDecFromInt(auctionSize).Quo(sdk.NewDecFromInt(totalCollateral))).Mul(sdk.NewDecFromInt(debt)).RoundInt()
penalty := k.ApplyLiquidationPenalty(ctx, depositDenom, depositDebtAmount)
// start an auction for one lot, attempting to raise depositDebtAmount plus the liquidation penalty
ctx sdk.Context, collateral sdk.Coin, returnAddr sdk.AccAddress, debt, auctionSize sdk.Int,
principalDenom string) (err error) {
amountToAuction := collateral.Amount
totalCollateralAmount := collateral.Amount
remainingDebt := debt
if !amountToAuction.IsPositive() {
return nil
}
for amountToAuction.GT(auctionSize) {
debtCoveredByAuction := (sdk.NewDecFromInt(auctionSize).Quo(sdk.NewDecFromInt(totalCollateralAmount))).Mul(sdk.NewDecFromInt(debt)).RoundInt()
penalty := k.ApplyLiquidationPenalty(ctx, collateral.Denom, debtCoveredByAuction)
_, err := k.auctionKeeper.StartCollateralAuction(
ctx, types.LiquidatorMacc, sdk.NewCoin(depositDenom, auctionSize), sdk.NewCoin(principalDenom, depositDebtAmount.Add(penalty)), []sdk.AccAddress{dep.Depositor},
[]sdk.Int{auctionSize}, sdk.NewCoin(k.GetDebtDenom(ctx), depositDebtAmount))
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), debtCoveredByAuction))
if err != nil {
return sdk.ZeroInt(), sdk.ZeroInt(), err
return err
}
depositAmount = depositAmount.Sub(auctionSize)
totalCollateral = totalCollateral.Sub(auctionSize)
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)
amountToAuction = amountToAuction.Sub(auctionSize)
remainingDebt = remainingDebt.Sub(debtCoveredByAuction)
}
return debtChange, collateralChange, nil
}
// CreateAuctionFromPartialDeposits creates an auction from the input partial deposits
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()))
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},
[]sdk.Int{amountToAuction}, sdk.NewCoin(k.GetDebtDenom(ctx), remainingDebt))
if err != nil {
return sdk.ZeroInt(), sdk.ZeroInt(), err
return err
}
debtChange = partialDeps.SumDebt()
collateralChange = partialDeps.SumCollateral()
return debtChange, collateralChange, nil
return nil
}
// 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,
// 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. moves debt coins from the cdp module to the liquidator module account,
// 4. decrements the total amount of principal outstanding for that collateral type
// 4. moves debt coins from the cdp module to the liquidator module account,
// 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)
func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) error {
// Calculate the previous collateral ratio