Refactor CreateAuctionsFromDeposit (#589)

* refactor CreateAuctionsFromDeposit to complete exhibits 7 & 8 by
removing auction slices and simplify usage of largest remainder method
This commit is contained in:
Nick DeLuca 2020-06-16 10:02:47 -05:00 committed by GitHub
parent db2b237e1d
commit f0e73e4aa8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 62 additions and 99 deletions

View File

@ -30,36 +30,80 @@ func (k Keeper) AuctionCollateral(ctx sdk.Context, deposits types.Deposits, debt
// CreateAuctionsFromDeposit creates auctions from the input deposit // CreateAuctionsFromDeposit creates auctions from the input deposit
func (k Keeper) CreateAuctionsFromDeposit( func (k Keeper) CreateAuctionsFromDeposit(
ctx sdk.Context, collateral sdk.Coin, returnAddr sdk.AccAddress, debt, auctionSize sdk.Int, ctx sdk.Context, collateral sdk.Coin, returnAddr sdk.AccAddress, debt, auctionSize sdk.Int,
principalDenom string) (err error) { principalDenom string) error {
// the number of auctions to start with lot = auctionSize // number of auctions of auctionSize
wholeAuctions := collateral.Amount.Quo(auctionSize) numberOfAuctions := collateral.Amount.Quo(auctionSize)
// remaining collateral (< lot) to auction debtPerAuction := debt.Mul(auctionSize).Quo(collateral.Amount)
partialAuctionAmount := collateral.Amount.Mod(auctionSize)
auctionLots := []sdk.Int{}
for i := int64(0); i < wholeAuctions.Int64(); i++ { // last auction for remaining collateral (collateral < auctionSize)
auctionLots = append(auctionLots, auctionSize) lastAuctionCollateral := collateral.Amount.Mod(auctionSize)
lastAuctionDebt := debt.Mul(lastAuctionCollateral).Quo(collateral.Amount)
// amount of debt that has not been allocated due to
// rounding error (unallocated debt is less than numberOfAuctions + 1)
unallocatedDebt := debt.Sub(numberOfAuctions.Mul(debtPerAuction).Add(lastAuctionDebt))
// rounding error for whole and last auctions in units of collateral
// higher value means a larger truncation
wholeAuctionError := debt.Mul(auctionSize).Mod(collateral.Amount)
lastAuctionError := debt.Mul(lastAuctionCollateral).Mod(collateral.Amount)
// if last auction has larger rounding error, then allocate one debt to last auction first
// follows the largest remainder method https://en.wikipedia.org/wiki/Largest_remainder_method
if lastAuctionError.GT(wholeAuctionError) {
lastAuctionDebt = lastAuctionDebt.Add(sdk.OneInt())
unallocatedDebt = unallocatedDebt.Sub(sdk.OneInt())
} }
if partialAuctionAmount.IsPositive() {
auctionLots = append(auctionLots, partialAuctionAmount)
}
// use the auction lots as weights to split the debt into buckets,
// where each bucket represents how much debt that auction will attempt to cover
debtAmounts := splitIntIntoWeightedBuckets(debt, auctionLots)
debtDenom := k.GetDebtDenom(ctx) debtDenom := k.GetDebtDenom(ctx)
for i, debtAmount := range debtAmounts { numAuctions := numberOfAuctions.Int64()
// create whole auctions
for i := int64(0); i < numAuctions; i++ {
debtAmount := debtPerAuction
// distribute unallocated debt left over starting with first auction created
if unallocatedDebt.IsPositive() {
debtAmount = debtAmount.Add(sdk.OneInt())
unallocatedDebt = unallocatedDebt.Sub(sdk.OneInt())
}
penalty := k.ApplyLiquidationPenalty(ctx, collateral.Denom, debtAmount) penalty := k.ApplyLiquidationPenalty(ctx, collateral.Denom, debtAmount)
_, err := k.auctionKeeper.StartCollateralAuction( _, err := k.auctionKeeper.StartCollateralAuction(
ctx, types.LiquidatorMacc, sdk.NewCoin(collateral.Denom, auctionLots[i]), ctx, types.LiquidatorMacc, sdk.NewCoin(collateral.Denom, auctionSize),
sdk.NewCoin(principalDenom, debtAmount.Add(penalty)), []sdk.AccAddress{returnAddr}, sdk.NewCoin(principalDenom, debtAmount.Add(penalty)), []sdk.AccAddress{returnAddr},
[]sdk.Int{auctionLots[i]}, sdk.NewCoin(debtDenom, debtAmount), []sdk.Int{auctionSize}, sdk.NewCoin(debtDenom, debtAmount),
) )
if err != nil { if err != nil {
return err return err
} }
} }
return nil
// skip last auction if there is no collateral left to auction
if !lastAuctionCollateral.IsPositive() {
return nil
}
// if the last auction had a larger rounding error than whole auctions,
// then unallocatedDebt will be zero since we will have already distributed
// all of the unallocated debt
if unallocatedDebt.IsPositive() {
lastAuctionDebt = lastAuctionDebt.Add(sdk.OneInt())
unallocatedDebt = unallocatedDebt.Sub(sdk.OneInt())
}
penalty := k.ApplyLiquidationPenalty(ctx, collateral.Denom, lastAuctionDebt)
_, err := k.auctionKeeper.StartCollateralAuction(
ctx, types.LiquidatorMacc, sdk.NewCoin(collateral.Denom, lastAuctionCollateral),
sdk.NewCoin(principalDenom, lastAuctionDebt.Add(penalty)), []sdk.AccAddress{returnAddr},
[]sdk.Int{lastAuctionCollateral}, sdk.NewCoin(debtDenom, lastAuctionDebt),
)
return err
} }
// 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

@ -1,81 +0,0 @@
package keeper
import (
"sort"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// splitIntIntoWeightedBuckets divides an initial +ve integer among several buckets in proportion to the buckets' weights
// It uses the largest remainder method: https://en.wikipedia.org/wiki/Largest_remainder_method
// See also: https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100
// note - copied from auction, tests are located there.
func splitIntIntoWeightedBuckets(amount sdk.Int, buckets []sdk.Int) []sdk.Int {
// 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")
}
}
// 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")
}
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}
}
// 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)
})
// 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
if !leftToAllocate.IsZero() {
results[qr.index] = results[qr.index].Add(sdk.OneInt())
leftToAllocate = leftToAllocate.Sub(sdk.OneInt())
}
}
return results
}
type quoRem struct {
index int
quo sdk.Int
rem sdk.Int
}
// totalInts adds together sdk.Ints
func totalInts(is ...sdk.Int) sdk.Int {
total := sdk.ZeroInt()
for _, i := range is {
total = total.Add(i)
}
return total
}