diff --git a/x/auction/abci_test.go b/x/auction/abci_test.go index 04a7efe7..7f69cab6 100644 --- a/x/auction/abci_test.go +++ b/x/auction/abci_test.go @@ -25,7 +25,7 @@ func TestKeeper_EndBlocker(t *testing.T) { tApp := app.NewTestApp() sellerAcc := supply.NewEmptyModuleAccount(sellerModName) - require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100)))) + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100), c("debt", 100)))) tApp.InitializeFromGenesisStates( NewAuthGenStateFromAccs(authexported.GenesisAccounts{ auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), @@ -36,7 +36,7 @@ func TestKeeper_EndBlocker(t *testing.T) { ctx := tApp.NewContext(true, abci.Header{}) keeper := tApp.GetAuctionKeeper() - auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights) + auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights, c("debt", 40)) require.NoError(t, err) require.NoError(t, keeper.PlaceBid(ctx, auctionID, buyer, c("token2", 30))) diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index cecbadd5..50979e26 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -31,13 +31,14 @@ func (k Keeper) StartSurplusAuction(ctx sdk.Context, seller string, lot sdk.Coin } // StartDebtAuction starts a new debt (reverse) auction. -func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin) (uint64, sdk.Error) { +func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, initialLot sdk.Coin, debt sdk.Coin) (uint64, sdk.Error) { auction := types.NewDebtAuction( buyer, bid, initialLot, - ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration)) + ctx.BlockTime().Add(k.GetParams(ctx).MaxAuctionDuration), + 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) @@ -45,6 +46,11 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in return 0, sdk.ErrInternal("module does not have minting permissions") } + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, buyer, types.ModuleName, sdk.NewCoins(debt)) + if err != nil { + return 0, err + } + auctionID, err := k.StoreNewAuction(ctx, auction) if err != nil { return 0, err @@ -53,18 +59,28 @@ func (k Keeper) StartDebtAuction(ctx sdk.Context, buyer string, bid sdk.Coin, in } // StartCollateralAuction starts a new collateral (2-phase) auction. -func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int) (uint64, sdk.Error) { +func (k Keeper) StartCollateralAuction(ctx sdk.Context, seller string, lot sdk.Coin, maxBid sdk.Coin, lotReturnAddrs []sdk.AccAddress, lotReturnWeights []sdk.Int, debt sdk.Coin) (uint64, sdk.Error) { weightedAddresses, err := types.NewWeightedAddresses(lotReturnAddrs, lotReturnWeights) if err != nil { return 0, err } - auction := types.NewCollateralAuction(seller, lot, ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), maxBid, weightedAddresses) + auction := types.NewCollateralAuction( + seller, + lot, + ctx.BlockTime().Add(types.DefaultMaxAuctionDuration), + maxBid, + weightedAddresses, + debt) err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(lot)) if err != nil { return 0, err } + err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, seller, types.ModuleName, sdk.NewCoins(debt)) + if err != nil { + return 0, err + } auctionID, err := k.StoreNewAuction(ctx, auction) if err != nil { @@ -184,10 +200,24 @@ func (k Keeper) PlaceForwardBidCollateral(ctx sdk.Context, a types.CollateralAuc } } // Increase in bid sent to auction initiator - err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, a.Initiator, sdk.NewCoins(bid.Sub(a.Bid))) + bidIncrement := bid.Sub(a.Bid) + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, bidder, a.Initiator, sdk.NewCoins(bidIncrement)) if err != nil { return a, err } + // Debt coins are sent to liquidator (until there is no CorrespondingDebt left). Amount sent is equal to bidIncrement. + if a.CorrespondingDebt.IsPositive() { + + debtAmountToReturn := sdk.MinInt(bidIncrement.Amount, a.CorrespondingDebt.Amount) + debtToReturn := sdk.NewCoin(a.CorrespondingDebt.Denom, debtAmountToReturn) + + err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(debtToReturn)) + if err != nil { + return a, err + } + a.CorrespondingDebt = a.CorrespondingDebt.Sub(debtToReturn) // debtToReturn will always be ≤ a.CorrespondingDebt from the MinInt above + // TODO optionally burn out debt and stable just returned to liquidator + } // Update Auction a.Bidder = bidder @@ -271,6 +301,19 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac return a, err } } + // Debt coins are sent to liquidator the first time a bid is placed. Amount sent is equal to Bid. + if a.Bidder.Equals(supply.NewModuleAddress(a.Initiator)) { + + debtAmountToReturn := sdk.MinInt(a.Bid.Amount, a.CorrespondingDebt.Amount) + debtToReturn := sdk.NewCoin(a.CorrespondingDebt.Denom, debtAmountToReturn) + + err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(debtToReturn)) + if err != nil { + return a, err + } + a.CorrespondingDebt = a.CorrespondingDebt.Sub(debtToReturn) // debtToReturn will always be ≤ a.CorrespondingDebt from the MinInt above + // TODO optionally burn out debt and stable just returned to liquidator + } // Update Auction a.Bidder = bidder @@ -324,6 +367,12 @@ func (k Keeper) PayoutDebtAuction(ctx sdk.Context, a types.DebtAuction) sdk.Erro if err != nil { return err } + if a.CorrespondingDebt.IsPositive() { + err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(a.CorrespondingDebt)) + if err != nil { + return err + } + } return nil } @@ -342,6 +391,12 @@ func (k Keeper) PayoutCollateralAuction(ctx sdk.Context, a types.CollateralAucti if err != nil { return err } + if a.CorrespondingDebt.IsPositive() { + err = k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, a.Initiator, sdk.NewCoins(a.CorrespondingDebt)) + if err != nil { + return err + } + } return nil } diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index 18b1971b..4ac3590d 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -69,27 +69,29 @@ func TestDebtAuctionBasic(t *testing.T) { tApp := app.NewTestApp() + buyerAcc := supply.NewEmptyModuleAccount(buyerModName, supply.Minter) // reverse auctions mint payout + require.NoError(t, buyerAcc.SetCoins(cs(c("debt", 100)))) tApp.InitializeFromGenesisStates( NewAuthGenStateFromAccs(authexported.GenesisAccounts{ auth.NewBaseAccount(seller, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), - supply.NewEmptyModuleAccount(buyerModName, supply.Minter), // reverse auctions mint payout + buyerAcc, }), ) ctx := tApp.NewContext(false, abci.Header{}) keeper := tApp.GetAuctionKeeper() // Start auction - auctionID, err := keeper.StartDebtAuction(ctx, buyerModName, c("token1", 20), c("token2", 99999)) // buyer, bid, initialLot + auctionID, err := keeper.StartDebtAuction(ctx, buyerModName, c("token1", 20), c("token2", 99999), c("debt", 20)) require.NoError(t, err) - // Check buyer's coins have not decreased, as lot is minted at the end - tApp.CheckBalance(t, ctx, buyerAddr, nil) // zero coins + // Check buyer's coins have not decreased (except for debt), as lot is minted at the end + tApp.CheckBalance(t, ctx, buyerAddr, cs(c("debt", 80))) // Place a bid require.NoError(t, keeper.PlaceBid(ctx, 0, seller, c("token2", 10))) // Check seller's coins have decreased tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 100))) // Check buyer's coins have increased - tApp.CheckBalance(t, ctx, buyerAddr, cs(c("token1", 20))) + tApp.CheckBalance(t, ctx, buyerAddr, cs(c("token1", 20), c("debt", 100))) // Close auction at just after auction expiry ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration)) @@ -109,7 +111,7 @@ func TestCollateralAuctionBasic(t *testing.T) { tApp := app.NewTestApp() sellerAcc := supply.NewEmptyModuleAccount(sellerModName) - require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100)))) + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100), c("debt", 100)))) tApp.InitializeFromGenesisStates( NewAuthGenStateFromAccs(authexported.GenesisAccounts{ auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), @@ -123,17 +125,17 @@ func TestCollateralAuctionBasic(t *testing.T) { keeper := tApp.GetAuctionKeeper() // Start auction - auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights) // seller, lot, maxBid, otherPerson + auctionID, err := keeper.StartCollateralAuction(ctx, sellerModName, c("token1", 20), c("token2", 50), returnAddrs, returnWeights, c("debt", 40)) require.NoError(t, err) // 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), c("debt", 60))) // Place a forward bid require.NoError(t, keeper.PlaceBid(ctx, 0, buyer, c("token2", 10))) // Check bidder's coins have decreased tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 90))) // 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), c("debt", 70))) // Check return addresses have not received coins for _, ra := range returnAddrs { tApp.CheckBalance(t, ctx, ra, cs(c("token1", 100), c("token2", 100))) @@ -145,7 +147,7 @@ func TestCollateralAuctionBasic(t *testing.T) { // Check bidder's coins have decreased tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 100), c("token2", 50))) // 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), c("debt", 100))) // Check return addresses have received coins 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))) diff --git a/x/auction/keeper/keeper_test.go b/x/auction/keeper/keeper_test.go index 03adaeeb..4b811bb9 100644 --- a/x/auction/keeper/keeper_test.go +++ b/x/auction/keeper/keeper_test.go @@ -74,8 +74,8 @@ func TestIterateAuctions(t *testing.T) { auctions := []types.Auction{ types.NewSurplusAuction("sellerMod", c("denom", 12345678), "anotherdenom", time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)).WithID(0), - types.NewDebtAuction("buyerMod", c("denom", 12345678), c("anotherdenom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC)).WithID(1), - types.NewCollateralAuction("sellerMod", c("denom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC), c("anotherdenom", 12345678), types.WeightedAddresses{}).WithID(2), + types.NewDebtAuction("buyerMod", c("denom", 12345678), c("anotherdenom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC), c("debt", 12345678)).WithID(1), + types.NewCollateralAuction("sellerMod", c("denom", 12345678), time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC), c("anotherdenom", 12345678), types.WeightedAddresses{}, c("debt", 12345678)).WithID(2), } for _, a := range auctions { keeper.SetAuction(ctx, a) diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index 1db5036e..beec6d24 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -73,25 +73,28 @@ func NewSurplusAuction(seller string, lot sdk.Coin, bidDenom string, endTime tim // It is normally used to acquire pegged asset to cover the CDP system's debts that were not covered by selling collateral. type DebtAuction struct { BaseAuction + CorrespondingDebt sdk.Coin } // WithID returns an auction with the ID set. func (a DebtAuction) WithID(id uint64) Auction { a.ID = id; return a } // NewDebtAuction returns a new debt auction. -func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime time.Time) DebtAuction { +func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, EndTime time.Time, debt sdk.Coin) DebtAuction { // Note: Bidder is set to the initiator's module account address instead of module name. (when the first bid is placed, it is paid out to the initiator) // Setting to the module account address bypasses calling supply.SendCoinsFromModuleToModule, instead calls SendCoinsFromModuleToAccount. // This isn't a problem currently, but if additional logic/validation was added for sending to coins to Module Accounts, it would be bypassed. - auction := DebtAuction{BaseAuction{ - // no ID - Initiator: buyerModAccName, - Lot: initialLot, - Bidder: supply.NewModuleAddress(buyerModAccName), // send proceeds from the first bid to the buyer. - Bid: bid, // amount that the buyer is buying - doesn't change over course of auction - EndTime: EndTime, - MaxEndTime: EndTime, - }} + auction := DebtAuction{ + BaseAuction: BaseAuction{ + // no ID + Initiator: buyerModAccName, + Lot: initialLot, + Bidder: supply.NewModuleAddress(buyerModAccName), // send proceeds from the first bid to the buyer. + Bid: bid, // amount that the buyer is buying - doesn't change over course of auction + EndTime: EndTime, + MaxEndTime: EndTime}, + CorrespondingDebt: debt, + } return auction } @@ -102,8 +105,9 @@ func NewDebtAuction(buyerModAccName string, bid sdk.Coin, initialLot sdk.Coin, E // Collateral auctions are normally used to sell off collateral seized from CDPs. type CollateralAuction struct { BaseAuction - MaxBid sdk.Coin - LotReturns WeightedAddresses + CorrespondingDebt sdk.Coin + MaxBid sdk.Coin + LotReturns WeightedAddresses } // WithID returns an auction with the ID set. @@ -132,7 +136,7 @@ func (a CollateralAuction) String() string { } // NewCollateralAuction returns a new collateral auction. -func NewCollateralAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses) CollateralAuction { +func NewCollateralAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid sdk.Coin, lotReturns WeightedAddresses, debt sdk.Coin) CollateralAuction { auction := CollateralAuction{ BaseAuction: BaseAuction{ // no ID @@ -142,8 +146,9 @@ func NewCollateralAuction(seller string, lot sdk.Coin, EndTime time.Time, maxBid Bid: sdk.NewInt64Coin(maxBid.Denom, 0), EndTime: EndTime, MaxEndTime: EndTime}, - MaxBid: maxBid, - LotReturns: lotReturns, + CorrespondingDebt: debt, + MaxBid: maxBid, + LotReturns: lotReturns, } return auction }