From d2b9920d9496f0408c9ed494c033a8dffe3523ca Mon Sep 17 00:00:00 2001 From: Michael Tsitrin <114929630+mtsitrin@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:50:02 +0200 Subject: [PATCH] feat(incentives): earning events (#1545) --- x/incentives/keeper/distribute.go | 14 ++-- x/incentives/keeper/gauge_asset.go | 110 ++++++++++++++++++--------- x/incentives/keeper/gauge_rollapp.go | 11 ++- 3 files changed, 91 insertions(+), 44 deletions(-) diff --git a/x/incentives/keeper/distribute.go b/x/incentives/keeper/distribute.go index bd4975605..95efcc752 100644 --- a/x/incentives/keeper/distribute.go +++ b/x/incentives/keeper/distribute.go @@ -33,9 +33,11 @@ func (k Keeper) DistributeOnEpochEnd(ctx sdk.Context, gauges []types.Gauge) (sdk // the epoch. If it's called at the end, then the FilledEpochs field for every gauge is increased. Also, it uses // a cache specific for asset gauges that helps reduce the number of x/lockup requests. func (k Keeper) Distribute(ctx sdk.Context, gauges []types.Gauge, cache types.DenomLocksCache, epochEnd bool) (sdk.Coins, error) { - lockHolders := newDistributionInfo() - + // lockHolders is a map of address -> coins + // it used as an aggregator for owners of the locks over all gauges + lockHolders := NewRewardDistributionTracker() totalDistributedCoins := sdk.Coins{} + for _, gauge := range gauges { var ( gaugeDistributedCoins sdk.Coins @@ -43,10 +45,10 @@ func (k Keeper) Distribute(ctx sdk.Context, gauges []types.Gauge, cache types.De ) switch gauge.DistributeTo.(type) { case *types.Gauge_Asset: - filteredLocks := k.GetDistributeToBaseLocks(ctx, gauge, cache) - gaugeDistributedCoins, err = k.distributeToAssetGauge(ctx, gauge, filteredLocks, &lockHolders) + filteredLocks := k.GetDistributeToBaseLocks(ctx, gauge, cache) // get all locks that satisfy the gauge + gaugeDistributedCoins, err = k.calculateAssetGaugeRewards(ctx, gauge, filteredLocks, &lockHolders) case *types.Gauge_Rollapp: - gaugeDistributedCoins, err = k.distributeToRollappGauge(ctx, gauge) + gaugeDistributedCoins, err = k.calculateRollappGaugeRewards(ctx, gauge, &lockHolders) default: return nil, errorsmod.WithType(sdkerrors.ErrInvalidType, fmt.Errorf("gauge %d has an unsupported distribution type", gauge.Id)) } @@ -64,7 +66,7 @@ func (k Keeper) Distribute(ctx sdk.Context, gauges []types.Gauge, cache types.De } // apply the distribution to asset gauges - err := k.sendRewardsToLocks(ctx, &lockHolders) + err := k.distributeTrackedRewards(ctx, &lockHolders) if err != nil { return nil, err } diff --git a/x/incentives/keeper/gauge_asset.go b/x/incentives/keeper/gauge_asset.go index ddd70e1fe..4f9cf6a26 100644 --- a/x/incentives/keeper/gauge_asset.go +++ b/x/incentives/keeper/gauge_asset.go @@ -10,24 +10,27 @@ import ( lockuptypes "github.com/dymensionxyz/dymension/v3/x/lockup/types" ) -// distributionInfo stores all of the information for rewards distributions. -type distributionInfo struct { - nextID int - lockOwnerAddrToID map[string]int - idToBech32Addr []string - idToDecodedAddr []sdk.AccAddress - idToDistrCoins []sdk.Coins - // TODO: add totalDistrCoins to track total coins distributed +// RewardDistributionTracker maintains the state of pending reward distributions, +// tracking both total rewards and per-gauge rewards for each recipient. +// It uses array-based storage for better cache locality during distribution. +type RewardDistributionTracker struct { + nextID int // Next available ID for new recipients + lockOwnerAddrToID map[string]int // Maps lock owner addresses to their array index + idToBech32Addr []string // Recipient bech32 addresses indexed by ID + idToDecodedAddr []sdk.AccAddress // Decoded recipient addresses indexed by ID + idToDistrCoins []sdk.Coins // Total rewards per recipient indexed by ID + idToGaugeRewards []map[uint64]sdk.Coins // Per-gauge rewards for each recipient indexed by ID } -// newDistributionInfo creates a new distributionInfo struct -func newDistributionInfo() distributionInfo { - return distributionInfo{ +// NewRewardDistributionTracker creates a new tracker for managing reward distributions +func NewRewardDistributionTracker() RewardDistributionTracker { + return RewardDistributionTracker{ nextID: 0, lockOwnerAddrToID: make(map[string]int), idToBech32Addr: []string{}, idToDecodedAddr: []sdk.AccAddress{}, idToDistrCoins: []sdk.Coins{}, + idToGaugeRewards: []map[uint64]sdk.Coins{}, } } @@ -47,10 +50,18 @@ func (k Keeper) getLocksToDistributionWithMaxDuration(ctx sdk.Context, distrTo l } // addLockRewards adds the provided rewards to the lockID mapped to the provided owner address. -func (d *distributionInfo) addLockRewards(owner string, rewards sdk.Coins) error { +func (d *RewardDistributionTracker) addLockRewards(owner string, gaugeID uint64, rewards sdk.Coins) error { if id, ok := d.lockOwnerAddrToID[owner]; ok { + // Update total rewards oldDistrCoins := d.idToDistrCoins[id] d.idToDistrCoins[id] = rewards.Add(oldDistrCoins...) + + // Update gauge rewards (idToGaugeRewards[id] already initialized on first creation) + if existing, ok := d.idToGaugeRewards[id][gaugeID]; ok { + d.idToGaugeRewards[id][gaugeID] = existing.Add(rewards...) + } else { + d.idToGaugeRewards[id][gaugeID] = rewards + } } else { id := d.nextID d.nextID++ @@ -62,46 +73,77 @@ func (d *distributionInfo) addLockRewards(owner string, rewards sdk.Coins) error d.idToBech32Addr = append(d.idToBech32Addr, owner) d.idToDecodedAddr = append(d.idToDecodedAddr, decodedOwnerAddr) d.idToDistrCoins = append(d.idToDistrCoins, rewards) + + // Initialize and set gauge rewards + gaugeRewards := make(map[uint64]sdk.Coins) + gaugeRewards[gaugeID] = rewards + d.idToGaugeRewards = append(d.idToGaugeRewards, gaugeRewards) } return nil } -// sendRewardsToLocks utilizes provided distributionInfo to send coins from the module account to various recipients. -func (k Keeper) sendRewardsToLocks(ctx sdk.Context, distrs *distributionInfo) error { - numIDs := len(distrs.idToDecodedAddr) - if len(distrs.idToDistrCoins) != numIDs { - return fmt.Errorf("number of addresses and coins to distribute to must be equal") +// GetEvents returns distribution events for all recipients. +// For each recipient, it creates a single event with attributes for each gauge's rewards. +func (d *RewardDistributionTracker) GetEvents() sdk.Events { + events := make(sdk.Events, 0, len(d.idToBech32Addr)) + + for id := 0; id < len(d.idToBech32Addr); id++ { + attributes := []sdk.Attribute{ + sdk.NewAttribute(types.AttributeReceiver, d.idToBech32Addr[id]), + sdk.NewAttribute(types.AttributeAmount, d.idToDistrCoins[id].String()), + } + + // Add attributes for each gauge's rewards (events doesn't requires deterministic order) + for gaugeID, gaugeRewards := range d.idToGaugeRewards[id] { + attributes = append(attributes, + sdk.NewAttribute( + fmt.Sprintf("%s_%d", types.AttributeGaugeID, gaugeID), + gaugeRewards.String(), + ), + ) + } + + events = append(events, sdk.NewEvent( + types.TypeEvtDistribution, + attributes..., + )) + } + + return events +} + +// distributeTrackedRewards sends the tracked rewards from the module account to recipients +// and emits corresponding events for each gauge's rewards. +func (k Keeper) distributeTrackedRewards(ctx sdk.Context, tracker *RewardDistributionTracker) error { + numIDs := len(tracker.idToDecodedAddr) + if len(tracker.idToDistrCoins) != numIDs || len(tracker.idToGaugeRewards) != numIDs { + return fmt.Errorf("number of addresses, coins, and gauge rewards to distribute must be equal") } ctx.Logger().Debug("Beginning distribution to users", "num_of_user", numIDs) + // First send all rewards for id := 0; id < numIDs; id++ { err := k.bk.SendCoinsFromModuleToAccount( ctx, types.ModuleName, - distrs.idToDecodedAddr[id], - distrs.idToDistrCoins[id]) + tracker.idToDecodedAddr[id], + tracker.idToDistrCoins[id]) if err != nil { return err } } - ctx.Logger().Debug("Finished sending, now creating liquidity add events") - for id := 0; id < numIDs; id++ { - ctx.EventManager().EmitEvents(sdk.Events{ - sdk.NewEvent( - types.TypeEvtDistribution, - sdk.NewAttribute(types.AttributeReceiver, distrs.idToBech32Addr[id]), - sdk.NewAttribute(types.AttributeAmount, distrs.idToDistrCoins[id].String()), - ), - }) - } + + // Emit all events + ctx.EventManager().EmitEvents(tracker.GetEvents()) + ctx.Logger().Debug("Finished Distributing to users") return nil } -// distributeToAssetGauge runs the distribution logic for a gauge, and adds the sends to -// the distrInfo struct. It also updates the gauge for the distribution. -// Locks is expected to be the correct set of lock recipients for this gauge. -func (k Keeper) distributeToAssetGauge(ctx sdk.Context, gauge types.Gauge, locks []lockuptypes.PeriodLock, currResult *distributionInfo) (sdk.Coins, error) { +// calculateAssetGaugeRewards computes the reward distribution for an asset gauge based on lock amounts. +// It calculates rewards for each qualifying lock and tracks them in the distribution tracker. +// Returns the total coins allocated for distribution. +func (k Keeper) calculateAssetGaugeRewards(ctx sdk.Context, gauge types.Gauge, locks []lockuptypes.PeriodLock, tracker *RewardDistributionTracker) (sdk.Coins, error) { assetDist := gauge.GetAsset() if assetDist == nil { return sdk.Coins{}, fmt.Errorf("gauge %d is not an asset gauge", gauge.Id) @@ -152,7 +194,7 @@ func (k Keeper) distributeToAssetGauge(ctx sdk.Context, gauge types.Gauge, locks continue } // update the amount for that address - err := currResult.addLockRewards(lock.Owner, distrCoins) + err := tracker.addLockRewards(lock.Owner, gauge.Id, distrCoins) if err != nil { return sdk.Coins{}, err } diff --git a/x/incentives/keeper/gauge_rollapp.go b/x/incentives/keeper/gauge_rollapp.go index 3945eae2e..aa9229c1e 100644 --- a/x/incentives/keeper/gauge_rollapp.go +++ b/x/incentives/keeper/gauge_rollapp.go @@ -33,22 +33,25 @@ func (k Keeper) CreateRollappGauge(ctx sdk.Context, rollappId string) (uint64, e return gauge.Id, nil } -func (k Keeper) distributeToRollappGauge(ctx sdk.Context, gauge types.Gauge) (totalDistrCoins sdk.Coins, err error) { +// calculateRollappGaugeRewards computes the reward distribution for a rollapp gauge. +// Returns the total coins allocated for distribution. +func (k Keeper) calculateRollappGaugeRewards(ctx sdk.Context, gauge types.Gauge, tracker *RewardDistributionTracker) (sdk.Coins, error) { // Get the rollapp owner rollapp, found := k.rk.GetRollapp(ctx, gauge.GetRollapp().RollappId) if !found { return sdk.Coins{}, fmt.Errorf("gauge %d: rollapp %s not found", gauge.Id, gauge.GetRollapp().RollappId) } // Ignore the error since the owner must always be valid in x/rollapp - addr := sdk.MustAccAddressFromBech32(rollapp.Owner) + owner := rollapp.Owner - totalDistrCoins = gauge.Coins.Sub(gauge.DistributedCoins...) + totalDistrCoins := gauge.Coins.Sub(gauge.DistributedCoins...) // distribute all remaining coins if totalDistrCoins.Empty() { ctx.Logger().Debug(fmt.Sprintf("gauge %d is empty, skipping", gauge.Id)) return sdk.Coins{}, nil } - err = k.bk.SendCoinsFromModuleToAccount(ctx, types.ModuleName, addr, totalDistrCoins) + // Add rewards to the tracker + err := tracker.addLockRewards(owner, gauge.Id, totalDistrCoins) if err != nil { return sdk.Coins{}, err }