add payout to depositors feature

This commit is contained in:
rhuairahrighairigh 2019-12-31 11:10:15 +00:00
parent 8a4109ff26
commit 77bfe11f89
8 changed files with 191 additions and 23 deletions

View File

@ -97,8 +97,9 @@ func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) T
} }
func (tApp TestApp) CheckBalance(t *testing.T, ctx sdk.Context, owner sdk.AccAddress, expectedCoins sdk.Coins) { func (tApp TestApp) CheckBalance(t *testing.T, ctx sdk.Context, owner sdk.AccAddress, expectedCoins sdk.Coins) {
actualCoins := tApp.GetAccountKeeper().GetAccount(ctx, owner).GetCoins() acc := tApp.GetAccountKeeper().GetAccount(ctx, owner)
require.Equal(t, expectedCoins, actualCoins) require.NotNilf(t, acc, "account with address '%s' doesn't exist", owner)
require.Equal(t, expectedCoins, acc.GetCoins())
} }
// Create a new auth genesis state from some addresses and coins. The state is returned marshalled into a map. // Create a new auth genesis state from some addresses and coins. The state is returned marshalled into a map.

View File

@ -19,7 +19,8 @@ func TestKeeper_EndBlocker(t *testing.T) {
// Setup // Setup
_, addrs := app.GeneratePrivKeyAddressPairs(2) _, addrs := app.GeneratePrivKeyAddressPairs(2)
buyer := addrs[0] buyer := addrs[0]
recipient := addrs[1] returnAddrs := addrs[1:]
returnWeights := []sdk.Int{sdk.NewInt(1)}
sellerModName := liquidator.ModuleName sellerModName := liquidator.ModuleName
//sellerAddr := supply.NewModuleAddress(sellerModName) //sellerAddr := supply.NewModuleAddress(sellerModName)
@ -36,7 +37,7 @@ func TestKeeper_EndBlocker(t *testing.T) {
ctx := tApp.NewContext(true, abci.Header{}) ctx := tApp.NewContext(true, abci.Header{})
keeper := tApp.GetAuctionKeeper() keeper := tApp.GetAuctionKeeper()
auctionID, err := keeper.StartForwardReverseAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), recipient) auctionID, err := keeper.StartForwardReverseAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights)
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 30), c("token1", 20))) require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 30), c("token1", 20)))

View File

@ -46,12 +46,16 @@ func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer string, bid sdk.Coin,
} }
// StartForwardReverseAuction starts an auction where bidders bid up to a maxBid, then switch to bidding down on price. // StartForwardReverseAuction starts an auction where bidders bid up to a maxBid, then switch to bidding down on price.
func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, otherPerson sdk.AccAddress) (types.ID, sdk.Error) { func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int) (types.ID, sdk.Error) {
// create auction // create auction
auction := types.NewForwardReverseAuction(seller, lot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), maxBid, otherPerson) weightedAddresses, err := types.NewWeightedAddresses(lotReturnAddrs, lotReturnWeights)
if err != nil {
return 0, err
}
auction := types.NewForwardReverseAuction(seller, lot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), maxBid, weightedAddresses)
// take coins from module account // take coins from module account
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
} }
@ -206,10 +210,17 @@ func (k Keeper) PlaceBidForwardReverse(ctx sdk.Context, a types.ForwardReverseAu
if err != nil { if err != nil {
return a, err return a, err
} }
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.OtherPerson, sdk.NewCoins(lotDecrement)) // FIXME paying out rateably to cdp depositors is vulnerable to errors compounding over multiple bids
lotPayouts, err := splitCoinIntoWeightedBuckets(lotDecrement, a.LotReturns.Weights)
if err != nil { if err != nil {
return a, err return a, err
} }
for i, payout := range lotPayouts {
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, a.LotReturns.Addresses[i], sdk.NewCoins(payout))
if err != nil {
return a, err
}
}
// Update Auction // Update Auction
a.Bidder = bidder a.Bidder = bidder
@ -318,3 +329,17 @@ func earliestTime(t1, t2 time.Time) time.Time {
return t2 // also returned if times are equal return t2 // also returned if times are equal
} }
} }
func splitCoinIntoWeightedBuckets(coin sdk.Coin, buckets []sdk.Int) ([]sdk.Coin, sdk.Error) {
for _, bucket := range buckets {
if bucket.IsNegative() {
return nil, sdk.ErrInternal("cannot split coin into bucket with negative weight")
}
}
amounts := splitIntIntoWeightedBuckets(coin.Amount, buckets)
result := make([]sdk.Coin, len(amounts))
for i, a := range amounts {
result[i] = sdk.NewCoin(coin.Denom, a)
}
return result, nil
}

View File

@ -3,6 +3,7 @@ package keeper_test
import ( import (
"testing" "testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/supply" "github.com/cosmos/cosmos-sdk/x/supply"
@ -94,9 +95,10 @@ func TestReverseAuctionBasic(t *testing.T) {
func TestForwardReverseAuctionBasic(t *testing.T) { func TestForwardReverseAuctionBasic(t *testing.T) {
// Setup // Setup
_, addrs := app.GeneratePrivKeyAddressPairs(2) _, addrs := app.GeneratePrivKeyAddressPairs(4)
buyer := addrs[0] buyer := addrs[0]
recipient := addrs[1] returnAddrs := addrs[1:]
returnWeights := []sdk.Int{i(30), i(20), i(10)}
sellerModName := liquidator.ModuleName sellerModName := liquidator.ModuleName
sellerAddr := supply.NewModuleAddress(sellerModName) sellerAddr := supply.NewModuleAddress(sellerModName)
@ -106,7 +108,9 @@ func TestForwardReverseAuctionBasic(t *testing.T) {
tApp.InitializeFromGenesisStates( tApp.InitializeFromGenesisStates(
NewAuthGenStateFromAccs(authexported.GenesisAccounts{ NewAuthGenStateFromAccs(authexported.GenesisAccounts{
auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
auth.NewBaseAccount(recipient, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), auth.NewBaseAccount(returnAddrs[0], cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
auth.NewBaseAccount(returnAddrs[1], cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
auth.NewBaseAccount(returnAddrs[2], cs(c("token1", 100), c("token2", 100)), nil, 0, 0),
sellerAcc, sellerAcc,
}), }),
) )
@ -114,7 +118,7 @@ func TestForwardReverseAuctionBasic(t *testing.T) {
keeper := tApp.GetAuctionKeeper() keeper := tApp.GetAuctionKeeper()
// Start auction // Start auction
auctionID, err := keeper.StartForwardReverseAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), recipient) // seller, lot, maxBid, otherPerson auctionID, err := keeper.StartForwardReverseAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights) // seller, lot, maxBid, otherPerson
require.NoError(t, err) require.NoError(t, err)
// Check seller's coins have decreased // Check seller's coins have decreased
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100))) tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 100)))
@ -125,8 +129,10 @@ func TestForwardReverseAuctionBasic(t *testing.T) {
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90)))
// Check seller's coins have increased // Check seller's coins have increased
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 110))) tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 110)))
// Check recipient has not received coins // Check return addresses have not received coins
tApp.CheckBalance(t, ctx, recipient, cs(c("token1", 100), c("token2", 100))) for _, ra := range returnAddrs {
tApp.CheckBalance(t, ctx, ra, cs(c("token1", 100), c("token2", 100)))
}
// Place a reverse bid // Place a reverse bid
require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50), c("token1", 15))) // bid, lot require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 50), c("token1", 15))) // bid, lot
@ -134,8 +140,10 @@ func TestForwardReverseAuctionBasic(t *testing.T) {
tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50))) tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50)))
// Check seller's coins have increased // Check seller's coins have increased
tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 150))) tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 150)))
// Check "recipient" has received coins // Check return addresses have received coins
tApp.CheckBalance(t, ctx, recipient, cs(c("token1", 105), c("token2", 100))) tApp.CheckBalance(t, ctx, returnAddrs[0], cs(c("token1", 102), c("token2", 100)))
tApp.CheckBalance(t, ctx, returnAddrs[1], cs(c("token1", 102), c("token2", 100)))
tApp.CheckBalance(t, ctx, returnAddrs[2], cs(c("token1", 101), c("token2", 100)))
// Close auction at just after auction expiry // Close auction at just after auction expiry
ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration)) ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration))

View File

@ -10,6 +10,13 @@ import (
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
func i(n int64) sdk.Int { return sdk.NewInt(n) }
func is(ns ...int64) (is []sdk.Int) {
for _, n := range ns {
is = append(is, sdk.NewInt(n))
}
return
}
func NewAuthGenStateFromAccs(accounts authexported.GenesisAccounts) app.GenesisState { func NewAuthGenStateFromAccs(accounts authexported.GenesisAccounts) app.GenesisState {
authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts) authGenesis := auth.NewGenesisState(auth.DefaultParams(), accounts)

69
x/auction/keeper/math.go Normal file
View File

@ -0,0 +1,69 @@
package keeper
import (
"sort"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// splitIntIntoWeightedBuckets divides an initial +ve integer among several buckets in proportion to the buckets' weights
// It uses the largest remainder method:
// https://en.wikipedia.org/wiki/Largest_remainder_method
// see also: https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100
func splitIntIntoWeightedBuckets(amount sdk.Int, buckets []sdk.Int) []sdk.Int {
// TODO ideally change algorithm to work with -ve numbers. Limiting to +ve numbers until them
if amount.IsNegative() {
panic("negative amount")
}
for _, bucket := range buckets {
if bucket.IsNegative() {
panic("negative bucket")
}
}
totalWeights := totalInts(buckets...)
// split amount by weights, recording whole number part and remainder
quotients := make([]quoRem, len(buckets))
for i := range buckets {
q := amount.Mul(buckets[i]).Quo(totalWeights)
r := amount.Mul(buckets[i]).Mod(totalWeights)
quotients[i] = quoRem{index: i, quo: q, rem: r}
}
// apportion left over to buckets with the highest remainder (to minimize error)
sort.Slice(quotients, func(i, j int) bool {
return quotients[i].rem.GT(quotients[j].rem) // decreasing remainder order
})
allocated := sdk.ZeroInt()
for _, qr := range quotients {
allocated = allocated.Add(qr.quo)
}
leftToAllocate := amount.Sub(allocated)
results := make([]sdk.Int, len(quotients))
for _, qr := range quotients {
results[qr.index] = qr.quo
if !leftToAllocate.IsZero() {
results[qr.index] = results[qr.index].Add(sdk.OneInt())
leftToAllocate = leftToAllocate.Sub(sdk.OneInt())
}
}
return results
}
type quoRem struct {
index int
quo sdk.Int
rem sdk.Int
}
// totalInts adds together sdk.Ints
func totalInts(is ...sdk.Int) sdk.Int {
total := sdk.ZeroInt()
for _, i := range is {
total = total.Add(i)
}
return total
}

View File

@ -0,0 +1,37 @@
package keeper
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
func TestSplitIntIntoWeightedBuckets(t *testing.T) {
testCases := []struct {
name string
amount sdk.Int
buckets []sdk.Int
want []sdk.Int
}{
{"2split1,1", i(2), is(1, 1), is(1, 1)},
{"100split1,9", i(100), is(1, 9), is(10, 90)},
{"7split1,2", i(7), is(1, 2), is(2, 5)},
{"17split1,1,1", i(17), is(1, 1, 1), is(6, 6, 5)},
// TODO more tests
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := splitIntIntoWeightedBuckets(tc.amount, tc.buckets)
require.Equal(t, tc.want, got)
})
}
}
func i(n int64) sdk.Int { return sdk.NewInt(n) }
func is(ns ...int64) (is []sdk.Int) {
for _, n := range ns {
is = append(is, sdk.NewInt(n))
}
return
}

View File

@ -133,8 +133,8 @@ func NewReverseAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin
// ForwardReverseAuction type for forward reverse auction // ForwardReverseAuction type for forward reverse auction
type ForwardReverseAuction struct { type ForwardReverseAuction struct {
BaseAuction BaseAuction
MaxBid sdk.Coin MaxBid sdk.Coin
OtherPerson sdk.AccAddress // TODO rename, this is normally the original CDP owner, will have to be updated to account for deposits LotReturns WeightedAddresses // return addresses to pay out reductions in the lot amount to. Lot is bid down during reverse phase.
} }
// WithID returns an auction with the ID set // WithID returns an auction with the ID set
@ -149,15 +149,15 @@ func (a ForwardReverseAuction) String() string {
End Time: %s End Time: %s
Max End Time: %s Max End Time: %s
Max Bid %s Max Bid %s
Other Person %s`, LotReturns %s`,
a.GetID(), a.Initiator, a.Lot, a.GetID(), a.Initiator, a.Lot,
a.Bidder, a.Bid, a.GetEndTime().String(), a.Bidder, a.Bid, a.GetEndTime().String(),
a.MaxEndTime.String(), a.MaxBid, a.OtherPerson, a.MaxEndTime.String(), a.MaxBid, a.LotReturns,
) )
} }
// NewForwardReverseAuction creates a new forward reverse auction // NewForwardReverseAuction creates a new forward reverse auction
func NewForwardReverseAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, otherPerson sdk.AccAddress) ForwardReverseAuction { func NewForwardReverseAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses) ForwardReverseAuction {
auction := ForwardReverseAuction{ auction := ForwardReverseAuction{
BaseAuction: BaseAuction{ BaseAuction: BaseAuction{
// no ID // no ID
@ -167,8 +167,28 @@ func NewForwardReverseAuction(seller string, lot sdk.Coin, EndTime time.Time, ma
Bid: sdk.NewInt64Coin(maxBid.Denom, 0), Bid: sdk.NewInt64Coin(maxBid.Denom, 0),
EndTime: EndTime, EndTime: EndTime,
MaxEndTime: EndTime}, MaxEndTime: EndTime},
MaxBid: maxBid, MaxBid: maxBid,
OtherPerson: otherPerson, LotReturns: lotReturns,
} }
return auction return auction
} }
type WeightedAddresses struct {
Addresses []sdk.AccAddress
Weights []sdk.Int
}
func NewWeightedAddresses(addrs []sdk.AccAddress, weights []sdk.Int) (WeightedAddresses, sdk.Error) {
if len(addrs) != len(weights) {
return WeightedAddresses{}, sdk.ErrInternal("number of addresses doesn't match number of weights")
}
for _, w := range weights {
if w.IsNegative() {
return WeightedAddresses{}, sdk.ErrInternal("weights contain a negative amount")
}
}
return WeightedAddresses{
Addresses: addrs,
Weights: weights,
}, nil
}