code.vegaprotocol.io/vega@v0.79.0/core/execution/common/liquidity_provision_fees.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 common 17 18 import ( 19 "context" 20 "fmt" 21 "math/rand" 22 "sort" 23 24 "code.vegaprotocol.io/vega/core/events" 25 "code.vegaprotocol.io/vega/core/liquidity/v2" 26 "code.vegaprotocol.io/vega/core/types" 27 "code.vegaprotocol.io/vega/libs/num" 28 "code.vegaprotocol.io/vega/logging" 29 30 "golang.org/x/exp/constraints" 31 ) 32 33 func (m *MarketLiquidity) NewTransfer(partyID string, transferType types.TransferType, amount *num.Uint) *types.Transfer { 34 return &types.Transfer{ 35 Owner: partyID, 36 Amount: &types.FinancialAmount{ 37 Asset: m.asset, 38 Amount: amount.Clone(), 39 }, 40 Type: transferType, 41 MinAmount: amount.Clone(), 42 Market: m.marketID, 43 } 44 } 45 46 type FeeTransfer struct { 47 transfers []*types.Transfer 48 totalFeesPerParty map[string]*num.Uint 49 } 50 51 func NewFeeTransfer(transfers []*types.Transfer, totalFeesPerParty map[string]*num.Uint) FeeTransfer { 52 return FeeTransfer{ 53 transfers: transfers, 54 totalFeesPerParty: totalFeesPerParty, 55 } 56 } 57 58 func (ft FeeTransfer) Transfers() []*types.Transfer { 59 return ft.transfers 60 } 61 62 func (ft FeeTransfer) TotalFeesAmountPerParty() map[string]*num.Uint { 63 return ft.totalFeesPerParty 64 } 65 66 // AllocateFees distributes fee from a market fee account to LP fee accounts. 67 func (m *MarketLiquidity) AllocateFees(ctx context.Context) error { 68 acc, err := m.collateral.GetMarketLiquidityFeeAccount(m.marketID, m.asset) 69 if err != nil { 70 return fmt.Errorf("failed to get market liquidity fee account: %w", err) 71 } 72 73 // We can't distribute any share when no balance. 74 if acc.Balance.IsZero() { 75 return nil 76 } 77 78 // if there are AMM's which were created during this epoch and we are distributing fees 79 // want want to using a mid-epoch ELS 80 newAMMs := []string{} 81 for ammParty, stats := range m.ammStats { 82 if !stats.stake.IsZero() && !m.equityShares.HasShares(ammParty) { 83 // add a temporary mid epoch share 84 stake, _ := num.UintFromDecimal(stats.stake) 85 m.equityShares.SetPartyStake(ammParty, stake) 86 newAMMs = append(newAMMs, ammParty) 87 } 88 } 89 90 // Get equity like shares per party. 91 sharesPerLp := m.equityShares.AllShares() 92 93 // revert the temporary ELS for the new AMM 94 for _, ammParty := range newAMMs { 95 m.equityShares.SetPartyStake(ammParty, nil) 96 } 97 98 // get the LP scores 99 scoresPerLp := m.liquidityEngine.GetAverageLiquidityScores() 100 if len(sharesPerLp) == 0 && len(m.ammStats) == 0 && len(scoresPerLp) == 0 { 101 return nil 102 } 103 104 // include AMM average LP scores 105 for id, av := range m.ammStats { 106 scoresPerLp[id] = av.score 107 } 108 109 // a map of all the LP scores, both AMM and LPs, scaled to 1. 110 scaledScores := m.scaleScores(scoresPerLp) 111 // Multiplies each equity like share with corresponding score, scaled to 1. 112 updatedShares := m.updateSharesWithLiquidityScores(sharesPerLp, scoresPerLp) 113 // now combine the above maps, multiply the ELS part with the fee factor, the score-based map by 1 - factor. 114 updatedShares = m.mergeScores(updatedShares, scaledScores) 115 116 feeTransfer := m.fee.BuildLiquidityFeeAllocationTransfer(updatedShares, acc) 117 if feeTransfer == nil { 118 return nil 119 } 120 121 ledgerMovements, err := m.transferFees(ctx, feeTransfer) 122 if err != nil { 123 return fmt.Errorf("failed to transfer fees: %w", err) 124 } 125 126 m.liquidityEngine.RegisterAllocatedFeesPerParty(feeTransfer.TotalFeesAmountPerParty()) 127 128 if len(ledgerMovements) > 0 { 129 m.broker.Send(events.NewLedgerMovements(ctx, ledgerMovements)) 130 } 131 132 return nil 133 } 134 135 func (m *MarketLiquidity) updateAMMCommitment(count int64) { 136 if m.amm == nil { 137 // spot market, amm won't be set 138 return 139 } 140 141 minP, maxP, err := m.ValidOrdersPriceRange() 142 if err != nil { 143 m.log.Debug("get amm scores error", logging.Error(err)) 144 // no price range -> we cannot determine the AMM scores. 145 return 146 } 147 148 skipScore := false 149 bestB, err := m.orderBook.GetBestStaticBidPrice() 150 if err != nil { 151 m.log.Debug("could not get best bid", logging.Error(err)) 152 skipScore = true 153 } 154 bestA, err := m.orderBook.GetBestStaticAskPrice() 155 if err != nil { 156 m.log.Debug("could not get best ask", logging.Error(err)) 157 skipScore = true 158 } 159 160 pools := m.amm.GetAMMPoolsBySubAccount() 161 for ammParty, pool := range pools { 162 r := pool.OrderbookShape(minP, maxP, nil) 163 164 buyTotal, sellTotal := num.UintZero(), num.UintZero() 165 for _, b := range r.Buys { 166 size := num.UintFromUint64(b.Size) 167 buyTotal.AddSum(size.Mul(size, b.Price)) 168 } 169 for _, s := range r.Sells { 170 size := num.UintFromUint64(s.Size) 171 sellTotal.AddSum(size.Mul(size, s.Price)) 172 } 173 174 // divide the lesser value by the market.liquidity.stakeToCcyVolume to get the equivalent bond 175 value := num.Min(buyTotal, sellTotal) 176 stake := num.DecimalFromUint(value).Div(m.stakeToCcyVolume) 177 178 as, ok := m.ammStats[ammParty] 179 if !ok { 180 as = newAMMState(count) 181 m.ammStats[ammParty] = as 182 } 183 184 score := as.score 185 if !skipScore { 186 bb, ba := num.DecimalFromUint(bestB), num.DecimalFromUint(bestA) 187 score = m.liquidityEngine.GetPartyLiquidityScore(append(r.Buys, r.Sells...), bb, ba, minP, maxP) 188 } 189 190 // set the stake and score 191 as.UpdateTick(stake, score) 192 } 193 194 // if we have an AMM that is in our stats but isn't in the current AMM engine then it has been cancelled so we give it a score of 0 195 for ammParty, stats := range m.ammStats { 196 if _, ok := pools[ammParty]; !ok { 197 stats.UpdateTick(num.DecimalZero(), num.DecimalZero()) 198 } 199 } 200 } 201 202 func (m *MarketLiquidity) scaleScores(scores map[string]num.Decimal) map[string]num.Decimal { 203 if len(scores) == 0 { 204 return scores 205 } 206 total := num.DecimalZero() 207 for _, s := range scores { 208 total = total.Add(s) 209 } 210 211 if total.IsZero() { 212 return scores 213 } 214 215 for k, v := range scores { 216 scores[k] = v.Div(total) 217 } 218 return scores 219 } 220 221 func (m *MarketLiquidity) mergeScores(els, scores map[string]num.Decimal) map[string]num.Decimal { 222 if len(scores) == 0 { 223 return els 224 } 225 if len(els) == 0 { 226 return scores // this probably never happens 227 } 228 // len(scores) because every entry in the ELS map will have a score 229 // the other way around is not certain. 230 ret := make(map[string]num.Decimal, len(scores)) 231 scoreFactor := num.DecimalOne().Sub(m.elsFeeFactor) // if amm is entitled to 0.2 of the fees, then ELS receives 0.8 232 for k, v := range els { 233 if v.IsZero() { 234 continue // omit zero values 235 } 236 ret[k] = v.Mul(m.elsFeeFactor) // the score * ELS map is entitled to the elsFeeFactor of the total fees 237 } 238 for k, v := range scores { 239 rv := v.Mul(scoreFactor) 240 if ev, ok := ret[k]; ok { 241 rv = rv.Add(ev) // party has ELS, just add the portion of fees they are entitled to from the second bucket 242 } 243 if rv.IsZero() { 244 continue // again, leave out the zero values 245 } 246 ret[k] = rv // add the entry to the map of either the score-based portion, or the sum of ELS and score. 247 } 248 return ret 249 } 250 251 func (m *MarketLiquidity) processBondPenalties( 252 ctx context.Context, 253 partyIDs []string, 254 penaltiesPerParty map[string]*liquidity.SlaPenalty, 255 ) { 256 ledgerMovements := make([]*types.LedgerMovement, 0, len(partyIDs)) 257 258 for _, partyID := range partyIDs { 259 penalty := penaltiesPerParty[partyID] 260 261 provision := m.liquidityEngine.LiquidityProvisionByPartyID(partyID) 262 263 amountUint := num.UintZero() 264 if provision != nil { 265 // bondPenalty x commitmentAmount. 266 amount := penalty.Bond.Mul(provision.CommitmentAmount.ToDecimal()) 267 amountUint, _ = num.UintFromDecimal(amount) 268 } 269 270 if amountUint.IsZero() { 271 continue 272 } 273 274 transfer := m.NewTransfer(partyID, types.TransferTypeSLAPenaltyBondApply, amountUint) 275 276 bondLedgerMovement, err := m.bondUpdate(ctx, transfer) 277 if err != nil { 278 m.log.Panic("failed to apply SLA penalties to bond account", logging.Error(err)) 279 } 280 281 ledgerMovements = append(ledgerMovements, bondLedgerMovement) 282 } 283 284 if len(ledgerMovements) > 0 { 285 m.broker.Send(events.NewLedgerMovements(ctx, ledgerMovements)) 286 } 287 } 288 289 func (m *MarketLiquidity) getAccruedPerPartyAndTotalFees(partyIDs []string) (map[string]*num.Uint, *num.Uint) { 290 perParty := map[string]*num.Uint{} 291 total := num.UintZero() 292 293 for _, partyID := range partyIDs { 294 liquidityFeeAcc, err := m.collateral.GetPartyLiquidityFeeAccount(m.marketID, partyID, m.asset) 295 if err != nil { 296 m.log.Panic("failed to get party liquidity fee account", logging.Error(err)) 297 } 298 299 perParty[partyID] = liquidityFeeAcc.Balance.Clone() 300 total.AddSum(liquidityFeeAcc.Balance) 301 } 302 303 return perParty, total 304 } 305 306 func (m *MarketLiquidity) distributeFeesAndCalculateBonuses( 307 ctx context.Context, 308 partyIDs []string, 309 slaPenalties liquidity.SlaPenalties, 310 ) map[string]num.Decimal { 311 perPartAccruedFees, totalAccruedFees := m.getAccruedPerPartyAndTotalFees(partyIDs) 312 313 allTransfers := FeeTransfer{ 314 transfers: []*types.Transfer{}, 315 totalFeesPerParty: perPartAccruedFees, 316 } 317 318 bonusPerParty := map[string]num.Decimal{} 319 totalBonuses := num.DecimalZero() 320 321 for _, partyID := range partyIDs { 322 accruedFeeAmount := perPartAccruedFees[partyID] 323 324 // if all parties have a full penalty then transfer all accrued fees to insurance pool. 325 if slaPenalties.AllPartiesHaveFullFeePenalty && !accruedFeeAmount.IsZero() { 326 transfer := m.NewTransfer(partyID, types.TransferTypeSLAPenaltyLpFeeApply, accruedFeeAmount) 327 allTransfers.transfers = append(allTransfers.transfers, transfer) 328 continue 329 } 330 331 penalty := slaPenalties.PenaltiesPerParty[partyID] 332 oneMinusPenalty := num.DecimalOne().Sub(penalty.Fee) 333 334 // transfers fees after penalty is applied. 335 // (1-feePenalty) x accruedFeeAmount. 336 netDistributionAmount := oneMinusPenalty.Mul(accruedFeeAmount.ToDecimal()) 337 netDistributionAmountUint, _ := num.UintFromDecimal(netDistributionAmount) 338 339 if !netDistributionAmountUint.IsZero() { 340 netFeeDistributeTransfer := m.NewTransfer(partyID, types.TransferTypeLiquidityFeeNetDistribute, netDistributionAmountUint) 341 allTransfers.transfers = append(allTransfers.transfers, netFeeDistributeTransfer) 342 } 343 344 // transfer unpaid accrued fee to bonus account 345 // accruedFeeAmount - netDistributionAmountUint 346 unpaidFees := num.UintZero().Sub(accruedFeeAmount, netDistributionAmountUint) 347 if !unpaidFees.IsZero() { 348 unpaidFeesTransfer := m.NewTransfer(partyID, types.TransferTypeLiquidityFeeUnpaidCollect, unpaidFees) 349 allTransfers.transfers = append(allTransfers.transfers, unpaidFeesTransfer) 350 } 351 352 bonus := num.DecimalZero() 353 // this is just to avoid panic. 354 if !totalAccruedFees.IsZero() { 355 // calculate bonus. 356 // (1-feePenalty) x (accruedFeeAmount/totalAccruedFees). 357 bonus = oneMinusPenalty.Mul(accruedFeeAmount.ToDecimal().Div(totalAccruedFees.ToDecimal())) 358 } 359 360 totalBonuses = totalBonuses.Add(bonus) 361 bonusPerParty[partyID] = bonus 362 } 363 364 m.marketActivityTracker.UpdateFeesFromTransfers(m.asset, m.marketID, allTransfers.transfers) 365 366 // transfer all the fees. 367 ledgerMovements, err := m.transferFees(ctx, allTransfers) 368 if err != nil { 369 m.log.Panic("failed to transfer fees from LP's fees accounts", logging.Error(err)) 370 } 371 372 if len(ledgerMovements) > 0 { 373 m.broker.Send(events.NewLedgerMovements(ctx, ledgerMovements)) 374 } 375 376 if !totalBonuses.IsZero() { 377 // normalize bonuses. 378 for party, bonus := range bonusPerParty { 379 // bonus / totalBonuses. 380 bonusPerParty[party] = bonus.Div(totalBonuses) 381 } 382 } 383 384 return bonusPerParty 385 } 386 387 func (m *MarketLiquidity) distributePerformanceBonuses( 388 ctx context.Context, 389 partyIDs []string, 390 bonuses map[string]num.Decimal, 391 ) { 392 bonusDistributionAcc, err := m.collateral.GetLiquidityFeesBonusDistributionAccount(m.marketID, m.asset) 393 if err != nil { 394 m.log.Panic("failed to get bonus distribution account", logging.Error(err)) 395 } 396 397 bonusTransfers := FeeTransfer{ 398 transfers: []*types.Transfer{}, 399 } 400 401 remainingBalance := bonusDistributionAcc.Balance.Clone() 402 for _, partyID := range partyIDs { 403 bonus := bonuses[partyID] 404 405 // if bonus is 0 there is no need to process. 406 if bonus.IsZero() { 407 continue 408 } 409 410 amountD := bonus.Mul(bonusDistributionAcc.Balance.ToDecimal()) 411 amount, _ := num.UintFromDecimal(amountD) 412 413 if !amount.IsZero() { 414 transfer := m.NewTransfer(partyID, types.TransferTypeSlaPerformanceBonusDistribute, amount) 415 bonusTransfers.transfers = append(bonusTransfers.transfers, transfer) 416 } 417 418 remainingBalance.Sub(remainingBalance, amount) 419 } 420 421 // in case of remaining balance choose pseudo random provider to receive it. 422 if !remainingBalance.IsZero() { 423 keys := sortedKeys(bonuses) 424 425 rand.Seed(remainingBalance.BigInt().Int64()) 426 randIndex := rand.Intn(len(keys)) 427 selectedParty := keys[randIndex] 428 429 transfer := m.NewTransfer(selectedParty, types.TransferTypeSlaPerformanceBonusDistribute, remainingBalance) 430 bonusTransfers.transfers = append(bonusTransfers.transfers, transfer) 431 } 432 433 m.marketActivityTracker.UpdateFeesFromTransfers(m.asset, m.marketID, bonusTransfers.transfers) 434 ledgerMovements, err := m.transferFees(ctx, bonusTransfers) 435 if err != nil { 436 m.log.Panic("failed to distribute SLA bonuses", logging.Error(err)) 437 } 438 439 if len(ledgerMovements) > 0 { 440 m.broker.Send(events.NewLedgerMovements(ctx, ledgerMovements)) 441 } 442 } 443 444 func (m *MarketLiquidity) distributeFeesBonusesAndApplyPenalties( 445 ctx context.Context, 446 slaPenalties liquidity.SlaPenalties, 447 ) { 448 // No LP penalties available so no need to continue. 449 // This could happen during opening auction. 450 if len(slaPenalties.PenaltiesPerParty) < 1 { 451 return 452 } 453 454 partyIDs := sortedKeys(slaPenalties.PenaltiesPerParty) 455 456 // first process bond penalties. 457 m.processBondPenalties(ctx, partyIDs, slaPenalties.PenaltiesPerParty) 458 459 // then distribute fees and calculate bonus. 460 bonusPerParty := m.distributeFeesAndCalculateBonuses(ctx, partyIDs, slaPenalties) 461 462 // lastly distribute performance bonus. 463 m.distributePerformanceBonuses(ctx, partyIDs, bonusPerParty) 464 } 465 466 func (m *MarketLiquidity) SetELSFeeFraction(d num.Decimal) { 467 m.elsFeeFactor = d 468 } 469 470 func sortedKeys[K constraints.Ordered, V any](m map[K]V) []K { 471 keys := make([]K, 0, len(m)) 472 for k := range m { 473 keys = append(keys, k) 474 } 475 476 sort.Slice(keys, func(i, j int) bool { 477 return keys[i] < keys[j] 478 }) 479 480 return keys 481 }