package keeper_test import ( "errors" "strings" "testing" "time" "github.com/stretchr/testify/require" 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" 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 Collateral AuctionType = 3 ) 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) 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 } tests := []struct { name string auctionArgs auctionArgs setupBids []bidArgs bidArgs bidArgs expectedError error expectedEndTime time.Time expectedBidder sdk.AccAddress expectedBid sdk.Coin expectPass bool expectPanic bool }{ { "basic: auction doesn't exist", auctionArgs{Surplus, "", c("token1", 1), c("token2", 1), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, nil, bidArgs{buyer, c("token2", 10)}, types.ErrAuctionNotFound, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 10), false, true, }, { "basic: closed auction", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, nil, bidArgs{buyer, c("token2", 10)}, types.ErrAuctionHasExpired, types.DistantFuture, nil, c("token2", 0), false, false, }, { "surplus: normal", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, nil, bidArgs{buyer, c("token2", 10)}, nil, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 10), true, false, }, { "surplus: second bidder", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, []bidArgs{{buyer, c("token2", 10)}}, bidArgs{secondBuyer, c("token2", 11)}, nil, someTime.Add(types.DefaultBidDuration), secondBuyer, c("token2", 11), true, false, }, { "surplus: invalid bid denom", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 10), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, nil, bidArgs{buyer, c("badtoken", 10)}, types.ErrInvalidBidDenom, types.DistantFuture, nil, // surplus auctions are created with initial bidder as a nil address c("token2", 0), false, false, }, { "surplus: invalid bid (less than)", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 0), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, []bidArgs{{buyer, c("token2", 100)}}, bidArgs{buyer, c("token2", 99)}, types.ErrBidTooSmall, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 100), false, false, }, { "surplus: invalid bid (equal)", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 0), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, nil, bidArgs{buyer, c("token2", 0)}, // min bid is technically 0 at default 5%, but it's capped at 1 types.ErrBidTooSmall, types.DistantFuture, nil, // surplus auctions are created with initial bidder as a nil address c("token2", 0), false, false, }, { "surplus: invalid bid (less than min increment)", auctionArgs{Surplus, modName, c("token1", 100), c("token2", 0), sdk.Coin{}, []sdk.AccAddress{}, []sdk.Int{}}, []bidArgs{{buyer, c("token2", 100)}}, bidArgs{buyer, c("token2", 104)}, // min bid is 105 at default 5% types.ErrBidTooSmall, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 100), false, false, }, { "debt: normal", auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot nil, bidArgs{buyer, c("token1", 10)}, nil, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 100), true, false, }, { "debt: second bidder", auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot []bidArgs{{buyer, c("token1", 10)}}, bidArgs{secondBuyer, c("token1", 9)}, nil, someTime.Add(types.DefaultBidDuration), secondBuyer, c("token2", 100), true, false, }, { "debt: invalid lot denom", auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}}, // initial bid, lot nil, bidArgs{buyer, c("badtoken", 10)}, types.ErrInvalidLotDenom, types.DistantFuture, supply.NewModuleAddress(modName), c("token2", 100), false, false, }, { "debt: invalid lot size (larger)", auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}}, nil, bidArgs{buyer, c("token1", 21)}, types.ErrLotTooLarge, types.DistantFuture, supply.NewModuleAddress(modName), c("token2", 100), false, false, }, { "debt: invalid lot size (equal)", auctionArgs{Debt, modName, c("token1", 20), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}}, nil, bidArgs{buyer, c("token1", 20)}, types.ErrLotTooLarge, types.DistantFuture, supply.NewModuleAddress(modName), c("token2", 100), false, false, }, { "debt: invalid lot size (larger than min increment)", auctionArgs{Debt, modName, c("token1", 60), c("token2", 100), c("debt", 100), []sdk.AccAddress{}, []sdk.Int{}}, nil, bidArgs{buyer, c("token1", 58)}, // max lot at default 5% is 57 types.ErrLotTooLarge, types.DistantFuture, supply.NewModuleAddress(modName), c("token2", 100), false, false, }, { "collateral [forward]: normal", auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid nil, bidArgs{buyer, c("token2", 10)}, nil, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 10), true, false, }, { "collateral [forward]: second bidder", auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid []bidArgs{{buyer, c("token2", 10)}}, bidArgs{secondBuyer, c("token2", 11)}, nil, someTime.Add(types.DefaultBidDuration), secondBuyer, c("token2", 11), true, false, }, { "collateral [forward]: invalid bid denom", auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid nil, bidArgs{buyer, c("badtoken", 10)}, types.ErrInvalidBidDenom, types.DistantFuture, nil, c("token2", 0), false, false, }, { "collateral [forward]: invalid bid size (smaller)", auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid []bidArgs{{buyer, c("token2", 10)}}, bidArgs{buyer, c("token2", 9)}, types.ErrBidTooSmall, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 10), false, false, }, { "collateral [forward]: invalid bid size (equal)", auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid nil, bidArgs{buyer, c("token2", 0)}, types.ErrBidTooSmall, types.DistantFuture, nil, c("token2", 0), false, false, }, { "collateral [forward]: invalid bid size (less than min increment)", auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid []bidArgs{{buyer, c("token2", 50)}}, bidArgs{buyer, c("token2", 51)}, types.ErrBidTooSmall, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 50), false, false, }, { "collateral [forward]: less than min increment but equal to maxBid", auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid []bidArgs{{buyer, c("token2", 99)}}, bidArgs{buyer, c("token2", 100)}, // min bid at default 5% is 104 nil, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 100), true, false, }, { "collateral [forward]: invalid bid size (greater than max)", auctionArgs{Collateral, modName, c("token1", 20), c("token2", 100), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid nil, bidArgs{buyer, c("token2", 101)}, types.ErrBidTooLarge, types.DistantFuture, nil, c("token2", 0), false, false, }, { "collateral [reverse]: normal", auctionArgs{Collateral, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid []bidArgs{{buyer, c("token2", 50)}}, // put auction into reverse phase bidArgs{buyer, c("token1", 15)}, nil, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 50), true, false, }, { "collateral [reverse]: second bidder", auctionArgs{Collateral, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid []bidArgs{{buyer, c("token2", 50)}, {buyer, c("token1", 15)}}, // put auction into reverse phase, and add a reverse phase bid bidArgs{secondBuyer, c("token1", 14)}, nil, someTime.Add(types.DefaultBidDuration), secondBuyer, c("token2", 50), true, false, }, { "collateral [reverse]: invalid lot denom", auctionArgs{Collateral, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid []bidArgs{{buyer, c("token2", 50)}}, // put auction into reverse phase bidArgs{buyer, c("badtoken", 15)}, types.ErrInvalidLotDenom, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 50), false, false, }, { "collateral [reverse]: invalid lot size (greater)", auctionArgs{Collateral, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid []bidArgs{{buyer, c("token2", 50)}}, // put auction into reverse phase bidArgs{buyer, c("token1", 21)}, types.ErrLotTooLarge, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 50), false, false, }, { "collateral [reverse]: invalid lot size (equal)", auctionArgs{Collateral, modName, c("token1", 20), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid []bidArgs{{buyer, c("token2", 50)}}, // put auction into reverse phase bidArgs{buyer, c("token1", 20)}, types.ErrLotTooLarge, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 50), false, false, }, { "collateral [reverse]: invalid lot size (larger than min increment)", auctionArgs{Collateral, modName, c("token1", 60), c("token2", 50), c("debt", 50), collateralAddrs, collateralWeights}, // lot, max bid []bidArgs{{buyer, c("token2", 50)}}, // put auction into reverse phase bidArgs{buyer, c("token1", 58)}, // max lot at default 5% is 57 types.ErrLotTooLarge, someTime.Add(types.DefaultBidDuration), buyer, c("token2", 50), false, false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Setup test 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() bank := tApp.GetBankKeeper() // Start Auction var id uint64 var err error switch tc.auctionArgs.auctionType { case Surplus: if tc.expectPanic { require.Panics(t, func() { id, err = keeper.StartSurplusAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid.Denom) }) } else { id, err = keeper.StartSurplusAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.lot, tc.auctionArgs.bid.Denom) } case Debt: id, err = keeper.StartDebtAuction(ctx, tc.auctionArgs.seller, tc.auctionArgs.bid, tc.auctionArgs.lot, tc.auctionArgs.debt) case Collateral: id, err = 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 default: t.Fail() } require.NoError(t, err) // Place setup bids for _, b := range tc.setupBids { require.NoError(t, keeper.PlaceBid(ctx, id, b.bidder, b.amount)) } // Close the auction early to test late bidding (if applicable) if strings.Contains(tc.name, "closed") { ctx = ctx.WithBlockTime(types.DistantFuture.Add(1)) } // Store some state for use in checks oldAuction, found := keeper.GetAuction(ctx, id) var oldBidder sdk.AccAddress if found { oldBidder = oldAuction.GetBidder() } oldBidderOldCoins := bank.GetCoins(ctx, oldBidder) newBidderOldCoins := bank.GetCoins(ctx, tc.bidArgs.bidder) // Place bid on auction err = keeper.PlaceBid(ctx, id, tc.bidArgs.bidder, tc.bidArgs.amount) // Check success/failure if tc.expectPass { require.Nil(t, err) // Check auction was found newAuction, found := keeper.GetAuction(ctx, id) require.True(t, found) // Check auction values require.Equal(t, modName, newAuction.GetInitiator()) require.Equal(t, tc.expectedBidder, newAuction.GetBidder()) require.Equal(t, tc.expectedBid, newAuction.GetBid()) require.Equal(t, tc.expectedEndTime, newAuction.GetEndTime()) // Check coins have moved between bidder and previous bidder bidAmt := tc.bidArgs.amount switch tc.auctionArgs.auctionType { case Debt: bidAmt = oldAuction.GetBid() case Collateral: collatAuction, ok := oldAuction.(types.CollateralAuction) require.True(t, ok, tc.name) if collatAuction.IsReversePhase() { bidAmt = oldAuction.GetBid() } } if oldBidder.Equals(tc.bidArgs.bidder) { // same bidder require.Equal(t, newBidderOldCoins.Sub(cs(bidAmt.Sub(oldAuction.GetBid()))), bank.GetCoins(ctx, tc.bidArgs.bidder)) } else { require.Equal(t, newBidderOldCoins.Sub(cs(bidAmt)), bank.GetCoins(ctx, tc.bidArgs.bidder)) // wrapping in cs() to avoid comparing nil and empty coins if oldBidder.Equals(supply.NewModuleAddress(oldAuction.GetInitiator())) { // handle checking debt coins for case debt auction has had no bids placed yet TODO make this less confusing require.Equal(t, oldBidderOldCoins.Add(oldAuction.GetBid()).Add(c("debt", oldAuction.GetBid().Amount.Int64())), bank.GetCoins(ctx, oldBidder)) } else { require.Equal(t, cs(oldBidderOldCoins.Add(oldAuction.GetBid())...), bank.GetCoins(ctx, oldBidder)) } } } else { // Check expected error code type require.NotNil(t, err, "PlaceBid did not return an error") // catch nil values before they cause a panic below require.True(t, errors.Is(err, tc.expectedError)) // Check auction values newAuction, found := keeper.GetAuction(ctx, id) if found { require.Equal(t, modName, newAuction.GetInitiator()) require.Equal(t, tc.expectedBidder, newAuction.GetBidder()) require.Equal(t, tc.expectedBid, newAuction.GetBid()) require.Equal(t, tc.expectedEndTime, newAuction.GetEndTime()) } // Check coins have not moved require.Equal(t, newBidderOldCoins, bank.GetCoins(ctx, tc.bidArgs.bidder)) require.Equal(t, oldBidderOldCoins, bank.GetCoins(ctx, oldBidder)) } }) } }