code.vegaprotocol.io/vega@v0.79.0/core/vesting/engine.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package vesting
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"sort"
    22  	"time"
    23  
    24  	"code.vegaprotocol.io/vega/core/assets"
    25  	"code.vegaprotocol.io/vega/core/events"
    26  	"code.vegaprotocol.io/vega/core/types"
    27  	vgcontext "code.vegaprotocol.io/vega/libs/context"
    28  	"code.vegaprotocol.io/vega/libs/crypto"
    29  	"code.vegaprotocol.io/vega/libs/num"
    30  	"code.vegaprotocol.io/vega/logging"
    31  	proto "code.vegaprotocol.io/vega/protos/vega"
    32  	eventspb "code.vegaprotocol.io/vega/protos/vega/events/v1"
    33  
    34  	"golang.org/x/exp/maps"
    35  	"golang.org/x/exp/slices"
    36  )
    37  
    38  //go:generate go run github.com/golang/mock/mockgen -destination mocks/mocks.go -package mocks code.vegaprotocol.io/vega/core/vesting ActivityStreakVestingMultiplier,Assets,Parties,StakeAccounting,Time
    39  
    40  type Collateral interface {
    41  	TransferVestedRewards(ctx context.Context, transfers []*types.Transfer) ([]*types.LedgerMovement, error)
    42  	GetVestingRecovery() map[string]map[string]*num.Uint
    43  	GetAllVestingQuantumBalance(party string) num.Decimal
    44  	GetAllVestingAndVestedAccountForAsset(asset string) []*types.Account
    45  }
    46  
    47  type ActivityStreakVestingMultiplier interface {
    48  	GetRewardsVestingMultiplier(party string) num.Decimal
    49  }
    50  
    51  type Broker interface {
    52  	Send(events events.Event)
    53  	Stage(event events.Event)
    54  }
    55  
    56  type Assets interface {
    57  	Get(assetID string) (*assets.Asset, error)
    58  }
    59  
    60  type Parties interface {
    61  	RelatedKeys(key string) (*types.PartyID, []string)
    62  }
    63  
    64  type StakeAccounting interface {
    65  	AddEvent(ctx context.Context, evt *types.StakeLinking)
    66  }
    67  
    68  type Time interface {
    69  	GetTimeNow() time.Time
    70  }
    71  
    72  type PartyRewards struct {
    73  	// the amounts per assets still being locked in the
    74  	// account and not available to be released
    75  	// this is a map of:
    76  	// asset -> (remainingEpochLock -> Amount)
    77  	Locked map[string]map[uint64]*num.Uint
    78  	// the current part of the vesting account
    79  	// per asset available for vesting
    80  	Vesting map[string]*num.Uint
    81  }
    82  
    83  type MultiplierAndQuantBalance struct {
    84  	Multiplier     num.Decimal
    85  	QuantumBalance num.Decimal
    86  }
    87  
    88  type Engine struct {
    89  	log *logging.Logger
    90  
    91  	c      Collateral
    92  	asvm   ActivityStreakVestingMultiplier
    93  	broker Broker
    94  	assets Assets
    95  
    96  	minTransfer  num.Decimal
    97  	baseRate     num.Decimal
    98  	benefitTiers []*types.VestingBenefitTier
    99  
   100  	state                map[string]*PartyRewards
   101  	epochSeq             uint64
   102  	upgradeHackActivated bool
   103  
   104  	parties Parties
   105  
   106  	// cache the reward bonus multiplier and quantum balance
   107  	rewardBonusMultiplierCache map[string]MultiplierAndQuantBalance
   108  
   109  	stakingAsset    string
   110  	stakeAccounting StakeAccounting
   111  
   112  	t Time
   113  }
   114  
   115  func New(
   116  	log *logging.Logger,
   117  	c Collateral,
   118  	asvm ActivityStreakVestingMultiplier,
   119  	broker Broker,
   120  	assets Assets,
   121  	parties Parties,
   122  	t Time,
   123  	stakeAccounting StakeAccounting,
   124  ) *Engine {
   125  	log = log.Named(namedLogger)
   126  
   127  	return &Engine{
   128  		log:                        log,
   129  		c:                          c,
   130  		asvm:                       asvm,
   131  		broker:                     broker,
   132  		assets:                     assets,
   133  		parties:                    parties,
   134  		state:                      map[string]*PartyRewards{},
   135  		rewardBonusMultiplierCache: map[string]MultiplierAndQuantBalance{},
   136  		t:                          t,
   137  		stakeAccounting:            stakeAccounting,
   138  	}
   139  }
   140  
   141  func (e *Engine) OnCheckpointLoaded() {
   142  	vestingBalances := e.c.GetVestingRecovery()
   143  	for party, assetBalances := range vestingBalances {
   144  		for asset, balance := range assetBalances {
   145  			e.increaseVestingBalance(party, asset, balance.Clone())
   146  		}
   147  	}
   148  }
   149  
   150  func (e *Engine) OnBenefitTiersUpdate(_ context.Context, v interface{}) error {
   151  	tiers, err := types.VestingBenefitTiersFromUntypedProto(v)
   152  	if err != nil {
   153  		return err
   154  	}
   155  
   156  	e.benefitTiers = tiers.Clone().Tiers
   157  	sort.Slice(e.benefitTiers, func(i, j int) bool {
   158  		return e.benefitTiers[i].MinimumQuantumBalance.LT(e.benefitTiers[j].MinimumQuantumBalance)
   159  	})
   160  	return nil
   161  }
   162  
   163  func (e *Engine) OnStakingAssetUpdate(_ context.Context, stakingAsset string) error {
   164  	e.stakingAsset = stakingAsset
   165  	return nil
   166  }
   167  
   168  func (e *Engine) OnRewardVestingBaseRateUpdate(_ context.Context, baseRate num.Decimal) error {
   169  	e.baseRate = baseRate
   170  	return nil
   171  }
   172  
   173  func (e *Engine) OnRewardVestingMinimumTransferUpdate(_ context.Context, minimumTransfer num.Decimal) error {
   174  	e.minTransfer = minimumTransfer
   175  	return nil
   176  }
   177  
   178  func (e *Engine) OnEpochEvent(ctx context.Context, epoch types.Epoch) {
   179  	if epoch.Action == proto.EpochAction_EPOCH_ACTION_END {
   180  		e.clearMultiplierCache()
   181  		e.moveLocked()
   182  		e.distributeVested(ctx)
   183  		e.broadcastVestingStatsUpdate(ctx, epoch.Seq)
   184  		e.broadcastSummary(ctx, epoch.Seq)
   185  		e.clearState()
   186  		e.clearMultiplierCache()
   187  	}
   188  }
   189  
   190  func (e *Engine) OnEpochRestore(_ context.Context, epoch types.Epoch) {
   191  	e.epochSeq = epoch.Seq
   192  }
   193  
   194  func (e *Engine) updateStakingAccount(
   195  	ctx context.Context,
   196  	party string,
   197  	amount *num.Uint,
   198  	logIndex uint64,
   199  	brokerFunc func(events.Event),
   200  ) {
   201  	var (
   202  		now       = e.t.GetTimeNow().Unix()
   203  		height, _ = vgcontext.BlockHeightFromContext(ctx)
   204  		txhash, _ = vgcontext.TxHashFromContext(ctx)
   205  		id        = crypto.HashStrToHex(fmt.Sprintf("%v%v%v", party, txhash, height))
   206  	)
   207  
   208  	stakeLinking := &types.StakeLinking{
   209  		ID:              id,
   210  		Type:            types.StakeLinkingTypeDeposited,
   211  		TS:              now,
   212  		Party:           party,
   213  		Amount:          amount,
   214  		Status:          types.StakeLinkingStatusAccepted,
   215  		FinalizedAt:     now,
   216  		TxHash:          txhash,
   217  		BlockHeight:     height,
   218  		BlockTime:       now,
   219  		LogIndex:        logIndex,
   220  		EthereumAddress: "",
   221  	}
   222  
   223  	e.stakeAccounting.AddEvent(context.Background(), stakeLinking)
   224  	brokerFunc(events.NewStakeLinking(ctx, *stakeLinking))
   225  }
   226  
   227  func (e *Engine) AddReward(
   228  	ctx context.Context,
   229  	party, asset string,
   230  	amount *num.Uint,
   231  	lockedForEpochs uint64,
   232  ) {
   233  	// send to staking
   234  	if asset == e.stakingAsset {
   235  		e.updateStakingAccount(ctx, party, amount.Clone(), 1, e.broker.Send)
   236  	}
   237  
   238  	// no locktime, just increase the amount in vesting
   239  	if lockedForEpochs == 0 {
   240  		e.increaseVestingBalance(party, asset, amount)
   241  		return
   242  	}
   243  
   244  	e.increaseLockedForAsset(party, asset, amount, lockedForEpochs)
   245  }
   246  
   247  func (e *Engine) rewardBonusMultiplier(quantumBalance num.Decimal) num.Decimal {
   248  	multiplier := num.DecimalOne()
   249  
   250  	for _, b := range e.benefitTiers {
   251  		if quantumBalance.LessThan(num.DecimalFromUint(b.MinimumQuantumBalance)) {
   252  			break
   253  		}
   254  
   255  		multiplier = b.RewardMultiplier
   256  	}
   257  
   258  	return multiplier
   259  }
   260  
   261  // GetSingleAndSummedRewardBonusMultipliers returns a single and summed reward bonus multipliers and quantum balances for a party.
   262  // The single multiplier is calculated based on the quantum balance of the party.
   263  // The summed multiplier is calculated based on the quantum balance of the party and all derived keys.
   264  // Caches the summed multiplier and quantum balance for the party.
   265  func (e *Engine) GetSingleAndSummedRewardBonusMultipliers(party string) (MultiplierAndQuantBalance, MultiplierAndQuantBalance) {
   266  	owner := party
   267  
   268  	partyID, derivedKeys := e.parties.RelatedKeys(party)
   269  	if partyID != nil {
   270  		owner = partyID.String()
   271  	}
   272  
   273  	ownerKey := fmt.Sprintf("owner-%s", owner)
   274  
   275  	summed, foundSummed := e.rewardBonusMultiplierCache[ownerKey]
   276  
   277  	for _, key := range append(derivedKeys, owner) {
   278  		single, foundSingle := e.rewardBonusMultiplierCache[key]
   279  		if !foundSingle {
   280  			quantumBalanceForKey := e.c.GetAllVestingQuantumBalance(key)
   281  
   282  			single.QuantumBalance = quantumBalanceForKey
   283  			single.Multiplier = e.rewardBonusMultiplier(quantumBalanceForKey)
   284  			e.rewardBonusMultiplierCache[key] = single
   285  		}
   286  
   287  		if !foundSummed {
   288  			summed.QuantumBalance = summed.QuantumBalance.Add(single.QuantumBalance)
   289  		}
   290  	}
   291  
   292  	if !foundSummed {
   293  		summed.Multiplier = e.rewardBonusMultiplier(summed.QuantumBalance)
   294  		e.rewardBonusMultiplierCache[ownerKey] = summed
   295  	}
   296  
   297  	return e.rewardBonusMultiplierCache[party], e.rewardBonusMultiplierCache[ownerKey]
   298  }
   299  
   300  func (e *Engine) getPartyRewards(party string) *PartyRewards {
   301  	partyRewards, ok := e.state[party]
   302  	if !ok {
   303  		e.state[party] = &PartyRewards{
   304  			Locked:  map[string]map[uint64]*num.Uint{},
   305  			Vesting: map[string]*num.Uint{},
   306  		}
   307  		partyRewards = e.state[party]
   308  	}
   309  
   310  	return partyRewards
   311  }
   312  
   313  func (e *Engine) increaseLockedForAsset(
   314  	party, asset string,
   315  	amount *num.Uint,
   316  	lockedForEpochs uint64,
   317  ) {
   318  	partyRewards := e.getPartyRewards(party)
   319  	locked, ok := partyRewards.Locked[asset]
   320  	if !ok {
   321  		locked = map[uint64]*num.Uint{}
   322  	}
   323  	amountLockedForEpochs, ok := locked[lockedForEpochs]
   324  	if !ok {
   325  		amountLockedForEpochs = num.UintZero()
   326  	}
   327  	amountLockedForEpochs.Add(amountLockedForEpochs, amount)
   328  	locked[lockedForEpochs] = amountLockedForEpochs
   329  	partyRewards.Locked[asset] = locked
   330  }
   331  
   332  func (e *Engine) increaseVestingBalance(
   333  	party, asset string,
   334  	amount *num.Uint,
   335  ) {
   336  	partyRewards := e.getPartyRewards(party)
   337  
   338  	vesting, ok := partyRewards.Vesting[asset]
   339  	if !ok {
   340  		vesting = num.UintZero()
   341  	}
   342  	vesting.Add(vesting, amount)
   343  	partyRewards.Vesting[asset] = vesting
   344  }
   345  
   346  // moveLocked will move around locked funds.
   347  // if the lock for epoch reach 0, the full amount
   348  // is added to the vesting amount for the asset.
   349  func (e *Engine) moveLocked() {
   350  	for party, partyReward := range e.state {
   351  		for asset, assetLocks := range partyReward.Locked {
   352  			newLocked := map[uint64]*num.Uint{}
   353  			for epochLeft, amount := range assetLocks {
   354  				if epochLeft == 0 {
   355  					e.increaseVestingBalance(party, asset, amount)
   356  					continue
   357  				}
   358  				epochLeft--
   359  				// just add the new map
   360  				newLocked[epochLeft] = amount
   361  			}
   362  
   363  			// clear up if no rewards left
   364  			if len(newLocked) <= 0 {
   365  				delete(partyReward.Locked, asset)
   366  				continue
   367  			}
   368  
   369  			partyReward.Locked[asset] = newLocked
   370  		}
   371  	}
   372  }
   373  
   374  func (e *Engine) distributeVested(ctx context.Context) {
   375  	transfers := []*types.Transfer{}
   376  	parties := maps.Keys(e.state)
   377  	sort.Strings(parties)
   378  	for _, party := range parties {
   379  		rewards := e.state[party]
   380  		assets := maps.Keys(rewards.Vesting)
   381  		sort.Strings(assets)
   382  		for _, asset := range assets {
   383  			balance := rewards.Vesting[asset]
   384  			transfer := e.makeTransfer(party, asset, balance.Clone())
   385  
   386  			// we are clearing the account,
   387  			// we can delete it.
   388  			if transfer.MinAmount.EQ(balance) {
   389  				delete(rewards.Vesting, asset)
   390  			} else {
   391  				rewards.Vesting[asset] = balance.Sub(balance, transfer.MinAmount)
   392  			}
   393  
   394  			transfers = append(transfers, transfer)
   395  		}
   396  	}
   397  
   398  	// nothing to be done
   399  	if len(transfers) == 0 {
   400  		return
   401  	}
   402  
   403  	responses, err := e.c.TransferVestedRewards(ctx, transfers)
   404  	if err != nil {
   405  		e.log.Panic("could not transfer funds", logging.Error(err))
   406  	}
   407  
   408  	e.broker.Send(events.NewLedgerMovements(ctx, responses))
   409  }
   410  
   411  // OnTick is called on the beginning of the block. In here
   412  // this is a post upgrade.
   413  func (e *Engine) OnTick(ctx context.Context, _ time.Time) {
   414  	if e.upgradeHackActivated {
   415  		e.broadcastSummary(ctx, e.epochSeq)
   416  		e.upgradeHackActivated = false
   417  	}
   418  }
   419  
   420  func (e *Engine) makeTransfer(
   421  	party, assetID string,
   422  	balance *num.Uint,
   423  ) *types.Transfer {
   424  	asset, _ := e.assets.Get(assetID)
   425  	quantum := asset.Type().Details.Quantum
   426  	minTransferAmount, _ := num.UintFromDecimal(quantum.Mul(e.minTransfer))
   427  
   428  	transfer := &types.Transfer{
   429  		Owner: party,
   430  		Amount: &types.FinancialAmount{
   431  			Asset: assetID,
   432  		},
   433  		Type: types.TransferTypeRewardsVested,
   434  	}
   435  
   436  	expectTransfer, _ := num.UintFromDecimal(
   437  		balance.ToDecimal().Mul(e.baseRate).Mul(e.asvm.GetRewardsVestingMultiplier(party)),
   438  	)
   439  
   440  	// now we see which is the largest between the minimumTransfer
   441  	// and the expected transfer
   442  	expectTransfer = num.Max(expectTransfer, minTransferAmount)
   443  
   444  	// and now we prevent any transfer to exceed the current balance
   445  	expectTransfer = num.Min(expectTransfer, balance)
   446  
   447  	transfer.Amount.Amount = expectTransfer.Clone()
   448  	transfer.MinAmount = expectTransfer
   449  
   450  	return transfer
   451  }
   452  
   453  func (e *Engine) clearState() {
   454  	for party, v := range e.state {
   455  		if len(v.Locked) == 0 && len(v.Vesting) == 0 {
   456  			delete(e.state, party)
   457  		}
   458  	}
   459  }
   460  
   461  func (e *Engine) clearMultiplierCache() {
   462  	e.rewardBonusMultiplierCache = map[string]MultiplierAndQuantBalance{}
   463  }
   464  
   465  func (e *Engine) broadcastSummary(ctx context.Context, seq uint64) {
   466  	evt := &eventspb.VestingBalancesSummary{
   467  		EpochSeq:              seq,
   468  		PartiesVestingSummary: []*eventspb.PartyVestingSummary{},
   469  	}
   470  
   471  	for p, pRewards := range e.state {
   472  		if len(pRewards.Vesting) == 0 && len(pRewards.Locked) == 0 {
   473  			continue
   474  		}
   475  
   476  		pSummary := &eventspb.PartyVestingSummary{
   477  			Party:                p,
   478  			PartyLockedBalances:  []*eventspb.PartyLockedBalance{},
   479  			PartyVestingBalances: []*eventspb.PartyVestingBalance{},
   480  		}
   481  
   482  		// doing vesting first
   483  		for asset, balance := range pRewards.Vesting {
   484  			pSummary.PartyVestingBalances = append(
   485  				pSummary.PartyVestingBalances,
   486  				&eventspb.PartyVestingBalance{
   487  					Asset:   asset,
   488  					Balance: balance.String(),
   489  				},
   490  			)
   491  		}
   492  
   493  		sort.Slice(pSummary.PartyVestingBalances, func(i, j int) bool {
   494  			return pSummary.PartyVestingBalances[i].Asset < pSummary.PartyVestingBalances[j].Asset
   495  		})
   496  
   497  		for asset, remainingEpochLockBalance := range pRewards.Locked {
   498  			for remainingEpochs, balance := range remainingEpochLockBalance {
   499  				pSummary.PartyLockedBalances = append(
   500  					pSummary.PartyLockedBalances,
   501  					&eventspb.PartyLockedBalance{
   502  						Asset:      asset,
   503  						Balance:    balance.String(),
   504  						UntilEpoch: seq + remainingEpochs + 1, // we add one here because the remainingEpochs can be 0, meaning the funds are released next epoch
   505  					},
   506  				)
   507  			}
   508  		}
   509  
   510  		sort.Slice(pSummary.PartyLockedBalances, func(i, j int) bool {
   511  			if pSummary.PartyLockedBalances[i].Asset == pSummary.PartyLockedBalances[j].Asset {
   512  				return pSummary.PartyLockedBalances[i].UntilEpoch < pSummary.PartyLockedBalances[j].UntilEpoch
   513  			}
   514  			return pSummary.PartyLockedBalances[i].Asset < pSummary.PartyLockedBalances[j].Asset
   515  		})
   516  
   517  		evt.PartiesVestingSummary = append(evt.PartiesVestingSummary, pSummary)
   518  	}
   519  
   520  	sort.Slice(evt.PartiesVestingSummary, func(i, j int) bool {
   521  		return evt.PartiesVestingSummary[i].Party < evt.PartiesVestingSummary[j].Party
   522  	})
   523  
   524  	e.broker.Send(events.NewVestingBalancesSummaryEvent(ctx, evt))
   525  }
   526  
   527  func (e *Engine) broadcastVestingStatsUpdate(ctx context.Context, seq uint64) {
   528  	evt := &eventspb.VestingStatsUpdated{
   529  		AtEpoch: seq,
   530  		Stats:   make([]*eventspb.PartyVestingStats, 0, len(e.state)),
   531  	}
   532  
   533  	parties := maps.Keys(e.state)
   534  	slices.Sort(parties)
   535  
   536  	for _, party := range parties {
   537  		single, summed := e.GetSingleAndSummedRewardBonusMultipliers(party)
   538  		// To avoid excessively large decimals.
   539  		single.QuantumBalance.Round(2)
   540  		summed.QuantumBalance.Round(2)
   541  		evt.Stats = append(evt.Stats, &eventspb.PartyVestingStats{
   542  			PartyId:                     party,
   543  			RewardBonusMultiplier:       single.Multiplier.String(),
   544  			QuantumBalance:              single.QuantumBalance.String(),
   545  			SummedRewardBonusMultiplier: summed.Multiplier.String(),
   546  			SummedQuantumBalance:        summed.QuantumBalance.String(),
   547  		})
   548  	}
   549  
   550  	e.broker.Send(events.NewVestingStatsUpdatedEvent(ctx, evt))
   551  }