From 8128a680cc8c36c23d28c2ec183aea2377047419 Mon Sep 17 00:00:00 2001 From: Kevin Davis Date: Tue, 14 Jan 2020 16:04:47 +0100 Subject: [PATCH] Ro address auction todos (#284) * make auctions not expire without bids * add events * improve genesis state validation * add genesis tests * Keeper auctions test, types auctions test, keeper bidding test * Resolved TODOs, added querier test * Removed 'import x/liquidator' from keeper_test package for circleci * Fixes for lack of liquidator module account in tests * update comment Co-Authored-By: Kevin Davis * add more events attributes * feat: add back bidding on closed auction test * feat: test failed debt/collateral auctions Co-authored-by: Ruaridh Co-authored-by: Denali Marsh --- x/auction/keeper/auctions.go | 6 - x/auction/keeper/auctions_test.go | 160 ++++++++++++++ x/auction/keeper/bidding_test.go | 349 ++++++++++++++++++++++++++++++ x/auction/keeper/querier.go | 5 +- x/auction/keeper/querier_test.go | 104 +++++++++ x/auction/types/auctions.go | 19 ++ x/auction/types/auctions_test.go | 195 +++++++++++++++++ x/auction/types/keys.go | 2 + x/auction/types/querier.go | 14 ++ 9 files changed, 845 insertions(+), 9 deletions(-) create mode 100644 x/auction/keeper/bidding_test.go create mode 100644 x/auction/keeper/querier_test.go create mode 100644 x/auction/types/auctions_test.go diff --git a/x/auction/keeper/auctions.go b/x/auction/keeper/auctions.go index b47313ce..e1465668 100644 --- a/x/auction/keeper/auctions.go +++ b/x/auction/keeper/auctions.go @@ -294,9 +294,6 @@ func (k Keeper) PlaceReverseBidCollateral(ctx sdk.Context, a types.CollateralAuc if !a.IsReversePhase() { return a, sdk.ErrInternal("auction not in reverse phase") } - if lot.IsNegative() { - return a, sdk.ErrInternal("can't bid negative amount") - } if !lot.IsLT(a.Lot) { return a, sdk.ErrInternal("auction in reverse phase, new bid not less than previous amount") } @@ -354,9 +351,6 @@ func (k Keeper) PlaceBidDebt(ctx sdk.Context, a types.DebtAuction, bidder sdk.Ac if lot.Denom != a.Lot.Denom { return a, sdk.ErrInternal("lot denom doesn't match auction") } - if lot.IsNegative() { - return a, sdk.ErrInternal("lot less than 0") - } if !lot.IsLT(a.Lot) { return a, sdk.ErrInternal("lot not smaller than last lot") } diff --git a/x/auction/keeper/auctions_test.go b/x/auction/keeper/auctions_test.go index 3b0bc658..3b7fd5d4 100644 --- a/x/auction/keeper/auctions_test.go +++ b/x/auction/keeper/auctions_test.go @@ -100,6 +100,48 @@ func TestDebtAuctionBasic(t *testing.T) { tApp.CheckBalance(t, ctx, seller, cs(c("token1", 80), c("token2", 110))) } +func TestDebtAuctionDebtRemaining(t *testing.T) { + // Setup + _, addrs := app.GeneratePrivKeyAddressPairs(1) + seller := addrs[0] + buyerModName := cdp.LiquidatorMacc + buyerAddr := supply.NewModuleAddress(buyerModName) + + 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), + buyerAcc, + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Start auction + auctionID, err := keeper.StartDebtAuction(ctx, buyerModName, c("token1", 10), c("token2", 99999), c("debt", 20)) + require.NoError(t, err) + // 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", 90), c("token2", 100))) + // Check buyer's coins have increased + tApp.CheckBalance(t, ctx, buyerAddr, cs(c("token1", 10), c("debt", 90))) + + // Close auction at just after auction expiry + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration)) + require.NoError(t, keeper.CloseAuction(ctx, auctionID)) + // Check seller's coins increased + tApp.CheckBalance(t, ctx, seller, cs(c("token1", 90), c("token2", 110))) + // check that debt has increased due to corresponding debt being greater than bid + tApp.CheckBalance(t, ctx, buyerAddr, cs(c("token1", 10), c("debt", 100))) +} + func TestCollateralAuctionBasic(t *testing.T) { // Setup _, addrs := app.GeneratePrivKeyAddressPairs(4) @@ -160,6 +202,59 @@ func TestCollateralAuctionBasic(t *testing.T) { tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 115), c("token2", 50))) } +func TestCollateralAuctionDebtRemaining(t *testing.T) { + // Setup + _, addrs := app.GeneratePrivKeyAddressPairs(4) + buyer := addrs[0] + returnAddrs := addrs[1:] + returnWeights := is(30, 20, 10) + sellerModName := cdp.LiquidatorMacc + sellerAddr := supply.NewModuleAddress(sellerModName) + + tApp := app.NewTestApp() + sellerAcc := supply.NewEmptyModuleAccount(sellerModName) + 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), + 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, + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Start auction + 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), 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), 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))) + } + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultBidDuration)) + require.NoError(t, keeper.CloseAuction(ctx, auctionID)) + + // check that buyers coins have increased + tApp.CheckBalance(t, ctx, buyer, cs(c("token1", 120), c("token2", 90))) + // Check return addresses have not received coins + for _, ra := range returnAddrs { + tApp.CheckBalance(t, ctx, ra, cs(c("token1", 100), c("token2", 100))) + } + // check that token2 has increased by 10, debt by 40, for a net debt increase of 30 debt + tApp.CheckBalance(t, ctx, sellerAddr, cs(c("token1", 80), c("token2", 110), c("debt", 100))) +} + func TestStartSurplusAuction(t *testing.T) { someTime := time.Date(1998, time.January, 1, 0, 0, 0, 0, time.UTC) type args struct { @@ -247,3 +342,68 @@ func TestStartSurplusAuction(t *testing.T) { }) } } + +func TestCloseAuction(t *testing.T) { + // Set up + _, addrs := app.GeneratePrivKeyAddressPairs(1) + buyer := addrs[0] + sellerModName := cdp.LiquidatorMacc + + tApp := app.NewTestApp() + + sellerAcc := supply.NewEmptyModuleAccount(sellerModName, supply.Burner) // forward auctions burn proceeds + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100)))) + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + sellerAcc, + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Create an auction (lot: 20 token1, initialBid: 0 token2) + id, err := keeper.StartSurplusAuction(ctx, sellerModName, c("token1", 20), "token2") // lot, bid denom + require.NoError(t, err) + + // Attempt to close the auction before EndTime + require.Error(t, keeper.CloseAuction(ctx, id)) + + // Attempt to close auction that does not exist + require.Error(t, keeper.CloseAuction(ctx, 999)) +} + +func TestCloseExpiredAuctions(t *testing.T) { + // Set up + _, addrs := app.GeneratePrivKeyAddressPairs(1) + buyer := addrs[0] + sellerModName := "liquidator" + + tApp := app.NewTestApp() + + sellerAcc := supply.NewEmptyModuleAccount(sellerModName, supply.Burner) // forward auctions burn proceeds + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 100), c("token2", 100)))) + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 100), c("token2", 100)), nil, 0, 0), + sellerAcc, + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + // Start auction 1 + _, err := keeper.StartSurplusAuction(ctx, sellerModName, c("token1", 20), "token2") // lot, bid denom + require.NoError(t, err) + + // Start auction 2 + _, err = keeper.StartSurplusAuction(ctx, sellerModName, c("token1", 20), "token2") // lot, bid denom + require.NoError(t, err) + + // Fast forward the block time + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(types.DefaultMaxAuctionDuration).Add(1)) + + // Close expired auctions + err = keeper.CloseExpiredAuctions(ctx) + require.NoError(t, err) +} diff --git a/x/auction/keeper/bidding_test.go b/x/auction/keeper/bidding_test.go new file mode 100644 index 00000000..7703f86e --- /dev/null +++ b/x/auction/keeper/bidding_test.go @@ -0,0 +1,349 @@ +package keeper_test + +import ( + "strings" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + "github.com/cosmos/cosmos-sdk/x/supply" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/auction/types" +) + +type AuctionType int + +const ( + Invalid AuctionType = 0 + Surplus AuctionType = 1 + Debt AuctionType = 2 + CollateralPhase1 AuctionType = 3 + CollateralPhase2 AuctionType = 4 +) + +func TestAuctionBidding(t *testing.T) { + someTime := time.Date(0001, time.January, 1, 0, 0, 0, 0, time.UTC) + + _, addrs := app.GeneratePrivKeyAddressPairs(5) + buyer := addrs[0] + secondBuyer := addrs[1] + modName := "liquidator" + collateralAddrs := addrs[2:] + collateralWeights := is(30, 20, 10) + + tApp := app.NewTestApp() + + // Set up seller account + sellerAcc := supply.NewEmptyModuleAccount(modName, supply.Minter, supply.Burner) + require.NoError(t, sellerAcc.SetCoins(cs(c("token1", 1000), c("token2", 1000), c("debt", 1000)))) + + // Initialize genesis accounts + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + auth.NewBaseAccount(secondBuyer, cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + auth.NewBaseAccount(collateralAddrs[0], cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + auth.NewBaseAccount(collateralAddrs[1], cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + auth.NewBaseAccount(collateralAddrs[2], cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + sellerAcc, + }), + ) + ctx := tApp.NewContext(false, abci.Header{}) + keeper := tApp.GetAuctionKeeper() + + type auctionArgs struct { + auctionType AuctionType + seller string + lot sdk.Coin + bid sdk.Coin + debt sdk.Coin + addresses []sdk.AccAddress + weights []sdk.Int + } + + type bidArgs struct { + bidder sdk.AccAddress + amount sdk.Coin + secondBidder sdk.AccAddress + } + + tests := []struct { + name string + auctionArgs auctionArgs + bidArgs bidArgs + expectedError string + expectedEndTime time.Time + expectedBidder sdk.AccAddress + expectedBid sdk.Coin + expectpass bool + }{ + { + "basic: auction doesn't exist", + auctionArgs{Surplus, "", c("token1", 1), c("token2", 1), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, + bidArgs{buyer, c("token2", 10), nil}, + "auction doesn't exist", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + false, + }, + { + "surplus: normal", + auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, + bidArgs{buyer, c("token2", 10), nil}, + "", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + true, + }, + { + "surplus: second bidder", + auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, + bidArgs{buyer, c("token2", 10), secondBuyer}, + "", + someTime.Add(types.DefaultBidDuration), + secondBuyer, + c("token2", 11), + true, + }, + { + "surplus: invalid bid denom", + auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, + bidArgs{buyer, c("badtoken", 10), nil}, + "bid denom doesn't match auction", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + false, + }, + { + "surplus: invalid bid (equal)", + auctionArgs{Surplus, modName, c("token1", 100), c("token2", 0), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, + bidArgs{buyer, c("token2", 0), nil}, + "bid not greater than last bid", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + false, + }, + { + "debt: normal", + auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot + bidArgs{buyer, c("token1", 10), nil}, + "", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 100), + true, + }, + { + "debt: second bidder", + auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot + bidArgs{buyer, c("token1", 10), secondBuyer}, + "", + someTime.Add(types.DefaultBidDuration), + secondBuyer, + c("token2", 100), + true, + }, + { + "debt: invalid lot denom", + auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot + bidArgs{buyer, c("badtoken", 10), nil}, + "lot denom doesn't match auction", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token1", 20), + false, + }, + { + "debt: invalid lot size (larger)", + auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 20), []sdk.AccAddress{}, []sdk.Int{}}, + bidArgs{buyer, c("token1", 21), nil}, + "lot not smaller than last lot", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token1", 20), + false, + }, + { + "collateral [forward]: normal", + auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token2", 10), nil}, + "", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + true, + }, + { + "collateral [forward]: second bidder", + auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token2", 10), secondBuyer}, + "", + someTime.Add(types.DefaultBidDuration), + secondBuyer, + c("token2", 11), + true, + }, + { + "collateral [forward]: invalid bid denom", + auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("badtoken", 10), nil}, + "bid denom doesn't match auction", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + false, + }, + { + "collateral [forward]: invalid bid size (smaller)", + auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token2", 0), nil}, // lot, bid + "auction in forward phase, new bid not higher than last bid", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + false, + }, + { + "collateral [forward]: invalid bid size (greater than max)", + auctionArgs{CollateralPhase1, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token2", 101), nil}, // lot, bid + "bid higher than max bid", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + false, + }, + { + "collateral [reverse]: normal", + auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token1", 15), nil}, + "", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 50), + true, + }, + { + "collateral [reverse]: second bidder", + auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token1", 15), secondBuyer}, + "", + someTime.Add(types.DefaultBidDuration), + secondBuyer, + c("token2", 50), + true, + }, + { + "collateral [reverse]: invalid lot denom", + auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("badtoken", 15), nil}, + "lot denom doesn't match auction", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 50), + false, + }, + { + "collateral [reverse]: invalid lot size (equal)", + auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token1", 20), nil}, + "auction in reverse phase, new bid not less than previous amount", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 50), + false, + }, + { + "collateral [reverse]: invalid lot size (greater)", + auctionArgs{CollateralPhase2, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid + bidArgs{buyer, c("token1", 21), nil}, + "auction in reverse phase, new bid not less than previous amount", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 50), + false, + }, + { + "basic: closed auction", + auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, + bidArgs{buyer, c("token2", 10), nil}, + "auction has closed", + someTime.Add(types.DefaultBidDuration), + buyer, + c("token2", 10), + false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Start Auction + var id uint64 + var err error + switch tc.auctionArgs.auctionType { + case Surplus: + id, _ = keeper.StartSurplusAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid.Denom) + case Debt: + id, _ = keeper.StartDebtAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.bid, tc.auctionArgs.lot, tc.auctionArgs.debt) + case CollateralPhase1, CollateralPhase2: + id, _ = keeper.StartCollateralAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid, tc.auctionArgs.addresses, tc.auctionArgs.weights, tc.auctionArgs.debt) // seller, lot, maxBid, otherPerson + // Move CollateralAuction to debt phase by placing max bid + if tc.auctionArgs.auctionType == CollateralPhase2 { + err = keeper.PlaceBid(ctx, id, tc.bidArgs.bidder, tc.auctionArgs.bid) + require.NoError(t, err) + } + default: + t.Fail() + } + + // Close the auction early to test late bidding (if applicable) + if strings.Contains(tc.name, "closed") { + ctx = ctx.WithBlockTime(types.DistantFuture.Add(1)) + } + + // Place bid on auction + err = keeper.PlaceBid(ctx, id, tc.bidArgs.bidder, tc.bidArgs.amount) + + // Place second bid from new bidder + if tc.bidArgs.secondBidder != nil { + // Set bid increase/decrease based on auction type, phase + var secondBid sdk.Coin + switch tc.auctionArgs.auctionType { + case Surplus, CollateralPhase1: + secondBid = tc.bidArgs.amount.Add(c(tc.bidArgs.amount.Denom, 1)) + case Debt, CollateralPhase2: + secondBid = tc.bidArgs.amount.Sub(c(tc.bidArgs.amount.Denom, 1)) + default: + t.Fail() + } + // Place the second bid + err2 := keeper.PlaceBid(ctx, id, tc.bidArgs.secondBidder, secondBid) + require.NoError(t, err2) + } + + // Check success/failure + if tc.expectpass { + require.Nil(t, err) + // Get auction from store + auction, found := keeper.GetAuction(ctx, id) + require.True(t, found) + // Check auction values + require.Equal(t, modName, auction.GetInitiator()) + require.Equal(t, tc.expectedBidder, auction.GetBidder()) + require.Equal(t, tc.expectedBid, auction.GetBid()) + require.Equal(t, tc.expectedEndTime, auction.GetEndTime()) + } else { + // Check expected error message + require.Contains(t, err.Error(), tc.expectedError) + } + }) + } +} diff --git a/x/auction/keeper/querier.go b/x/auction/keeper/querier.go index 3b92476e..50afa363 100644 --- a/x/auction/keeper/querier.go +++ b/x/auction/keeper/querier.go @@ -1,7 +1,6 @@ package keeper import ( - "fmt" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/x/auction/types" @@ -21,10 +20,10 @@ func NewQuerier(keeper Keeper) sdk.Querier { } func queryAuctions(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { - var auctionsList types.QueryResAuctions + var auctionsList types.Auctions keeper.IterateAuctions(ctx, func(a types.Auction) bool { - auctionsList = append(auctionsList, fmt.Sprintf("%+v", a)) // TODO formatting + auctionsList = append(auctionsList, a) return false }) diff --git a/x/auction/keeper/querier_test.go b/x/auction/keeper/querier_test.go new file mode 100644 index 00000000..c1ec1f7f --- /dev/null +++ b/x/auction/keeper/querier_test.go @@ -0,0 +1,104 @@ +package keeper_test + +import ( + "math/rand" + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/cosmos/cosmos-sdk/x/supply" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/auction/keeper" + "github.com/kava-labs/kava/x/auction/types" + "github.com/kava-labs/kava/x/cdp" + "github.com/stretchr/testify/suite" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +const ( + custom = "custom" + TestAuctionCount = 10 +) + +type QuerierTestSuite struct { + suite.Suite + + keeper keeper.Keeper + app app.TestApp + auctions types.Auctions + ctx sdk.Context + querier sdk.Querier +} + +func (suite *QuerierTestSuite) SetupTest() { + tApp := app.NewTestApp() + ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) + + _, addrs := app.GeneratePrivKeyAddressPairs(1) + buyer := addrs[0] + modName := cdp.LiquidatorMacc + + // Set up seller account + sellerAcc := supply.NewEmptyModuleAccount(modName, supply.Minter, supply.Burner) + sellerAcc.SetCoins(cs(c("token1", 1000), c("token2", 1000), c("debt", 1000))) + + // Initialize genesis accounts + tApp.InitializeFromGenesisStates( + NewAuthGenStateFromAccs(authexported.GenesisAccounts{ + auth.NewBaseAccount(buyer, cs(c("token1", 1000), c("token2", 1000)), nil, 0, 0), + sellerAcc, + }), + ) + + suite.ctx = ctx + suite.app = tApp + suite.keeper = tApp.GetAuctionKeeper() + + // Populate with auctions + for j := 0; j < TestAuctionCount; j++ { + lotAmount := simulation.RandIntBetween(rand.New(rand.NewSource(int64(j))), 10, 100) + id, err := suite.keeper.StartSurplusAuction(suite.ctx, modName, c("token1", int64(lotAmount)), "token2") + suite.Nil(err) + + auc, found := suite.keeper.GetAuction(suite.ctx, id) + suite.True(found) + suite.auctions = append(suite.auctions, auc) + } + + suite.querier = keeper.NewQuerier(suite.keeper) +} + +func (suite *QuerierTestSuite) TestQueryAuctions() { + ctx := suite.ctx.WithIsCheckTx(false) + // Set up request query + query := abci.RequestQuery{ + Path: strings.Join([]string{custom, types.QuerierRoute, types.QueryGetAuction}, "/"), + Data: types.ModuleCdc.MustMarshalJSON(types.NewQueryAllAuctionParams(1, TestAuctionCount)), + } + + // Execute query and check the []byte result + bz, err := suite.querier(ctx, []string{types.QueryGetAuction}, query) + suite.NoError(err) + suite.NotNil(bz) + + // Unmarshal the bytes into type Auctions + var auctions types.Auctions + suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &auctions)) + + // Check that each Auction has correct values + for i := 0; i < TestAuctionCount; i++ { + suite.Equal(suite.auctions[i].GetID(), auctions[i].GetID()) + suite.Equal(suite.auctions[i].GetInitiator(), auctions[i].GetInitiator()) + suite.Equal(suite.auctions[i].GetLot(), auctions[i].GetLot()) + suite.Equal(suite.auctions[i].GetBid(), auctions[i].GetBid()) + suite.Equal(suite.auctions[i].GetEndTime(), auctions[i].GetEndTime()) + } +} + +func TestQuerierTestSuite(t *testing.T) { + suite.Run(t, new(QuerierTestSuite)) +} diff --git a/x/auction/types/auctions.go b/x/auction/types/auctions.go index c4b469f8..203d9fec 100644 --- a/x/auction/types/auctions.go +++ b/x/auction/types/auctions.go @@ -17,9 +17,16 @@ var DistantFuture time.Time = time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC) type Auction interface { GetID() uint64 WithID(uint64) Auction + GetInitiator() string + GetLot() sdk.Coin + GetBidder() sdk.AccAddress + GetBid() sdk.Coin GetEndTime() time.Time } +// Auctions is a slice of auctions. +type Auctions []Auction + // BaseAuction is a common type shared by all Auctions. type BaseAuction struct { ID uint64 @@ -35,6 +42,18 @@ type BaseAuction struct { // GetID is a getter for auction ID. func (a BaseAuction) GetID() uint64 { return a.ID } +// GetInitiator is a getter for auction Initiator. +func (a BaseAuction) GetInitiator() string { return a.Initiator } + +// GetLot is a getter for auction Lot. +func (a BaseAuction) GetLot() sdk.Coin { return a.Lot } + +// GetBidder is a getter for auction Bidder. +func (a BaseAuction) GetBidder() sdk.AccAddress { return a.Bidder } + +// GetBid is a getter for auction Bid. +func (a BaseAuction) GetBid() sdk.Coin { return a.Bid } + // GetEndTime is a getter for auction end time. func (a BaseAuction) GetEndTime() time.Time { return a.EndTime } diff --git a/x/auction/types/auctions_test.go b/x/auction/types/auctions_test.go new file mode 100644 index 00000000..f46968d6 --- /dev/null +++ b/x/auction/types/auctions_test.go @@ -0,0 +1,195 @@ +package types + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +const ( + TestInitiatorModuleName = "liquidator" + TestLotDenom = "usdx" + TestLotAmount = 100 + TestBidDenom = "kava" + TestBidAmount = 20 + TestDebtDenom = "debt" + TestDebtAmount1 = 20 + TestDebtAmount2 = 15 + TestExtraEndTime = 10000 + TestAuctionID = 9999123 + TestAccAddress1 = "kava1qcfdf69js922qrdr4yaww3ax7gjml6pd39p8lj" + TestAccAddress2 = "kava1pdfav2cjhry9k79nu6r8kgknnjtq6a7rcr0qlr" +) + +func TestNewWeightedAddresses(t *testing.T) { + + tests := []struct { + name string + addresses []sdk.AccAddress + weights []sdk.Int + expectpass bool + }{ + { + "normal", + []sdk.AccAddress{ + sdk.AccAddress([]byte(TestAccAddress1)), + sdk.AccAddress([]byte(TestAccAddress2)), + }, + []sdk.Int{ + sdk.NewInt(6), + sdk.NewInt(8), + }, + true, + }, + { + "mismatched", + []sdk.AccAddress{ + sdk.AccAddress([]byte(TestAccAddress1)), + sdk.AccAddress([]byte(TestAccAddress2)), + }, + []sdk.Int{ + sdk.NewInt(6), + }, + false, + }, + { + "negativeWeight", + []sdk.AccAddress{ + sdk.AccAddress([]byte(TestAccAddress1)), + sdk.AccAddress([]byte(TestAccAddress2)), + }, + []sdk.Int{ + sdk.NewInt(6), + sdk.NewInt(-8), + }, + false, + }, + } + + // Run NewWeightedAdresses tests + for _, tc := range tests { + t.Run(tc.name, func(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) + + // 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() + } + } + }) + } +} + +func TestBaseAuctionGetters(t *testing.T) { + endTime := time.Now().Add(TestExtraEndTime) + + // Create a new BaseAuction (via SurplusAuction) + auction := NewSurplusAuction( + TestInitiatorModuleName, + c(TestLotDenom, TestLotAmount), + TestBidDenom, endTime, + ) + + auctionID := auction.GetID() + auctionBid := auction.GetBid() + auctionLot := auction.GetLot() + auctionEndTime := auction.GetEndTime() + auctionString := auction.String() + + require.Equal(t, auction.ID, auctionID) + require.Equal(t, auction.Bid, auctionBid) + require.Equal(t, auction.Lot, auctionLot) + require.Equal(t, auction.EndTime, auctionEndTime) + require.NotNil(t, auctionString) +} + +func TestNewSurplusAuction(t *testing.T) { + endTime := time.Now().Add(TestExtraEndTime) + + // Create a new SurplusAuction + surplusAuction := NewSurplusAuction( + TestInitiatorModuleName, + c(TestLotDenom, TestLotAmount), + TestBidDenom, endTime, + ) + + require.Equal(t, surplusAuction.Initiator, TestInitiatorModuleName) + require.Equal(t, surplusAuction.Lot, c(TestLotDenom, TestLotAmount)) + require.Equal(t, surplusAuction.Bid, c(TestBidDenom, 0)) + require.Equal(t, surplusAuction.EndTime, endTime) + require.Equal(t, surplusAuction.MaxEndTime, endTime) +} + +func TestNewDebtAuction(t *testing.T) { + endTime := time.Now().Add(TestExtraEndTime) + + // Create a new DebtAuction + debtAuction := NewDebtAuction( + TestInitiatorModuleName, + c(TestBidDenom, TestBidAmount), + c(TestLotDenom, TestLotAmount), + endTime, + c(TestDebtDenom, TestDebtAmount1), + ) + + require.Equal(t, debtAuction.Initiator, TestInitiatorModuleName) + require.Equal(t, debtAuction.Lot, c(TestLotDenom, TestLotAmount)) + require.Equal(t, debtAuction.Bid, c(TestBidDenom, TestBidAmount)) + require.Equal(t, debtAuction.EndTime, endTime) + require.Equal(t, debtAuction.MaxEndTime, endTime) + require.Equal(t, debtAuction.CorrespondingDebt, c(TestDebtDenom, TestDebtAmount1)) +} + +func TestNewCollateralAuction(t *testing.T) { + // Set up WeightedAddresses + addresses := []sdk.AccAddress{ + sdk.AccAddress([]byte(TestAccAddress1)), + sdk.AccAddress([]byte(TestAccAddress2)), + } + + weights := []sdk.Int{ + sdk.NewInt(6), + sdk.NewInt(8), + } + + weightedAddresses, _ := NewWeightedAddresses(addresses, weights) + + endTime := time.Now().Add(TestExtraEndTime) + + collateralAuction := NewCollateralAuction( + TestInitiatorModuleName, + c(TestLotDenom, TestLotAmount), + endTime, + c(TestBidDenom, TestBidAmount), + weightedAddresses, + c(TestDebtDenom, TestDebtAmount2), + ) + + require.Equal(t, collateralAuction.BaseAuction.Initiator, TestInitiatorModuleName) + require.Equal(t, collateralAuction.BaseAuction.Lot, c(TestLotDenom, TestLotAmount)) + require.Equal(t, collateralAuction.BaseAuction.Bid, c(TestBidDenom, 0)) + require.Equal(t, collateralAuction.BaseAuction.EndTime, endTime) + require.Equal(t, collateralAuction.BaseAuction.MaxEndTime, endTime) + require.Equal(t, collateralAuction.MaxBid, c(TestBidDenom, TestBidAmount)) + require.Equal(t, collateralAuction.LotReturns, weightedAddresses) + require.Equal(t, collateralAuction.CorrespondingDebt, c(TestDebtDenom, TestDebtAmount2)) +} diff --git a/x/auction/types/keys.go b/x/auction/types/keys.go index aca635a9..03c8cc29 100644 --- a/x/auction/types/keys.go +++ b/x/auction/types/keys.go @@ -19,6 +19,8 @@ const ( // DefaultParamspace default name for parameter store DefaultParamspace = ModuleName + + QuerierRoute = ModuleName ) var ( diff --git a/x/auction/types/querier.go b/x/auction/types/querier.go index 571c3908..3b373317 100644 --- a/x/auction/types/querier.go +++ b/x/auction/types/querier.go @@ -16,3 +16,17 @@ type QueryResAuctions []string func (n QueryResAuctions) String() string { return strings.Join(n[:], "\n") } + +// QueryAllAuctionParams is the params for an auctions query +type QueryAllAuctionParams struct { + Page int `json"page:" yaml:"page"` + Limit int `json"limit:" yaml:"limit"` +} + +// NewQueryAllAuctionParams creates a new QueryAllAuctionParams +func NewQueryAllAuctionParams(page int, limit int) QueryAllAuctionParams { + return QueryAllAuctionParams{ + Page: page, + Limit: limit, + } +}