From 667fd2f9eb41aaf64a3d0a7ad7bc58035ae9ac82 Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Wed, 14 Feb 2024 12:13:14 +0530 Subject: [PATCH] Implement end blocker to process auctions --- x/auction/keeper/keeper.go | 252 +++++++++++++++++++++++++++++++++++++ x/auction/module/abci.go | 7 +- 2 files changed, 256 insertions(+), 3 deletions(-) diff --git a/x/auction/keeper/keeper.go b/x/auction/keeper/keeper.go index 4f071bff..578fc361 100644 --- a/x/auction/keeper/keeper.go +++ b/x/auction/keeper/keeper.go @@ -4,6 +4,8 @@ import ( "encoding/hex" "encoding/json" "errors" + "fmt" + "time" "cosmossdk.io/collections" "cosmossdk.io/collections/indexes" @@ -20,6 +22,9 @@ import ( auctiontypes "git.vdb.to/cerc-io/laconic2d/x/auction" ) +// CompletedAuctionDeleteTimeout => Completed auctions are deleted after this timeout (after reveals end time). +const CompletedAuctionDeleteTimeout = time.Hour * 24 + type AuctionsIndexes struct { Owner *indexes.Multi[string, string, auctiontypes.Auction] } @@ -120,6 +125,23 @@ func (k Keeper) SaveAuction(ctx sdk.Context, auction *auctiontypes.Auction) erro // return nil } +// DeleteAuction - deletes the auction. +func (k Keeper) DeleteAuction(ctx sdk.Context, auction auctiontypes.Auction) error { + // Delete all bids first. + bids, err := k.GetBids(ctx, auction.Id) + if err != nil { + return err + } + + for _, bid := range bids { + if err := k.DeleteBid(ctx, *bid); err != nil { + return err + } + } + + return k.Auctions.Remove(ctx, auction.Id) +} + func (k Keeper) HasAuction(ctx sdk.Context, id string) (bool, error) { has, err := k.Auctions.Has(ctx, id) if err != nil { @@ -191,6 +213,29 @@ func (k Keeper) ListAuctions(ctx sdk.Context) ([]auctiontypes.Auction, error) { return iter.Values() } +// MatchAuctions - get all matching auctions. +func (k Keeper) MatchAuctions(ctx sdk.Context, matchFn func(*auctiontypes.Auction) (bool, error)) ([]*auctiontypes.Auction, error) { + var auctions []*auctiontypes.Auction + + err := k.Auctions.Walk(ctx, nil, func(key string, value auctiontypes.Auction) (stop bool, err error) { + auctionMatched, err := matchFn(&value) + if err != nil { + return false, err + } + + if auctionMatched { + auctions = append(auctions, &value) + } + + return true, nil + }) + if err != nil { + return nil, err + } + + return auctions, nil +} + // GetAuction - gets a record from the store. func (k Keeper) GetAuctionById(ctx sdk.Context, id string) (auctiontypes.Auction, error) { auction, err := k.Auctions.Get(ctx, id) @@ -485,3 +530,210 @@ func (k Keeper) GetAuctionModuleBalances(ctx sdk.Context) sdk.Coins { return balances } + +func (k Keeper) EndBlockerProcessAuctions(ctx sdk.Context) error { + var err error + + // Transition auction state (commit, reveal, expired, completed). + if err = k.processAuctionPhases(ctx); err != nil { + return err + } + + // Delete stale auctions. + return k.deleteCompletedAuctions(ctx) +} + +func (k Keeper) processAuctionPhases(ctx sdk.Context) error { + auctions, err := k.MatchAuctions(ctx, func(_ *auctiontypes.Auction) (bool, error) { + return true, nil + }) + if err != nil { + return err + } + + for _, auction := range auctions { + // Commit -> Reveal state. + if auction.Status == auctiontypes.AuctionStatusCommitPhase && ctx.BlockTime().After(auction.CommitsEndTime) { + auction.Status = auctiontypes.AuctionStatusRevealPhase + if err = k.SaveAuction(ctx, auction); err != nil { + return err + } + + ctx.Logger().Info(fmt.Sprintf("Moved auction %s to reveal phase.", auction.Id)) + } + + // Reveal -> Expired state. + if auction.Status == auctiontypes.AuctionStatusRevealPhase && ctx.BlockTime().After(auction.RevealsEndTime) { + auction.Status = auctiontypes.AuctionStatusExpired + if err = k.SaveAuction(ctx, auction); err != nil { + return err + } + + ctx.Logger().Info(fmt.Sprintf("Moved auction %s to expired state.", auction.Id)) + } + + // If auction has expired, pick a winner from revealed bids. + if auction.Status == auctiontypes.AuctionStatusExpired { + if err = k.pickAuctionWinner(ctx, auction); err != nil { + return err + } + } + } + + return nil +} + +// Delete completed stale auctions. +func (k Keeper) deleteCompletedAuctions(ctx sdk.Context) error { + auctions, err := k.MatchAuctions(ctx, func(auction *auctiontypes.Auction) (bool, error) { + deleteTime := auction.RevealsEndTime.Add(CompletedAuctionDeleteTimeout) + return auction.Status == auctiontypes.AuctionStatusCompleted && ctx.BlockTime().After(deleteTime), nil + }) + if err != nil { + return err + } + + for _, auction := range auctions { + ctx.Logger().Info(fmt.Sprintf("Deleting completed auction %s after timeout.", auction.Id)) + if err := k.DeleteAuction(ctx, *auction); err != nil { + return err + } + } + + return nil +} + +func (k Keeper) pickAuctionWinner(ctx sdk.Context, auction *auctiontypes.Auction) error { + ctx.Logger().Info(fmt.Sprintf("Picking auction %s winner.", auction.Id)) + + var highestBid *auctiontypes.Bid + var secondHighestBid *auctiontypes.Bid + + bids, err := k.GetBids(ctx, auction.Id) + if err != nil { + return err + } + + for _, bid := range bids { + ctx.Logger().Info(fmt.Sprintf("Processing bid %s %s", bid.BidderAddress, bid.BidAmount.String())) + + // Only consider revealed bids. + if bid.Status != auctiontypes.BidStatusRevealed { + ctx.Logger().Info(fmt.Sprintf("Ignoring unrevealed bid %s %s", bid.BidderAddress, bid.BidAmount.String())) + continue + } + + // Init highest bid. + if highestBid == nil { + highestBid = bid + ctx.Logger().Info(fmt.Sprintf("Initializing 1st bid %s %s", bid.BidderAddress, bid.BidAmount.String())) + continue + } + + //nolint: all + if highestBid.BidAmount.IsLT(bid.BidAmount) { + ctx.Logger().Info(fmt.Sprintf("New highest bid %s %s", bid.BidderAddress, bid.BidAmount.String())) + + secondHighestBid = highestBid + highestBid = bid + + ctx.Logger().Info(fmt.Sprintf("Updated 1st bid %s %s", highestBid.BidderAddress, highestBid.BidAmount.String())) + ctx.Logger().Info(fmt.Sprintf("Updated 2nd bid %s %s", secondHighestBid.BidderAddress, secondHighestBid.BidAmount.String())) + + } else if secondHighestBid == nil || secondHighestBid.BidAmount.IsLT(bid.BidAmount) { + ctx.Logger().Info(fmt.Sprintf("New 2nd highest bid %s %s", bid.BidderAddress, bid.BidAmount.String())) + + secondHighestBid = bid + ctx.Logger().Info(fmt.Sprintf("Updated 2nd bid %s %s", secondHighestBid.BidderAddress, secondHighestBid.BidAmount.String())) + } else { + ctx.Logger().Info(fmt.Sprintf("Ignoring bid as it doesn't affect 1st/2nd price %s %s", bid.BidderAddress, bid.BidAmount.String())) + } + } + + // Highest bid is the winner, but pays second highest bid price. + auction.Status = auctiontypes.AuctionStatusCompleted + + if highestBid != nil { + auction.WinnerAddress = highestBid.BidderAddress + auction.WinningBid = highestBid.BidAmount + + // Winner pays 2nd price, if a 2nd price exists. + auction.WinningPrice = highestBid.BidAmount + if secondHighestBid != nil { + auction.WinningPrice = secondHighestBid.BidAmount + } + ctx.Logger().Info(fmt.Sprintf("Auction %s winner %s.", auction.Id, auction.WinnerAddress)) + ctx.Logger().Info(fmt.Sprintf("Auction %s winner bid %s.", auction.Id, auction.WinningBid.String())) + ctx.Logger().Info(fmt.Sprintf("Auction %s winner price %s.", auction.Id, auction.WinningPrice.String())) + } else { + ctx.Logger().Info(fmt.Sprintf("Auction %s has no valid revealed bids (no winner).", auction.Id)) + } + + if err := k.SaveAuction(ctx, auction); err != nil { + return err + } + + for _, bid := range bids { + bidderAddress, err := sdk.AccAddressFromBech32(bid.BidderAddress) + if err != nil { + ctx.Logger().Error(fmt.Sprintf("Invalid bidderAddress address. %v", err)) + panic("Invalid bidder address.") + } + + if bid.Status == auctiontypes.BidStatusRevealed { + // Send reveal fee back to bidders that've revealed the bid. + sdkErr := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, auctiontypes.ModuleName, bidderAddress, sdk.NewCoins(bid.RevealFee)) + if sdkErr != nil { + ctx.Logger().Error(fmt.Sprintf("Auction error returning reveal fee: %v", sdkErr)) + panic(sdkErr) + } + } + + // Send back locked bid amount to all bidders. + sdkErr := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, auctiontypes.ModuleName, bidderAddress, sdk.NewCoins(bid.BidAmount)) + if sdkErr != nil { + ctx.Logger().Error(fmt.Sprintf("Auction error returning bid amount: %v", sdkErr)) + panic(sdkErr) + } + } + + // Process winner account (if nobody bids, there won't be a winner). + if auction.WinnerAddress != "" { + winnerAddress, err := sdk.AccAddressFromBech32(auction.WinnerAddress) + if err != nil { + ctx.Logger().Error(fmt.Sprintf("Invalid winner address. %v", err)) + panic("Invalid winner address.") + } + + // Take 2nd price from winner. + sdkErr := k.bankKeeper.SendCoinsFromAccountToModule(ctx, winnerAddress, auctiontypes.ModuleName, sdk.NewCoins(auction.WinningPrice)) + if sdkErr != nil { + ctx.Logger().Error(fmt.Sprintf("Auction error taking funds from winner: %v", sdkErr)) + panic(sdkErr) + } + + // Burn anything over the min. bid amount. + amountToBurn := auction.WinningPrice.Sub(auction.MinimumBid) + if amountToBurn.IsNegative() { + ctx.Logger().Error("Auction coins to burn cannot be negative.") + panic("Auction coins to burn cannot be negative.") + } + + // Use auction burn module account instead of actually burning coins to better keep track of supply. + sdkErr = k.bankKeeper.SendCoinsFromModuleToModule(ctx, auctiontypes.ModuleName, auctiontypes.AuctionBurnModuleAccountName, sdk.NewCoins(amountToBurn)) + if sdkErr != nil { + ctx.Logger().Error(fmt.Sprintf("Auction error burning coins: %v", sdkErr)) + panic(sdkErr) + } + } + + // TODO + // Notify other modules (hook). + // ctx.Logger().Info(fmt.Sprintf("Auction %s notifying %d modules.", auction.Id, len(k.usageKeepers))) + // for _, keeper := range k.usageKeepers { + // ctx.Logger().Info(fmt.Sprintf("Auction %s notifying module %s.", auction.Id, keeper.ModuleName())) + // keeper.OnAuctionWinnerSelected(ctx, auction.Id) + // } + + return nil +} diff --git a/x/auction/module/abci.go b/x/auction/module/abci.go index 42d1b245..63701487 100644 --- a/x/auction/module/abci.go +++ b/x/auction/module/abci.go @@ -3,13 +3,14 @@ package module import ( "context" + sdk "github.com/cosmos/cosmos-sdk/types" + "git.vdb.to/cerc-io/laconic2d/x/auction/keeper" ) // EndBlocker is called every block func EndBlocker(ctx context.Context, k keeper.Keeper) error { - // TODO: Implement - // k.EndBlockerProcessAuctions(ctx) + sdkCtx := sdk.UnwrapSDKContext(ctx) - return nil + return k.EndBlockerProcessAuctions(sdkCtx) }