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 }