From 78cadd257614b3d121859059811d68fe61de6bb5 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Mon, 18 Dec 2023 12:01:30 +0200 Subject: [PATCH 01/44] chore: extract calls to staking-supervisor into a lib for reusing --- .../transactions-validation/paraBoost.ts | 16 ++-------- src/lib/utils/staking-supervisor.ts | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 src/lib/utils/staking-supervisor.ts diff --git a/scripts/gas-refund-program/transactions-validation/paraBoost.ts b/scripts/gas-refund-program/transactions-validation/paraBoost.ts index f038aa82..fa744672 100644 --- a/scripts/gas-refund-program/transactions-validation/paraBoost.ts +++ b/scripts/gas-refund-program/transactions-validation/paraBoost.ts @@ -1,25 +1,15 @@ -import axios from 'axios'; import { assert } from 'ts-essentials'; import { GasRefundV2EpochFlip } from '../../../src/lib/gas-refund/gas-refund'; +import { fetchAccountsScores } from '../../../src/lib/utils/staking-supervisor'; export type ParaBoostPerAccount = { [account: string]: number }; -type MinParaBoostData = { - account: string; - paraBoostFactor: string; -}; async function fetchParaBoostPerAccount(epoch1: number) { const epoch2 = epoch1 - GasRefundV2EpochFlip; assert(epoch2 >= 0, 'epoch2 can never be negative'); - - const { data } = await axios.get( - `https://api.paraswap.io/stk/paraboost/list?epoch=${epoch2}`, - ); - assert( - data.length > 0, - 'logic error: unlikely that no paraboost was recorded', - ); + + const data = await fetchAccountsScores(epoch2); const paraBoostFactorByAccount = data.reduce( (acc, paraBoostData) => { diff --git a/src/lib/utils/staking-supervisor.ts b/src/lib/utils/staking-supervisor.ts new file mode 100644 index 00000000..bfe02296 --- /dev/null +++ b/src/lib/utils/staking-supervisor.ts @@ -0,0 +1,32 @@ +import axios from 'axios'; +import { assert } from 'ts-essentials'; + +export type MinParaBoostData = { + account: string; + score: string; + stakesScore: string; + sePSP1UnderlyingPSPBalance: string; + sePSP2UnderlyingPSPBalance: string; + claimableSePSP1Balance: string; + paraBoostFactor: string; +}; + +export async function fetchAccountsScores( + epoch: number, +): Promise { + const { data } = await axios.get( + `https://api.paraswap.io/stk/paraboost/list?epoch=${epoch}`, + ); + assert( + data.length > 0, + 'logic error: unlikely that no paraboost was recorded', + ); + return data; +} + +export async function fetchTotalScore(epoch: number): Promise { + const { data } = await axios.get( + `https://api.paraswap.io/stk/paraboost/totalScore?epoch=${epoch}`, + ); + return data; +} From e3a19ee0c8b9a484d38b7c0d797aa9fe726d4c6e Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Mon, 18 Dec 2023 12:22:02 +0200 Subject: [PATCH 02/44] draft: serve aura rewards part in the sePSP1 distribution on Optimism --- src/lib/gas-refund/gas-refund-api.ts | 78 ++++++++++++++++++---------- src/lib/utils/aura-rewards.ts | 72 +++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 27 deletions(-) create mode 100644 src/lib/utils/aura-rewards.ts diff --git a/src/lib/gas-refund/gas-refund-api.ts b/src/lib/gas-refund/gas-refund-api.ts index bcea5d18..d7633b9b 100644 --- a/src/lib/gas-refund/gas-refund-api.ts +++ b/src/lib/gas-refund/gas-refund-api.ts @@ -23,6 +23,8 @@ import { } from '../../models/sePSPMigrations'; import { getCurrentEpoch, resolveV2EpochNumber } from './epoch-helpers'; import { grp2CConfigParticularities } from './config'; +import { getApproximateUserRewardWei } from '../utils/aura-rewards'; +import BigNumber from 'bignumber.js'; interface MerkleRedeem extends Contract { callStatic: { @@ -128,6 +130,9 @@ type MigrationData = hasMigrated: true; } & SePSPMigrationsData); +type DistData = (GasRefundClaim & { + amountsByProgram?: Record; +})[]; export class GasRefundApi { merkleRedem: MerkleRedeem; merkleRedemSePSP1?: MerkleRedeem; @@ -185,34 +190,53 @@ export class GasRefundApi { }; } - async _fetchMerkleData(address: string): Promise { - const grpDataResult: GasRefundClaim[] = ( - await Promise.all([ - Database.sequelize.query(MERKLE_DATA_SQL_QUERY, { - type: Sequelize.QueryTypes.SELECT, - replacements: { - address, - chainId: this.network, - }, - }), - ( - await GasRefundParticipation.findAll({ - raw: true, - where: { - address, - chainId: this.network, - epoch: { - [Sequelize.Op.gte]: EPOCH_WHEN_OPTIMISM_STAKING_ENABLED, - }, + async _fetchMerkleData(address: string): Promise { + const oldEpochs = Database.sequelize.query( + MERKLE_DATA_SQL_QUERY, + { + type: Sequelize.QueryTypes.SELECT, + replacements: { + address, + chainId: this.network, + }, + }, + ); + + const newEpochs = GasRefundParticipation.findAll({ + raw: true, + where: { + address, + chainId: this.network, + epoch: { + [Sequelize.Op.gte]: EPOCH_WHEN_OPTIMISM_STAKING_ENABLED, + }, + }, + }).then(v => { + return Promise.all( + v.map(async m => { + const auraWei = await getApproximateUserRewardWei( + m.address.toLowerCase(), + m.epoch, + m.chainId, + ); + return { + amountsByProgram: { + aura: auraWei, + paraswapGasRefund: new BigNumber(m.amount) + .minus(auraWei) + .toFixed(), }, - }) - ).map(m => ({ - refundedAmountPSP: m.amount, - epoch: m.epoch, - address: m.address, - merkleProofs: m.merkleProofs, - })), - ]) + refundedAmountPSP: m.amount, + epoch: m.epoch, + address: m.address, + merkleProofs: m.merkleProofs, + }; + }), + ); + }); + + const grpDataResult: DistData = ( + await Promise.all([oldEpochs, newEpochs]) ).flat(); return grpDataResult; diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts new file mode 100644 index 00000000..2de2940d --- /dev/null +++ b/src/lib/utils/aura-rewards.ts @@ -0,0 +1,72 @@ +import * as pMemoize from 'p-memoize'; +import { + MinParaBoostData, + fetchAccountsScores, + fetchTotalScore, +} from './staking-supervisor'; +import BigNumber from 'bignumber.js'; +import { GasRefundV2EpochFlip } from '../gas-refund/gas-refund'; +import { CHAIN_ID_OPTIMISM } from '../constants'; +import { getCurrentEpoch } from '../gas-refund/epoch-helpers'; + +const config: Record = { + // @TODO: update this config + 39: '1234567890123123123', +}; + +const AURA_REWARDS_START_EPOCH_OLD_STYLE = Math.min(...Object.keys(config).map(Number)); + +const logger = global.LOGGER('aura-rewards'); + +async function _fetchEpochData(epoch: number) { + const [totalScore, list] = await Promise.all([ + fetchTotalScore(epoch), + fetchAccountsScores(epoch), + ]); + + const byAccountLowercase = list.reduce>( + (acc, curr) => { + acc[curr.account.toLowerCase()] = curr; + return acc; + }, + {}, + ); + + logger.info(`loaded pre-requisites for computing Aura rewards for epoch ${epoch}`) + + return { + totalScore, + list, + byAccountLowercase, + }; +} + +// cache forever +const fetchPastEpochData = pMemoize(_fetchEpochData, { + cacheKey: ([epoch]) => `paraboost_epochData_${epoch}`, +}); + +// approximate -> because there's no integrity check (sum of parts = whole), just simple pro rata calc +export async function getApproximateUserRewardWei( + user: string, + epochOldStyle: number, + chainId: number +) { + if(epochOldStyle == getCurrentEpoch()){ + // not known yet + return "0"; + } + if(chainId !== CHAIN_ID_OPTIMISM) return "0" + // for epochs before Aura rewards started, return 0 + if(epochOldStyle < AURA_REWARDS_START_EPOCH_OLD_STYLE) return "0" + + const epoch = epochOldStyle - GasRefundV2EpochFlip; + const { byAccountLowercase, totalScore } = await fetchPastEpochData(epoch); + + const userScore = byAccountLowercase[user.toLowerCase()].score || '0'; + + return new BigNumber(userScore) + .times(config[epochOldStyle] || '0') + .div(totalScore) + .toFixed(0); +} From 79eb797a87089957be3765095a92202415143a35 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:44:26 +0200 Subject: [PATCH 03/44] chore: some annotations --- src/lib/utils/aura-rewards.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index 2de2940d..b2749d62 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -14,7 +14,9 @@ const config: Record = { 39: '1234567890123123123', }; -const AURA_REWARDS_START_EPOCH_OLD_STYLE = Math.min(...Object.keys(config).map(Number)); +const AURA_REWARDS_START_EPOCH_OLD_STYLE = Math.min( + ...Object.keys(config).map(Number), +); const logger = global.LOGGER('aura-rewards'); @@ -32,7 +34,9 @@ async function _fetchEpochData(epoch: number) { {}, ); - logger.info(`loaded pre-requisites for computing Aura rewards for epoch ${epoch}`) + logger.info( + `loaded pre-requisites for computing Aura rewards for epoch ${epoch}`, + ); return { totalScore, @@ -50,15 +54,18 @@ const fetchPastEpochData = pMemoize(_fetchEpochData, { export async function getApproximateUserRewardWei( user: string, epochOldStyle: number, - chainId: number + chainId: number, ) { - if(epochOldStyle == getCurrentEpoch()){ - // not known yet - return "0"; + if (epochOldStyle == getCurrentEpoch()) { + // not known yet - hence display 0 + return '0'; } - if(chainId !== CHAIN_ID_OPTIMISM) return "0" - // for epochs before Aura rewards started, return 0 - if(epochOldStyle < AURA_REWARDS_START_EPOCH_OLD_STYLE) return "0" + + // Aura rewards only applicable on Optimism + if (chainId !== CHAIN_ID_OPTIMISM) return '0'; + + // for epochs before Aura rewards started, return 0 + if (epochOldStyle < AURA_REWARDS_START_EPOCH_OLD_STYLE) return '0'; const epoch = epochOldStyle - GasRefundV2EpochFlip; const { byAccountLowercase, totalScore } = await fetchPastEpochData(epoch); From 242c956e4eccd72c3a71c7b3ad4b30bf0ac8a76d Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:44:41 +0200 Subject: [PATCH 04/44] chore: avoid then + await mix --- src/lib/gas-refund/gas-refund-api.ts | 94 +++++++++++++--------------- 1 file changed, 44 insertions(+), 50 deletions(-) diff --git a/src/lib/gas-refund/gas-refund-api.ts b/src/lib/gas-refund/gas-refund-api.ts index d7633b9b..f0e82e1b 100644 --- a/src/lib/gas-refund/gas-refund-api.ts +++ b/src/lib/gas-refund/gas-refund-api.ts @@ -191,52 +191,8 @@ export class GasRefundApi { } async _fetchMerkleData(address: string): Promise { - const oldEpochs = Database.sequelize.query( - MERKLE_DATA_SQL_QUERY, - { - type: Sequelize.QueryTypes.SELECT, - replacements: { - address, - chainId: this.network, - }, - }, - ); - - const newEpochs = GasRefundParticipation.findAll({ - raw: true, - where: { - address, - chainId: this.network, - epoch: { - [Sequelize.Op.gte]: EPOCH_WHEN_OPTIMISM_STAKING_ENABLED, - }, - }, - }).then(v => { - return Promise.all( - v.map(async m => { - const auraWei = await getApproximateUserRewardWei( - m.address.toLowerCase(), - m.epoch, - m.chainId, - ); - return { - amountsByProgram: { - aura: auraWei, - paraswapGasRefund: new BigNumber(m.amount) - .minus(auraWei) - .toFixed(), - }, - refundedAmountPSP: m.amount, - epoch: m.epoch, - address: m.address, - merkleProofs: m.merkleProofs, - }; - }), - ); - }); - const grpDataResult: DistData = ( - await Promise.all([oldEpochs, newEpochs]) + await Promise.all([loadOldEpochs(address), loadNewEpochs(address)]) ).flat(); return grpDataResult; @@ -391,11 +347,6 @@ export class GasRefundApi { totalClaimable: totalClaimable.toString(), pendingClaimable: totalPendingRefundAmount.toString(), pendingRefundBreakdownPerEpoch, - // txParams: { - // to: this.merkleRedem.address, - // data, - // chainId: this.network, - // }, }; } @@ -451,3 +402,46 @@ export async function loadLatestDistributedEpoch(): Promise { ); return result[0].latestDistributedEpoch; } + +async function loadOldEpochs(address: string) { + return Database.sequelize.query(MERKLE_DATA_SQL_QUERY, { + type: Sequelize.QueryTypes.SELECT, + replacements: { + address, + chainId: this.network, + }, + }); +} + +async function loadNewEpochs(address: string) { + const newEpochs = await GasRefundParticipation.findAll({ + raw: true, + where: { + address, + chainId: this.network, + epoch: { + [Sequelize.Op.gte]: EPOCH_WHEN_OPTIMISM_STAKING_ENABLED, + }, + }, + }); + + return Promise.all( + newEpochs.map(async m => { + const auraWei = await getApproximateUserRewardWei( + m.address.toLowerCase(), + m.epoch, + m.chainId, + ); + return { + amountsByProgram: { + aura: auraWei, + paraswapGasRefund: new BigNumber(m.amount).minus(auraWei).toFixed(), + }, + refundedAmountPSP: m.amount, + epoch: m.epoch, + address: m.address, + merkleProofs: m.merkleProofs, + }; + }), + ); +} From b22a2ad522a7848123f6fdfa7a83ee75655ef3b0 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:46:54 +0200 Subject: [PATCH 05/44] fix: broken prev commit chore --- src/lib/gas-refund/gas-refund-api.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/lib/gas-refund/gas-refund-api.ts b/src/lib/gas-refund/gas-refund-api.ts index f0e82e1b..57015e86 100644 --- a/src/lib/gas-refund/gas-refund-api.ts +++ b/src/lib/gas-refund/gas-refund-api.ts @@ -192,7 +192,10 @@ export class GasRefundApi { async _fetchMerkleData(address: string): Promise { const grpDataResult: DistData = ( - await Promise.all([loadOldEpochs(address), loadNewEpochs(address)]) + await Promise.all([ + loadOldEpochs(address, this.network), + loadNewEpochs(address, this.network), + ]) ).flat(); return grpDataResult; @@ -403,22 +406,22 @@ export async function loadLatestDistributedEpoch(): Promise { return result[0].latestDistributedEpoch; } -async function loadOldEpochs(address: string) { +async function loadOldEpochs(address: string, chainId: number) { return Database.sequelize.query(MERKLE_DATA_SQL_QUERY, { type: Sequelize.QueryTypes.SELECT, replacements: { address, - chainId: this.network, + chainId, }, }); } -async function loadNewEpochs(address: string) { +async function loadNewEpochs(address: string, chainId: number) { const newEpochs = await GasRefundParticipation.findAll({ raw: true, where: { address, - chainId: this.network, + chainId, epoch: { [Sequelize.Op.gte]: EPOCH_WHEN_OPTIMISM_STAKING_ENABLED, }, From 7dea48f78be00a13768a1f5292f48251dcd82ba6 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 21 Dec 2023 09:43:23 +0200 Subject: [PATCH 06/44] add AmountsByProgram type --- src/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types.ts b/src/types.ts index 7a224027..d03cd668 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ export interface ChainBalanceMapping { [chainId: number]: string; } + +export type AmountsByProgram = Record; From 2a128a993459fb18b0a199d81370d0a3dfb58f80 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 21 Dec 2023 09:43:46 +0200 Subject: [PATCH 07/44] draft: add GasRefundParticipations.amountsByProgram migration (todo) --- migrations/add_GasRefundParticipation_amountsByProgram.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 migrations/add_GasRefundParticipation_amountsByProgram.sql diff --git a/migrations/add_GasRefundParticipation_amountsByProgram.sql b/migrations/add_GasRefundParticipation_amountsByProgram.sql new file mode 100644 index 00000000..796c2543 --- /dev/null +++ b/migrations/add_GasRefundParticipation_amountsByProgram.sql @@ -0,0 +1,2 @@ +alter table "GasRefundParticipations" +add column "amountsByProgram" jsonb; From 09882ed5a3f73f289ccd286e0c0013ff0a645907 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 21 Dec 2023 09:44:38 +0200 Subject: [PATCH 08/44] feat: adjust GasRefundParticipation model, add amountsByProgram field --- src/models/GasRefundParticipation.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/models/GasRefundParticipation.ts b/src/models/GasRefundParticipation.ts index 97bac53b..eae4fc80 100644 --- a/src/models/GasRefundParticipation.ts +++ b/src/models/GasRefundParticipation.ts @@ -14,7 +14,7 @@ import { DataType_ADDRESS, DataType_KECCAK256_HASHED_VALUE, } from '../lib/sql-data-types'; -import { ChainBalanceMapping } from '../types'; +import { AmountsByProgram, ChainBalanceMapping } from '../types'; const compositeIndex = createIndexDecorator({ name: 'epochgasrefund_epoch_address_chain', @@ -51,6 +51,10 @@ export class GasRefundParticipation extends Model { @Column({ type: DataType.JSONB }) GRPChainBreakDown: ChainBalanceMapping; + @AllowNull(true) + @Column({ type: DataType.JSONB }) + amountsByProgram: AmountsByProgram; + @AllowNull(true) @Column(DataType.DECIMAL) amount: string; From 9dca9f11c0095d5b130312f1b65a92976e377b8c Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 21 Dec 2023 09:49:40 +0200 Subject: [PATCH 09/44] feat: prop-drill new `amountsByProgram` in files- and DB-hydrating scripts --- scripts/gas-refund-program/persistance/db-persistance.ts | 3 ++- scripts/gas-refund-program/persistance/file-persistance.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/gas-refund-program/persistance/db-persistance.ts b/scripts/gas-refund-program/persistance/db-persistance.ts index 0b420faa..1aee37a4 100644 --- a/scripts/gas-refund-program/persistance/db-persistance.ts +++ b/scripts/gas-refund-program/persistance/db-persistance.ts @@ -262,8 +262,9 @@ export const saveMerkleTreeInDB = async ({ isCompleted: true, amount: leaf.amount, GRPChainBreakDown: stringifyGRPChainBreakDown( - userGRPChainsBreakDowns[leaf.address], + userGRPChainsBreakDowns[leaf.address].byChain, ), + amountsByProgram: userGRPChainsBreakDowns[leaf.address].amountsByProgram, }), ); diff --git a/scripts/gas-refund-program/persistance/file-persistance.ts b/scripts/gas-refund-program/persistance/file-persistance.ts index 9374cc97..0492f387 100644 --- a/scripts/gas-refund-program/persistance/file-persistance.ts +++ b/scripts/gas-refund-program/persistance/file-persistance.ts @@ -26,6 +26,7 @@ type FileMerkleTreeData = { amount: string; epoch: number; proof: string[]; + amountsByProgram?: Record; }[]; }; @@ -56,8 +57,9 @@ export async function saveMerkleTreeInFile({ ...r, proof: merkleProofs, GRPChainBreakDown: stringifyGRPChainBreakDown( - userGRPChainsBreakDowns[r.address], + userGRPChainsBreakDowns[r.address].byChain, ), + amountsByProgram: userGRPChainsBreakDowns[r.address].amountsByProgram, }; }), }; From badfe9eb2d2de3ddd12a51ce5d08abbf6ddd4369 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 21 Dec 2023 09:50:24 +0200 Subject: [PATCH 10/44] feat: hook `rewardsByProgram` extension into merkle root computing script --- scripts/gas-refund-program/computeMerkleTree.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/scripts/gas-refund-program/computeMerkleTree.ts b/scripts/gas-refund-program/computeMerkleTree.ts index 360a569c..91862d98 100644 --- a/scripts/gas-refund-program/computeMerkleTree.ts +++ b/scripts/gas-refund-program/computeMerkleTree.ts @@ -34,6 +34,7 @@ import { ChainRewardsMapping, } from './types'; import { composeRefundWithPIP38Refunds } from './pip38'; +import { composeWithAmountsByProgram } from '../../src/lib/utils/aura-rewards'; const logger = global.LOGGER('GRP:COMPUTE_MERKLE_TREE'); @@ -101,16 +102,18 @@ export async function computeAndStoreMerkleTree(epoch: number) { .flat() .filter(entry => !entry.amount.eq(0)); - const allChainsRefunds = composeRefundWithPIP38Refunds( - epoch, - _allChainsRefunds, - ); + const withPIP38 = composeRefundWithPIP38Refunds(epoch, _allChainsRefunds); + + const allChainsRefunds = await composeWithAmountsByProgram(epoch, withPIP38); const userGRPChainsBreakDowns = allChainsRefunds.reduce<{ [stakeChainId: number]: AddressRewardsMapping; }>((acc, curr) => { if (!acc[curr.chainId]) acc[curr.chainId] = {}; - acc[curr.chainId][curr.account] = curr.breakDownGRP; + acc[curr.chainId][curr.account] = { + byChain: curr.breakDownGRP, + amountsByProgram: curr.amountsByProgram, + }; return acc; }, {}); From 3667bcd30075500fd824cb4fa2a4ad2646331217 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 21 Dec 2023 09:51:02 +0200 Subject: [PATCH 11/44] add some types AmountsByProgram --- scripts/gas-refund-program/types.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/gas-refund-program/types.ts b/scripts/gas-refund-program/types.ts index bd869349..fb968b05 100644 --- a/scripts/gas-refund-program/types.ts +++ b/scripts/gas-refund-program/types.ts @@ -1,5 +1,6 @@ import { GasRefundTransactionData } from '../../src/lib/gas-refund/gas-refund'; import BigNumber from 'bignumber.js'; +import { AmountsByProgram } from '../../src/types'; export type HistoricalPrice = { [timestamp: string]: number }; @@ -108,8 +109,13 @@ export type AddressRewards = { breakDownGRP: { [GRPChainId: number]: BigNumber }; }; +export type AddressRewardsWithAmountsByProgram = AddressRewards & { + amountsByProgram: AmountsByProgram; +}; + export type AddressRewardsMapping = { [account: string]: { - [grpChainId: number]: BigNumber; + amountsByProgram: AmountsByProgram; + byChain: { [grpChainId: number]: BigNumber }; }; }; From 0ac0c0cde0e82fd9a2d1c1e4d708d322678d74e3 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 21 Dec 2023 09:51:36 +0200 Subject: [PATCH 12/44] chore: typing AmountsByProgram --- src/lib/gas-refund/gas-refund.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/gas-refund/gas-refund.ts b/src/lib/gas-refund/gas-refund.ts index 9b902260..0b2d27b6 100644 --- a/src/lib/gas-refund/gas-refund.ts +++ b/src/lib/gas-refund/gas-refund.ts @@ -8,6 +8,7 @@ import { CHAIN_ID_POLYGON, } from '../constants'; import { isTruthy } from '../utils'; +import { AmountsByProgram } from '../../types'; export const isMainnetStaking = true; // TODO FIXME move to env var @@ -116,6 +117,7 @@ export interface GasRefundParticipantData { merkleProofs: string[]; isCompleted: boolean; GRPChainBreakDown: GRPChainBreakDown; + amountsByProgram: AmountsByProgram; } export enum TransactionStatus { From 3c74ede57837a679e2236cc39933c068e8ee8fd6 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 21 Dec 2023 09:52:27 +0200 Subject: [PATCH 13/44] fix: in controller take data from DB, it's not computed now --- src/lib/gas-refund/gas-refund-api.ts | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/lib/gas-refund/gas-refund-api.ts b/src/lib/gas-refund/gas-refund-api.ts index 57015e86..3704c9aa 100644 --- a/src/lib/gas-refund/gas-refund-api.ts +++ b/src/lib/gas-refund/gas-refund-api.ts @@ -23,8 +23,7 @@ import { } from '../../models/sePSPMigrations'; import { getCurrentEpoch, resolveV2EpochNumber } from './epoch-helpers'; import { grp2CConfigParticularities } from './config'; -import { getApproximateUserRewardWei } from '../utils/aura-rewards'; -import BigNumber from 'bignumber.js'; +import { AmountsByProgram } from '../../types'; interface MerkleRedeem extends Contract { callStatic: { @@ -130,9 +129,9 @@ type MigrationData = hasMigrated: true; } & SePSPMigrationsData); -type DistData = (GasRefundClaim & { - amountsByProgram?: Record; -})[]; +type GasRefundClaimWithAmountsByProgram = GasRefundClaim & { + amountsByProgram?: AmountsByProgram | null; +}; export class GasRefundApi { merkleRedem: MerkleRedeem; merkleRedemSePSP1?: MerkleRedeem; @@ -190,8 +189,10 @@ export class GasRefundApi { }; } - async _fetchMerkleData(address: string): Promise { - const grpDataResult: DistData = ( + async _fetchMerkleData( + address: string, + ): Promise { + const grpDataResult: GasRefundClaimWithAmountsByProgram[] = ( await Promise.all([ loadOldEpochs(address, this.network), loadNewEpochs(address, this.network), @@ -430,16 +431,8 @@ async function loadNewEpochs(address: string, chainId: number) { return Promise.all( newEpochs.map(async m => { - const auraWei = await getApproximateUserRewardWei( - m.address.toLowerCase(), - m.epoch, - m.chainId, - ); return { - amountsByProgram: { - aura: auraWei, - paraswapGasRefund: new BigNumber(m.amount).minus(auraWei).toFixed(), - }, + amountsByProgram: m.amountsByProgram, refundedAmountPSP: m.amount, epoch: m.epoch, address: m.address, From 565376fd54e8a24a2dee6531d42019cd7afe3baa Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 21 Dec 2023 09:53:05 +0200 Subject: [PATCH 14/44] feat: aura rewards distribution without remainder + distribution helpers --- src/lib/utils/aura-rewards.ts | 170 +++++++++++++++++++++++++++++++--- 1 file changed, 157 insertions(+), 13 deletions(-) diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index b2749d62..035c5ab5 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -6,12 +6,16 @@ import { } from './staking-supervisor'; import BigNumber from 'bignumber.js'; import { GasRefundV2EpochFlip } from '../gas-refund/gas-refund'; -import { CHAIN_ID_OPTIMISM } from '../constants'; import { getCurrentEpoch } from '../gas-refund/epoch-helpers'; +import { + AddressRewards, + AddressRewardsWithAmountsByProgram, +} from '../../../scripts/gas-refund-program/types'; +import { CHAIN_ID_OPTIMISM, STAKING_CHAIN_IDS } from '../constants'; const config: Record = { // @TODO: update this config - 39: '1234567890123123123', + 41: '69311354102943179612461', }; const AURA_REWARDS_START_EPOCH_OLD_STYLE = Math.min( @@ -21,7 +25,8 @@ const AURA_REWARDS_START_EPOCH_OLD_STYLE = Math.min( const logger = global.LOGGER('aura-rewards'); async function _fetchEpochData(epoch: number) { - const [totalScore, list] = await Promise.all([ + // @TODO: don't fetch total score at all as it's not accurate + const [_totalScore, list] = await Promise.all([ fetchTotalScore(epoch), fetchAccountsScores(epoch), ]); @@ -38,6 +43,14 @@ async function _fetchEpochData(epoch: number) { `loaded pre-requisites for computing Aura rewards for epoch ${epoch}`, ); + const totalScore = list + .reduce((acc, curr) => { + return acc.plus(curr.score); + }, new BigNumber(0)) + .toFixed(); + + logger.info(`computed total score for epoch ${epoch}: ${totalScore}`); + logger.info(`fetched total score for epoch ${epoch}: ${_totalScore}`); return { totalScore, list, @@ -54,26 +67,157 @@ const fetchPastEpochData = pMemoize(_fetchEpochData, { export async function getApproximateUserRewardWei( user: string, epochOldStyle: number, - chainId: number, + counter: RewardsDistributionCounter, ) { if (epochOldStyle == getCurrentEpoch()) { // not known yet - hence display 0 return '0'; } - // Aura rewards only applicable on Optimism - if (chainId !== CHAIN_ID_OPTIMISM) return '0'; - // for epochs before Aura rewards started, return 0 if (epochOldStyle < AURA_REWARDS_START_EPOCH_OLD_STYLE) return '0'; const epoch = epochOldStyle - GasRefundV2EpochFlip; - const { byAccountLowercase, totalScore } = await fetchPastEpochData(epoch); + const { byAccountLowercase, totalScore, list } = await fetchPastEpochData( + epoch, + ); + // debugger; + + const userScore = byAccountLowercase[user.toLowerCase()]?.score || '0'; + + const totalRewards = config[epochOldStyle] || '0'; + const remainingRewards = new BigNumber(totalRewards) + .minus(counter.rewardsAllocated.toString()) + .toFixed(); + const remainingTotalScore = new BigNumber(totalScore) + .minus(counter.scoreCleared.toString()) + .toFixed(); + const userRewards = new BigNumber(userScore) + .times(remainingRewards) + .div(remainingTotalScore) + .toFixed(0, BigNumber.ROUND_HALF_FLOOR); + + counter.count(BigInt(userScore), BigInt(userRewards)); + + return userRewards; +} + +// @TODO: sort out cross-importing between scripts/ and src/ +export async function composeWithAmountsByProgram( + epoch: number, + data: AddressRewards[], +): Promise { + // split optimism and non-optimism refunds + const { optimismRefunds, nonOptimismRefunds } = data.reduce<{ + optimismRefunds: AddressRewards[]; + nonOptimismRefunds: AddressRewards[]; + }>( + (acc, curr) => { + if (curr.chainId === CHAIN_ID_OPTIMISM) { + acc.optimismRefunds.push(curr); + } else { + acc.nonOptimismRefunds.push(curr); + } + return acc; + }, + { + optimismRefunds: [] as AddressRewards[], + nonOptimismRefunds: [] as AddressRewards[], + }, + ); + + const optimismStakers = new Set(optimismRefunds.map(v => v.account)); + const { byAccountLowercase } = await fetchPastEpochData( + epoch - GasRefundV2EpochFlip, + ); + // prepare list of stakers that don't have refund on optimism + const nonOptimismStakers = new Set( + Object.keys(byAccountLowercase) + // .map(v => v.account) + .filter(account => !optimismStakers.has(account)), + ); + + const rewardsDistributionCounter = constructRewardsDistributionCounter(); + // compute resulting array by adjusting optimism refunds + creating optimism refunds for those who don't have it + const adjustedOptimismRefunds: Promise = + Promise.all( + optimismRefunds.map(async v => { + const aura = await getApproximateUserRewardWei( + v.account, + epoch, + rewardsDistributionCounter, + ); + return { + ...v, + amount: new BigNumber(v.amount).plus(aura), // add aura rewards to gas refunds json + amountsByProgram: { + aura, + paraswapGasRefund: v.amount.toFixed(), + }, + }; + }), + ); + + const additionalOptimismRefunds: Promise< + AddressRewardsWithAmountsByProgram[] + > = Promise.all( + Array.from(nonOptimismStakers).map< + Promise + >(async account => { + const aura = await getApproximateUserRewardWei( + account, + epoch, + rewardsDistributionCounter, + ); + return { + account, + amount: new BigNumber(aura), + chainId: CHAIN_ID_OPTIMISM, + amountsByProgram: { + aura, + paraswapGasRefund: '0', // the value is chain specific, relates to the `amount` field, not to total amount accross all networks + }, + breakDownGRP: STAKING_CHAIN_IDS.reduce>( + (acc, curr) => { + acc[curr] = new BigNumber(0); + return acc; + }, + {}, + ), + }; + }), + ); - const userScore = byAccountLowercase[user.toLowerCase()].score || '0'; + const nonOptimismRefundsWithAmountsByProgram: AddressRewardsWithAmountsByProgram[] = + nonOptimismRefunds.map(v => ({ + ...v, + amountsByProgram: { + aura: '0', + paraswapGasRefund: v.amount.toFixed(), + }, + })); - return new BigNumber(userScore) - .times(config[epochOldStyle] || '0') - .div(totalScore) - .toFixed(0); + const newAllRefunds = ( + await Promise.all([adjustedOptimismRefunds, additionalOptimismRefunds]) + ) + .flat() + .concat(nonOptimismRefundsWithAmountsByProgram); + + return newAllRefunds; +} + +type RewardsDistributionCounter = { + scoreCleared: BigInt; + rewardsAllocated: BigInt; + count: (userScore: BigInt, userRewards: BigInt) => void; +}; +function constructRewardsDistributionCounter(): RewardsDistributionCounter { + return { + scoreCleared: BigInt(0), + rewardsAllocated: BigInt(0), + count(userScore: BigInt, userRewards: BigInt) { + this.scoreCleared += userScore; + this.rewardsAllocated += userRewards; + }, + }; } From 1a4a643663116b2f3b2dcb9aee7d9597196192c6 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 21 Dec 2023 10:01:39 +0200 Subject: [PATCH 15/44] chore: better sorting of fields in the output --- src/lib/gas-refund/gas-refund-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/gas-refund/gas-refund-api.ts b/src/lib/gas-refund/gas-refund-api.ts index 3704c9aa..067c7a87 100644 --- a/src/lib/gas-refund/gas-refund-api.ts +++ b/src/lib/gas-refund/gas-refund-api.ts @@ -431,12 +431,12 @@ async function loadNewEpochs(address: string, chainId: number) { return Promise.all( newEpochs.map(async m => { - return { - amountsByProgram: m.amountsByProgram, + return { refundedAmountPSP: m.amount, epoch: m.epoch, address: m.address, merkleProofs: m.merkleProofs, + amountsByProgram: m.amountsByProgram, }; }), ); From 0dd09b39d7d89d33c0294241d72dae470d72afdd Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 21 Dec 2023 10:08:42 +0200 Subject: [PATCH 16/44] chore: actualize some naming and annotations; cleanup --- src/lib/utils/aura-rewards.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index 035c5ab5..eb73f993 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -63,8 +63,9 @@ const fetchPastEpochData = pMemoize(_fetchEpochData, { cacheKey: ([epoch]) => `paraboost_epochData_${epoch}`, }); -// approximate -> because there's no integrity check (sum of parts = whole), just simple pro rata calc -export async function getApproximateUserRewardWei( +// is used during when executing distribution script. +// is intended to compute user rewards for the current epoch without remainder +export async function computeUserRewardWei( user: string, epochOldStyle: number, counter: RewardsDistributionCounter, @@ -78,10 +79,9 @@ export async function getApproximateUserRewardWei( if (epochOldStyle < AURA_REWARDS_START_EPOCH_OLD_STYLE) return '0'; const epoch = epochOldStyle - GasRefundV2EpochFlip; - const { byAccountLowercase, totalScore, list } = await fetchPastEpochData( + const { byAccountLowercase, totalScore } = await fetchPastEpochData( epoch, - ); - // debugger; + ); const userScore = byAccountLowercase[user.toLowerCase()]?.score || '0'; @@ -97,6 +97,7 @@ export async function getApproximateUserRewardWei( .div(remainingTotalScore) .toFixed(0, BigNumber.ROUND_HALF_FLOOR); + // deduct from remaining rewards and scores - to guarantee that there is no remainder or excession counter.count(BigInt(userScore), BigInt(userRewards)); return userRewards; @@ -142,7 +143,7 @@ export async function composeWithAmountsByProgram( const adjustedOptimismRefunds: Promise = Promise.all( optimismRefunds.map(async v => { - const aura = await getApproximateUserRewardWei( + const aura = await computeUserRewardWei( v.account, epoch, rewardsDistributionCounter, @@ -164,7 +165,7 @@ export async function composeWithAmountsByProgram( Array.from(nonOptimismStakers).map< Promise >(async account => { - const aura = await getApproximateUserRewardWei( + const aura = await computeUserRewardWei( account, epoch, rewardsDistributionCounter, From c4d8a3e570c93742ab0a597ad956a61f7fe9d6b1 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 21 Dec 2023 10:13:08 +0200 Subject: [PATCH 17/44] chore: avoid redundant fetch; cleanup; kick `fetchTotalScore` --- src/lib/gas-refund/gas-refund-api.ts | 2 +- src/lib/utils/aura-rewards.ts | 19 ++++--------------- src/lib/utils/staking-supervisor.ts | 7 ------- 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/lib/gas-refund/gas-refund-api.ts b/src/lib/gas-refund/gas-refund-api.ts index 067c7a87..6e66b616 100644 --- a/src/lib/gas-refund/gas-refund-api.ts +++ b/src/lib/gas-refund/gas-refund-api.ts @@ -431,7 +431,7 @@ async function loadNewEpochs(address: string, chainId: number) { return Promise.all( newEpochs.map(async m => { - return { + return { refundedAmountPSP: m.amount, epoch: m.epoch, address: m.address, diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index eb73f993..be0b2ed4 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -1,9 +1,5 @@ import * as pMemoize from 'p-memoize'; -import { - MinParaBoostData, - fetchAccountsScores, - fetchTotalScore, -} from './staking-supervisor'; +import { MinParaBoostData, fetchAccountsScores } from './staking-supervisor'; import BigNumber from 'bignumber.js'; import { GasRefundV2EpochFlip } from '../gas-refund/gas-refund'; import { getCurrentEpoch } from '../gas-refund/epoch-helpers'; @@ -25,11 +21,7 @@ const AURA_REWARDS_START_EPOCH_OLD_STYLE = Math.min( const logger = global.LOGGER('aura-rewards'); async function _fetchEpochData(epoch: number) { - // @TODO: don't fetch total score at all as it's not accurate - const [_totalScore, list] = await Promise.all([ - fetchTotalScore(epoch), - fetchAccountsScores(epoch), - ]); + const list = await fetchAccountsScores(epoch); const byAccountLowercase = list.reduce>( (acc, curr) => { @@ -43,14 +35,13 @@ async function _fetchEpochData(epoch: number) { `loaded pre-requisites for computing Aura rewards for epoch ${epoch}`, ); + // computing total with BigNumber instead of taking it from the endpoint const totalScore = list .reduce((acc, curr) => { return acc.plus(curr.score); }, new BigNumber(0)) .toFixed(); - logger.info(`computed total score for epoch ${epoch}: ${totalScore}`); - logger.info(`fetched total score for epoch ${epoch}: ${_totalScore}`); return { totalScore, list, @@ -79,9 +70,7 @@ export async function computeUserRewardWei( if (epochOldStyle < AURA_REWARDS_START_EPOCH_OLD_STYLE) return '0'; const epoch = epochOldStyle - GasRefundV2EpochFlip; - const { byAccountLowercase, totalScore } = await fetchPastEpochData( - epoch, - ); + const { byAccountLowercase, totalScore } = await fetchPastEpochData(epoch); const userScore = byAccountLowercase[user.toLowerCase()]?.score || '0'; diff --git a/src/lib/utils/staking-supervisor.ts b/src/lib/utils/staking-supervisor.ts index bfe02296..cb9f1b48 100644 --- a/src/lib/utils/staking-supervisor.ts +++ b/src/lib/utils/staking-supervisor.ts @@ -23,10 +23,3 @@ export async function fetchAccountsScores( ); return data; } - -export async function fetchTotalScore(epoch: number): Promise { - const { data } = await axios.get( - `https://api.paraswap.io/stk/paraboost/totalScore?epoch=${epoch}`, - ); - return data; -} From c1f61762b61baaa777934c65f2e84b775873bcac Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 21 Dec 2023 10:17:44 +0200 Subject: [PATCH 18/44] chore: more some types to avoid shat /src depends on /scripts --- scripts/gas-refund-program/computeMerkleTree.ts | 5 +++-- scripts/gas-refund-program/pip38/index.ts | 2 +- scripts/gas-refund-program/types.ts | 11 ----------- src/lib/utils/aura-rewards.ts | 4 ++-- src/types.ts | 13 +++++++++++++ 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/scripts/gas-refund-program/computeMerkleTree.ts b/scripts/gas-refund-program/computeMerkleTree.ts index 91862d98..6802bbd4 100644 --- a/scripts/gas-refund-program/computeMerkleTree.ts +++ b/scripts/gas-refund-program/computeMerkleTree.ts @@ -29,12 +29,12 @@ import BigNumber from 'bignumber.js'; import { isTruthy } from '../../src/lib/utils'; import { AddressChainRewardsMapping, - AddressRewards, AddressRewardsMapping, ChainRewardsMapping, } from './types'; import { composeRefundWithPIP38Refunds } from './pip38'; import { composeWithAmountsByProgram } from '../../src/lib/utils/aura-rewards'; +import { AddressRewards } from '../../src/types'; const logger = global.LOGGER('GRP:COMPUTE_MERKLE_TREE'); @@ -305,12 +305,13 @@ async function startComputingMerkleTreesAllChains() { // await computeAndStoreMerkleTree(epoch); // } - await computeAndStoreMerkleTree(38); + await computeAndStoreMerkleTree(41); } startComputingMerkleTreesAllChains() .then(() => process.exit(0)) .catch(err => { + debugger; logger.error('computeMerkleTreesAllChains exited with error:', err); process.exit(1); }); diff --git a/scripts/gas-refund-program/pip38/index.ts b/scripts/gas-refund-program/pip38/index.ts index 74ff5b26..7d01678e 100644 --- a/scripts/gas-refund-program/pip38/index.ts +++ b/scripts/gas-refund-program/pip38/index.ts @@ -1,7 +1,7 @@ import { assert } from 'ts-essentials'; import * as fantomEpoch36 from './merkletree-chain-250-epoch-36.json'; import * as fantomEpoch37 from './merkletree-chain-250-epoch-37.json'; -import { AddressRewards } from '../types'; +import { AddressRewards } from '../../../src/types'; import { groupBy } from 'lodash'; import BigNumber from 'bignumber.js'; import { CHAIN_ID_FANTOM, CHAIN_ID_MAINNET } from '../../../src/lib/constants'; diff --git a/scripts/gas-refund-program/types.ts b/scripts/gas-refund-program/types.ts index fb968b05..e6c05c3b 100644 --- a/scripts/gas-refund-program/types.ts +++ b/scripts/gas-refund-program/types.ts @@ -102,17 +102,6 @@ export type ChainRewardsMapping = { }; }; -export type AddressRewards = { - account: string; - amount: BigNumber; - chainId: number; - breakDownGRP: { [GRPChainId: number]: BigNumber }; -}; - -export type AddressRewardsWithAmountsByProgram = AddressRewards & { - amountsByProgram: AmountsByProgram; -}; - export type AddressRewardsMapping = { [account: string]: { amountsByProgram: AmountsByProgram; diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index be0b2ed4..f88600bb 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -3,11 +3,11 @@ import { MinParaBoostData, fetchAccountsScores } from './staking-supervisor'; import BigNumber from 'bignumber.js'; import { GasRefundV2EpochFlip } from '../gas-refund/gas-refund'; import { getCurrentEpoch } from '../gas-refund/epoch-helpers'; +import { CHAIN_ID_OPTIMISM, STAKING_CHAIN_IDS } from '../constants'; import { AddressRewards, AddressRewardsWithAmountsByProgram, -} from '../../../scripts/gas-refund-program/types'; -import { CHAIN_ID_OPTIMISM, STAKING_CHAIN_IDS } from '../constants'; +} from '../../types'; const config: Record = { // @TODO: update this config diff --git a/src/types.ts b/src/types.ts index d03cd668..bc7d4a1d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,18 @@ +import type { BigNumber } from 'bignumber.js'; + export interface ChainBalanceMapping { [chainId: number]: string; } export type AmountsByProgram = Record; + +export type AddressRewards = { + account: string; + amount: BigNumber; + chainId: number; + breakDownGRP: { [GRPChainId: number]: BigNumber }; +}; + +export type AddressRewardsWithAmountsByProgram = AddressRewards & { + amountsByProgram: AmountsByProgram; +}; From a75f9803f01ef017c1035997afab98ce402ab49e Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 21 Dec 2023 10:25:30 +0200 Subject: [PATCH 19/44] chore: cleanup outdated annotation --- src/lib/utils/aura-rewards.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index f88600bb..e415ecf7 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -92,7 +92,6 @@ export async function computeUserRewardWei( return userRewards; } -// @TODO: sort out cross-importing between scripts/ and src/ export async function composeWithAmountsByProgram( epoch: number, data: AddressRewards[], From e3df49e6ee5e32ea2690a92d5bc0c5e6f7c74a2e Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 21 Dec 2023 10:25:43 +0200 Subject: [PATCH 20/44] chore: cleanup --- scripts/gas-refund-program/computeMerkleTree.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/gas-refund-program/computeMerkleTree.ts b/scripts/gas-refund-program/computeMerkleTree.ts index 6802bbd4..473226c8 100644 --- a/scripts/gas-refund-program/computeMerkleTree.ts +++ b/scripts/gas-refund-program/computeMerkleTree.ts @@ -310,8 +310,7 @@ async function startComputingMerkleTreesAllChains() { startComputingMerkleTreesAllChains() .then(() => process.exit(0)) - .catch(err => { - debugger; + .catch(err => { logger.error('computeMerkleTreesAllChains exited with error:', err); process.exit(1); }); From 2404c35e4d5506e10117dbafce89d8c988305a67 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Tue, 26 Dec 2023 15:19:08 +0200 Subject: [PATCH 21/44] chore: add couple integrity checks and improve some naming --- src/lib/utils/aura-rewards.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index e415ecf7..789bf8e2 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -8,6 +8,7 @@ import { AddressRewards, AddressRewardsWithAmountsByProgram, } from '../../types'; +import { assert } from 'ts-essentials'; const config: Record = { // @TODO: update this config @@ -115,15 +116,17 @@ export async function composeWithAmountsByProgram( }, ); - const optimismStakers = new Set(optimismRefunds.map(v => v.account)); - const { byAccountLowercase } = await fetchPastEpochData( + const optimismRefundEligibleStakers = new Set( + optimismRefunds.map(v => v.account), + ); + const { byAccountLowercase, totalScore, list } = await fetchPastEpochData( epoch - GasRefundV2EpochFlip, ); // prepare list of stakers that don't have refund on optimism - const nonOptimismStakers = new Set( + const stakersNotEligibleForOptimismRefund = new Set( Object.keys(byAccountLowercase) // .map(v => v.account) - .filter(account => !optimismStakers.has(account)), + .filter(account => !optimismRefundEligibleStakers.has(account)), ); const rewardsDistributionCounter = constructRewardsDistributionCounter(); @@ -150,7 +153,7 @@ export async function composeWithAmountsByProgram( const additionalOptimismRefunds: Promise< AddressRewardsWithAmountsByProgram[] > = Promise.all( - Array.from(nonOptimismStakers).map< + Array.from(stakersNotEligibleForOptimismRefund).map< Promise >(async account => { const aura = await computeUserRewardWei( @@ -192,6 +195,15 @@ export async function composeWithAmountsByProgram( .flat() .concat(nonOptimismRefundsWithAmountsByProgram); + assert( + rewardsDistributionCounter.rewardsAllocated == BigInt(config[epoch]), + 'rewards distribution counter does not match the total rewards', + ); + assert( + rewardsDistributionCounter.scoreCleared === BigInt(totalScore), + 'rewards distribution counter does not match the total score', + ); + return newAllRefunds; } From 0f416daaa5748e144ef650e1b7eac87e5b62ec4e Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Tue, 26 Dec 2023 15:20:56 +0200 Subject: [PATCH 22/44] chore: shoot redundant param --- src/lib/utils/aura-rewards.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index 789bf8e2..9908dee7 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -85,7 +85,7 @@ export async function computeUserRewardWei( const userRewards = new BigNumber(userScore) .times(remainingRewards) .div(remainingTotalScore) - .toFixed(0, BigNumber.ROUND_HALF_FLOOR); + .toFixed(0); // deduct from remaining rewards and scores - to guarantee that there is no remainder or excession counter.count(BigInt(userScore), BigInt(userRewards)); From 8ec581674d56e64eacef9236b37c25a2a63b129a Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Tue, 26 Dec 2023 15:29:42 +0200 Subject: [PATCH 23/44] chore: remove unneeded type cast --- src/lib/utils/aura-rewards.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index 9908dee7..fc15896e 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -111,8 +111,8 @@ export async function composeWithAmountsByProgram( return acc; }, { - optimismRefunds: [] as AddressRewards[], - nonOptimismRefunds: [] as AddressRewards[], + optimismRefunds: [], + nonOptimismRefunds: [], }, ); From 09315d7cadc2124e1ae9765f8fd8cfdcdd529397 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Tue, 26 Dec 2023 15:32:19 +0200 Subject: [PATCH 24/44] chore: shoot unused var --- src/lib/utils/aura-rewards.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index fc15896e..702065c2 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -119,7 +119,7 @@ export async function composeWithAmountsByProgram( const optimismRefundEligibleStakers = new Set( optimismRefunds.map(v => v.account), ); - const { byAccountLowercase, totalScore, list } = await fetchPastEpochData( + const { byAccountLowercase, totalScore } = await fetchPastEpochData( epoch - GasRefundV2EpochFlip, ); // prepare list of stakers that don't have refund on optimism From 7cc021502b03a8e52e043d517bed2a3559538ae8 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:01:42 +0200 Subject: [PATCH 25/44] lint fix --- scripts/gas-refund-program/computeMerkleTree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/gas-refund-program/computeMerkleTree.ts b/scripts/gas-refund-program/computeMerkleTree.ts index 473226c8..c97f926d 100644 --- a/scripts/gas-refund-program/computeMerkleTree.ts +++ b/scripts/gas-refund-program/computeMerkleTree.ts @@ -310,7 +310,7 @@ async function startComputingMerkleTreesAllChains() { startComputingMerkleTreesAllChains() .then(() => process.exit(0)) - .catch(err => { + .catch(err => { logger.error('computeMerkleTreesAllChains exited with error:', err); process.exit(1); }); From d6ca1a5d855efe39624144f947577c867fcf14a6 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Fri, 12 Jan 2024 07:59:00 +0200 Subject: [PATCH 26/44] chore: cleanup --- .../lib/computeDistributionMerkleData.ts | 3 --- .../distribution/lib/merkle-tree.ts | 14 +++++--------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts b/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts index 97334096..6c1675ae 100644 --- a/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts +++ b/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts @@ -232,11 +232,9 @@ export async function computeDistributionMerkleData( return acc; }, {}); - // debugger; const merkleTreeData = await computeMerkleData({ userRewards: allChainsRefunds, epoch, - userGRPChainsBreakDowns, }); // TODO ADD MORE SANITY CHECK @@ -252,6 +250,5 @@ export async function computeDistributionMerkleData( } }); }); - debugger; return merkleTreeData; } diff --git a/scripts/gas-refund-program/distribution/lib/merkle-tree.ts b/scripts/gas-refund-program/distribution/lib/merkle-tree.ts index 539f5e0e..1ac51f70 100644 --- a/scripts/gas-refund-program/distribution/lib/merkle-tree.ts +++ b/scripts/gas-refund-program/distribution/lib/merkle-tree.ts @@ -8,6 +8,7 @@ import { MerkleTree } from 'merkletreejs'; import { GasRefundTransaction } from '../../../../src/models/GasRefundTransaction'; import BigNumber from 'bignumber.js'; import { MerkleTreeAndChain } from './types'; +import { assert } from 'ts-essentials'; export type MinGasRefundTransaction = Pick< GasRefundTransaction, @@ -17,7 +18,6 @@ export type MinGasRefundTransaction = Pick< export async function computeMerkleData({ epoch, userRewards, - userGRPChainsBreakDowns, }: { epoch: number; userRewards: { @@ -25,9 +25,6 @@ export async function computeMerkleData({ amount: BigNumber; chainId: number; }[]; - userGRPChainsBreakDowns: { - [stakeChainId: number]: AddressRewardsMapping; - }; }): Promise { const userRefundsByChain = userRewards.reduce<{ [chainId: number]: { @@ -37,11 +34,10 @@ export async function computeMerkleData({ }[]; }>((acc, curr) => { if (!acc[curr.chainId]) acc[curr.chainId] = []; - if (curr.amount.isLessThan(0)) { - throw new Error( - `Negative amount for ${curr.account} on chain ${curr.chainId}`, - ); - } + assert( + curr.amount.isGreaterThanOrEqualTo(0), + `Negative amount for ${curr.account} on chain ${curr.chainId}`, + ); if (!curr.amount.isZero()) acc[curr.chainId].push(curr); return acc; From 729fab52a2d6b5c385d23e11da37a164dfb2e909 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Fri, 12 Jan 2024 08:09:00 +0200 Subject: [PATCH 27/44] feat: support migrations and add amountsByProgram field --- .sequelizerc | 10 +++++ package.json | 1 + sequelize/database.js | 39 +++++++++++++++++++ .../20240112060205-amount-by-program.js | 16 ++++++++ 4 files changed, 66 insertions(+) create mode 100644 .sequelizerc create mode 100644 sequelize/database.js create mode 100644 sequelize/migrations/20240112060205-amount-by-program.js diff --git a/.sequelizerc b/.sequelizerc new file mode 100644 index 00000000..87a5d4de --- /dev/null +++ b/.sequelizerc @@ -0,0 +1,10 @@ +// .sequelizerc + +const path = require('path'); + +module.exports = { + config: path.resolve('sequelize', 'database.js'), + // 'models-path': path.resolve('db', 'models'), + // 'seeders-path': path.resolve('db', 'seeders'), + 'migrations-path': path.resolve('sequelize', 'migrations'), +}; diff --git a/package.json b/package.json index 0004d293..c1099550 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "author": "Paraswap ", "private": true, "scripts": { + "migrate": "npx sequelize-cli db:migrate", "dev": "NODE_ENV=development nodemon", "build": "NODE_ENV=production tsc", "build:grp": "NODE_ENV=production tsc --project scripts/gas-refund-program/tsconfig.json", diff --git a/sequelize/database.js b/sequelize/database.js new file mode 100644 index 00000000..1aadb298 --- /dev/null +++ b/sequelize/database.js @@ -0,0 +1,39 @@ +const DATABASE_URL = + process.env.DATABASE_URL || + 'postgres://paraswap:paraswap@127.0.0.1:32780/volume_tracker'; + +function parseDatabaseURI(uri) { + // Use the URL constructor to parse the URI + const parsedURI = new URL(uri); + + // Extract the relevant parts + const protocol = parsedURI.protocol; // "postgres:" + const username = parsedURI.username; // "paraswap" + const password = parsedURI.password; // "paraswap" + const hostname = parsedURI.hostname; // "127.0.0.1" + const port = parsedURI.port; // "32780" + const database = parsedURI.pathname.slice(1); // "volume_tracker" (remove the leading "/") + + // Return an object with the extracted details + return { + protocol, + username, + password, + hostname, + port, + database, + }; +} + +const parsed = parseDatabaseURI(DATABASE_URL); + +module.exports = { + development: { + username: parsed.username, + password: parsed.password, + database: parsed.database, + host: parsed.hostname, + port: parsed.port, + dialect: 'postgres', + }, +}; diff --git a/sequelize/migrations/20240112060205-amount-by-program.js b/sequelize/migrations/20240112060205-amount-by-program.js new file mode 100644 index 00000000..ee5a1e03 --- /dev/null +++ b/sequelize/migrations/20240112060205-amount-by-program.js @@ -0,0 +1,16 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + return queryInterface.addColumn( + 'GasRefundParticipations', + 'amountsByProgram', + Sequelize.JSONB, + ); + }, + + async down(queryInterface) { + queryInterface.removeColumn('GasRefundParticipations', 'amountsByProgram'); + }, +}; From 8202c0c45c5e3cf16382c172d9dfabb03608187f Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Fri, 12 Jan 2024 08:14:02 +0200 Subject: [PATCH 28/44] fix: store amountsByProgram in DB --- .../distribution/lib/storeDistributionDataInDB.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts b/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts index 4a30f5c2..2f344067 100644 --- a/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts +++ b/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts @@ -36,6 +36,7 @@ export async function storeDistributionDataInDB( proof: merkleProofs, amount, GRPChainBreakDown, + amountsByProgram, } = leaf; assert( account == account.toLowerCase(), @@ -49,7 +50,7 @@ export async function storeDistributionDataInDB( isCompleted: true, amount, GRPChainBreakDown, - amountsByProgram: {}, + amountsByProgram, }; }, ); From 7d3f1c4f378713cce46a68fa0ca545dcdee24113 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Wed, 8 May 2024 19:40:16 +0300 Subject: [PATCH 29/44] chore: tools: add migrations commands to package.json --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 677751c7..cdb4bb1d 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "gas-refund:prod:compute-gas-refund-save-db": "node scripts/gas-refund-program/computeGasRefund.js", "gas-refund:computeDistributionDataAndPersistDB": "patch-package && NODE_ENV=development ts-node scripts/gas-refund-program/distribution/computeDistributionDataAndPersistDB", "gas-refund:computeDistributionFilesAndPersistIPFS": "patch-package && NODE_ENV=development ts-node scripts/gas-refund-program/distribution/computeDistributionFilesAndPersistIPFS", + "migrate:up": "source .env && DATABASE_URL=$DATABASE_URL npx sequelize-cli db:migrate # <- executes any new migrations that are not in sequalize meta table yet, sorted alphabetically", + "migrate:undo": "source .env && DATABASE_URL=$DATABASE_URL npx sequelize-cli db:migrate:undo # <- undoes the last migration from sequalize meta table, sorted alphabetically", "test": "jest" }, "husky": { From cc096921f9af037139e6a777fda809efe8e9125f Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Wed, 8 May 2024 19:40:35 +0300 Subject: [PATCH 30/44] chore: clearer exception handling --- src/lib/utils/aura-rewards.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index 4efd78df..b18dadb9 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -12,7 +12,7 @@ import { assert } from 'ts-essentials'; const config: Record = { // @TODO: update this config - 42: '0', + 46: '0', }; const AURA_REWARDS_START_EPOCH_OLD_STYLE = Math.min( @@ -70,6 +70,11 @@ export async function computeUserRewardWei( // for epochs before Aura rewards started, return 0 if (epochOldStyle < AURA_REWARDS_START_EPOCH_OLD_STYLE) return '0'; + assert( + config[epochOldStyle], + `config for epoch [${epochOldStyle}] is not defined`, + ); + const epoch = epochOldStyle - GasRefundV2EpochFlip; const { byAccountLowercase, totalScore } = await fetchPastEpochData(epoch); From c63b470b00114bb7cb372624f63ce4a3ef3c35de Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 9 May 2024 14:25:42 +0300 Subject: [PATCH 31/44] chore: cleanup "manual" migration as there's already an semi-automatic one --- migrations/add_GasRefundParticipation_amountsByProgram.sql | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 migrations/add_GasRefundParticipation_amountsByProgram.sql diff --git a/migrations/add_GasRefundParticipation_amountsByProgram.sql b/migrations/add_GasRefundParticipation_amountsByProgram.sql deleted file mode 100644 index 796c2543..00000000 --- a/migrations/add_GasRefundParticipation_amountsByProgram.sql +++ /dev/null @@ -1,2 +0,0 @@ -alter table "GasRefundParticipations" -add column "amountsByProgram" jsonb; From 9a5e9383ad67bbf0de2372fffe3e15fad479d8cb Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Thu, 9 May 2024 14:26:53 +0300 Subject: [PATCH 32/44] chore: cleanup redundant command --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index cdb4bb1d..975c6eb0 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "author": "Paraswap ", "private": true, "scripts": { - "migrate": "npx sequelize-cli db:migrate", "dev": "NODE_ENV=development nodemon", "build": "NODE_ENV=production tsc", "build:grp": "NODE_ENV=production tsc --project scripts/gas-refund-program/tsconfig.json", From 6608868e68622dab67c4e2b2e28fe0b6052745b7 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Fri, 17 May 2024 04:54:12 +0000 Subject: [PATCH 33/44] cleanup --- scripts/gas-refund-program/types.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/gas-refund-program/types.ts b/scripts/gas-refund-program/types.ts index f94780b7..ba4fe083 100644 --- a/scripts/gas-refund-program/types.ts +++ b/scripts/gas-refund-program/types.ts @@ -1,6 +1,3 @@ -import BigNumber from 'bignumber.js'; -import { AmountsByProgram } from '../../src/types'; - export type HistoricalPrice = { [timestamp: string]: number }; export type StakedPSPByAddress = { From c61374132b636e87e69868750401d35763b0e012 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Fri, 17 May 2024 05:31:56 +0000 Subject: [PATCH 34/44] change: aura distribution logic (distr on 2 networks among sePSP2 stakers), interim state (draft) --- src/lib/utils/aura-rewards.ts | 103 +++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 32 deletions(-) diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index b18dadb9..478ce094 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -12,7 +12,7 @@ import { assert } from 'ts-essentials'; const config: Record = { // @TODO: update this config - 46: '0', + 47: new BigNumber(1e18).multipliedBy(1000_000).toFixed(), }; const AURA_REWARDS_START_EPOCH_OLD_STYLE = Math.min( @@ -21,32 +21,50 @@ const AURA_REWARDS_START_EPOCH_OLD_STYLE = Math.min( const logger = global.LOGGER('aura-rewards'); +// TotalSupplySePSP2 = TotalSupply(SePSP2[ethereum]) + TotalSupply(SePSP2[optimism]) +async function fetchTotalSupplySePSP2(epoch: number): Promise { + throw new Error('TODO: Function not implemented.'); + return '123'; +} + +// TODO: fetch all holders from covalent? +// AllSePSP2Holders = Unique( Holders(SePSP2[ethereum]) + Holders(SePSP2[optimism]) ) +async function fetchSePSP2BalancesByUserByChain( + epoch: number, +): Promise>> { + throw new Error('TODO: Function not implemented.'); + return {}; +} async function _fetchEpochData(epoch: number) { const list = await fetchAccountsScores(epoch); - const byAccountLowercase = list.reduce>( - (acc, curr) => { + const sePSP2StakersByAccountLowercase = list.reduce< + Record + >((acc, curr) => { + if (Number(curr.sePSP2UnderlyingPSPBalance) > 0) { acc[curr.account.toLowerCase()] = curr; - return acc; - }, - {}, + } + return acc; + }, {}); + + const sePSP2BalancesByUserByChain = await fetchSePSP2BalancesByUserByChain( + epoch, + ); + + assertSePSP2BalancesIntegrity( + sePSP2StakersByAccountLowercase, + sePSP2BalancesByUserByChain, ); logger.info( `loaded pre-requisites for computing Aura rewards for epoch ${epoch}`, ); - // computing total with BigNumber instead of taking it from the endpoint - const totalScore = list - .reduce((acc, curr) => { - return acc.plus(curr.score); - }, new BigNumber(0)) - .toFixed(); + const totalSupplySePSP2 = await fetchTotalSupplySePSP2(epoch); return { - totalScore, - list, - byAccountLowercase, + totalSupplySePSP2, + sePSP2BalancesByUserByChain, }; } @@ -76,24 +94,30 @@ export async function computeUserRewardWei( ); const epoch = epochOldStyle - GasRefundV2EpochFlip; - const { byAccountLowercase, totalScore } = await fetchPastEpochData(epoch); + const { sePSP2BalancesByUserByChain, totalSupplySePSP2 } = + await fetchPastEpochData(epoch); - const userScore = byAccountLowercase[user.toLowerCase()]?.score || '0'; + // after going from byAccountLowercase to sePSP2BalancesByUserByChain here, it relies on sePSP2 balances rather than scores. + const userSePSP2Balance: { + 1: string; + 10: string; + combined: string; + } = sePSP2BalancesByUserByChain[user.toLowerCase()]; const totalRewards = config[epochOldStyle] || '0'; const remainingRewards = new BigNumber(totalRewards) .minus(counter.rewardsAllocated.toString()) .toFixed(); - const remainingTotalScore = new BigNumber(totalScore) - .minus(counter.scoreCleared.toString()) + const remainingTotalScore = new BigNumber(totalSupplySePSP2) + .minus(counter.sePSP2BalanceCleared.toString()) .toFixed(); - const userRewards = new BigNumber(userScore) + const userRewards = new BigNumber(userSePSP2Balance.combined) .times(remainingRewards) .div(remainingTotalScore) .toFixed(0); // deduct from remaining rewards and scores - to guarantee that there is no remainder or excession - counter.count(BigInt(userScore), BigInt(userRewards)); + counter.count(BigInt(userSePSP2Balance.combined), BigInt(userRewards)); return userRewards; } @@ -124,12 +148,16 @@ export async function composeWithAmountsByProgram( const optimismRefundEligibleStakers = new Set( optimismRefunds.map(v => v.account), ); - const { byAccountLowercase, totalScore } = await fetchPastEpochData( - epoch - GasRefundV2EpochFlip, - ); + + // TODO: revisit going from byAccountLowercase to sePSP2StakersByAccountLowercase here + const { sePSP2BalancesByUserByChain, totalSupplySePSP2 } = + await fetchPastEpochData(epoch - GasRefundV2EpochFlip); // prepare list of stakers that don't have refund on optimism const stakersNotEligibleForOptimismRefund = new Set( - Object.keys(byAccountLowercase) + // TODO: revisit going from byAccountLowercase to sePSP2StakersByAccountLowercase here + // will need to change the approach to distributing blockchain-wise (ethereum vs optimism) + // current code doesn't make sense any more and was updated just for the sake of interim commit + Object.keys(sePSP2BalancesByUserByChain) // .map(v => v.account) .filter(account => !optimismRefundEligibleStakers.has(account)), ); @@ -205,25 +233,36 @@ export async function composeWithAmountsByProgram( 'rewards distribution counter does not match the total rewards', ); assert( - rewardsDistributionCounter.scoreCleared === BigInt(totalScore), - 'rewards distribution counter does not match the total score', + rewardsDistributionCounter.sePSP2BalanceCleared === + BigInt(totalSupplySePSP2), + 'rewards distribution counter does not match the total supply of sePSP2', ); return newAllRefunds; } type RewardsDistributionCounter = { - scoreCleared: BigInt; + sePSP2BalanceCleared: BigInt; rewardsAllocated: BigInt; - count: (userScore: BigInt, userRewards: BigInt) => void; + count: (userSePSP2Balance: BigInt, userRewards: BigInt) => void; }; function constructRewardsDistributionCounter(): RewardsDistributionCounter { return { - scoreCleared: BigInt(0), + sePSP2BalanceCleared: BigInt(0), rewardsAllocated: BigInt(0), - count(userScore: BigInt, userRewards: BigInt) { - this.scoreCleared += userScore; + count(userSePSP2Balance: BigInt, userRewards: BigInt) { + this.sePSP2BalanceCleared += userSePSP2Balance; this.rewardsAllocated += userRewards; }, }; } +function assertSePSP2BalancesIntegrity( + sePSP2StakersByAccountLowercase: Record, + sePSP2BalancesByUserByChain: Record< + string, + Record<1 | 10 | 'combined', string> + >, +) { + // see if the stakers list is inclusive of all holders (throw exception) + throw new Error('TODO: Function not implemented.'); +} From a8c0a22e0d07980bdd34482771bec4e5b8dab533 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Fri, 17 May 2024 09:06:17 +0000 Subject: [PATCH 35/44] change: interm: prepare everything to adjust distribution logic to split betwen sePSP2 holders --- src/lib/utils/aura-rewards.ts | 256 ++++++++++++++++++++++++++++++---- src/lib/utils/covalent.ts | 2 +- 2 files changed, 228 insertions(+), 30 deletions(-) diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index 478ce094..0cf94793 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -1,14 +1,24 @@ import * as pMemoize from 'p-memoize'; import { MinParaBoostData, fetchAccountsScores } from './staking-supervisor'; import BigNumber from 'bignumber.js'; +import { BigNumber as BNfromEthers } from 'ethers'; import { GasRefundV2EpochFlip } from '../gas-refund/gas-refund'; -import { getCurrentEpoch } from '../gas-refund/epoch-helpers'; +import { + getCurrentEpoch, + getEpochStartCalcTime, +} from '../gas-refund/epoch-helpers'; import { CHAIN_ID_OPTIMISM, STAKING_CHAIN_IDS } from '../constants'; import { AddressRewards, AddressRewardsWithAmountsByProgram, } from '../../types'; import { assert } from 'ts-essentials'; +import * as ERC20ABI from '../abi/erc20.abi.json'; +import { Contract } from '@ethersproject/contracts'; +import { grp2ConfigByChain } from '../gas-refund/config'; +import { Provider } from '../provider'; +import { BlockInfo } from '../block-info'; +import { TokenItem, getTokenHolders } from './covalent'; const config: Record = { // @TODO: update this config @@ -22,19 +32,191 @@ const AURA_REWARDS_START_EPOCH_OLD_STYLE = Math.min( const logger = global.LOGGER('aura-rewards'); // TotalSupplySePSP2 = TotalSupply(SePSP2[ethereum]) + TotalSupply(SePSP2[optimism]) -async function fetchTotalSupplySePSP2(epoch: number): Promise { - throw new Error('TODO: Function not implemented.'); - return '123'; +async function fetchTotalSupplySePSP2OnTheChain( + blockNumber: number, + chainId: number, +): Promise { + const sePSP2Address = grp2ConfigByChain[chainId].sePSP2; + assert(sePSP2Address, `sePSP2 address is not defined for chain ${chainId}`); + + const contract = new Contract( + sePSP2Address, + ERC20ABI, + Provider.getJsonRpcProvider(chainId), + ); + + try { + const totalSupply: BNfromEthers = await contract.totalSupply({ + blockTag: blockNumber, + }); + + // DONE: checked the result here, seems to work fine + // from RPC: + // 10: 2248200733537860836142862 + // 1: 36493144844863467534634286 + const result = totalSupply.toString(); + // from covalent + // 10: 2248200733537860836142862 + // 1: 36493144844863467534634286 + debugger; + return result; + } catch (e) { + debugger; + throw e; + } +} +async function fetchTotalSupplySePSP2(blockNumberByChain: { + [chain: number]: number; +}): Promise { + const totalSupplySePSP2entries: [chain: number, balance: string][] = + await Promise.all( + STAKING_CHAIN_IDS.map>(async chainId => [ + chainId, + await fetchTotalSupplySePSP2OnTheChain( + blockNumberByChain[chainId], + chainId, + ), + ]), + ); + const totalSupplySePSP2ByChainId = Object.fromEntries( + totalSupplySePSP2entries, + ); + + // done: checked the result here, seems to work fine + const totalSupplySePSP2 = Object.values(totalSupplySePSP2ByChainId) + .reduce((acc, curr) => acc.plus(curr), new BigNumber(0)) + .toFixed(); + + // debugger; + + return totalSupplySePSP2; } -// TODO: fetch all holders from covalent? +async function fetchSePSP2BalancesByUser( + epochNewStyle: number, + chainId: number, +) { + const epochOldStyle = GasRefundV2EpochFlip + epochNewStyle; + const nextEpochStartTimestamp = await getEpochStartCalcTime( + epochOldStyle + 1, + ); + const blockInfo = BlockInfo.getInstance(chainId); + const nextEpochStartBlock = await blockInfo.getBlockAfterTimeStamp( + nextEpochStartTimestamp, + ); + + assert(nextEpochStartBlock, 'nextEpochStartBlock should be defined'); + const sePSP2Address = grp2ConfigByChain[chainId].sePSP2; + assert(sePSP2Address, `sePSP2 address is not defined for chain ${chainId}`); + + const options = { + token: sePSP2Address, + chainId, + blockHeight: String(nextEpochStartBlock), + }; + + const sePSPTokenHolders = await getTokenHolders(options); + + // breakpoint to verify match of totalSupply vs covalent holders combined balance + // // 10: 2248200733537860836142862 + // // 1: 36493144844863467534634286 + // const totalBalanceAmongHolders = sePSPTokenHolders + // .reduce((acc, curr) => acc.plus(curr.balance), new BigNumber(0)) + // .toFixed(); + // debugger; + // TODO: add assert here? + return { + sePSPTokenHolders, + nextEpochStartBlock, + }; +} + +// fetch all holders from covalent // AllSePSP2Holders = Unique( Holders(SePSP2[ethereum]) + Holders(SePSP2[optimism]) ) +type CovalentHoldersData = { sePSPTokenHolders: TokenItem[] }; +type CovalentHoldersDataByNetwork = Record; +type SePSP2BalancesByNetwork = { + byNetwork: { [chainId: number]: string }; + combined: string; +}; async function fetchSePSP2BalancesByUserByChain( - epoch: number, -): Promise>> { - throw new Error('TODO: Function not implemented.'); - return {}; + epochNewStyle: number, +): Promise<{ + balancesByAccount: { [account: string]: SePSP2BalancesByNetwork }; + + nextEpochStartBlockByChain: { + [chainId: number]: number; // nextEpochStartBlock: number} + }; +}> { + const sePSP2BalancesByUserByChainEntries: [ + chainId: number, + holdersdata: CovalentHoldersData & { nextEpochStartBlock: number }, + ][] = await Promise.all( + STAKING_CHAIN_IDS.map< + Promise<[number, CovalentHoldersData & { nextEpochStartBlock: number }]> + >(async chainId => { + const { nextEpochStartBlock, ...sePSP2BalancesByUser } = + await fetchSePSP2BalancesByUser(epochNewStyle, chainId); + return [chainId, { nextEpochStartBlock, ...sePSP2BalancesByUser }]; + }), + ); + + const sePSP2BalancesByUserByChain = Object.fromEntries( + sePSP2BalancesByUserByChainEntries, + ); + + function mapCovalentHoldersToSePSP2Balances( + sePSP2BalancesByUserByChain: CovalentHoldersDataByNetwork, + ): Record { + const allSePSP2StakersOnAllNetworksUniqueLowercase = new Set( + Object.values(sePSP2BalancesByUserByChain) + .flatMap(v => v.sePSPTokenHolders) + .map(v => v.address.toLowerCase()), + ); + + const result = Array.from( + allSePSP2StakersOnAllNetworksUniqueLowercase, + ).reduce>( + (acc, sePSP2HolderAddresLowerCase) => { + const sePSP2BalancesByUser = Object.fromEntries( + STAKING_CHAIN_IDS.map(chainId => [ + chainId, + sePSP2BalancesByUserByChain[chainId].sePSPTokenHolders.find( + v => v.address.toLowerCase() === sePSP2HolderAddresLowerCase, + )?.balance || '0', + ]), + ); + acc[sePSP2HolderAddresLowerCase] = { + byNetwork: sePSP2BalancesByUser, + combined: Object.values(sePSP2BalancesByUser) + .reduce((acc, curr) => acc.plus(curr), new BigNumber(0)) + .toFixed(), + }; + return acc; + }, + {}, + ); + + // checked seems ok + // debugger; + + return result; + } + + return { + balancesByAccount: mapCovalentHoldersToSePSP2Balances( + sePSP2BalancesByUserByChain, + ), + nextEpochStartBlockByChain: STAKING_CHAIN_IDS.reduce( + (acc, chainId) => ({ + ...acc, + [chainId]: sePSP2BalancesByUserByChain[chainId].nextEpochStartBlock, + }), + {}, + ), + }; } + async function _fetchEpochData(epoch: number) { const list = await fetchAccountsScores(epoch); @@ -47,20 +229,21 @@ async function _fetchEpochData(epoch: number) { return acc; }, {}); - const sePSP2BalancesByUserByChain = await fetchSePSP2BalancesByUserByChain( - epoch, - ); + const { nextEpochStartBlockByChain, ...sePSP2BalancesByUserByChain } = + await fetchSePSP2BalancesByUserByChain(epoch); assertSePSP2BalancesIntegrity( sePSP2StakersByAccountLowercase, - sePSP2BalancesByUserByChain, + sePSP2BalancesByUserByChain.balancesByAccount, ); logger.info( `loaded pre-requisites for computing Aura rewards for epoch ${epoch}`, ); - const totalSupplySePSP2 = await fetchTotalSupplySePSP2(epoch); + const totalSupplySePSP2 = await fetchTotalSupplySePSP2( + nextEpochStartBlockByChain, + ); return { totalSupplySePSP2, @@ -98,11 +281,11 @@ export async function computeUserRewardWei( await fetchPastEpochData(epoch); // after going from byAccountLowercase to sePSP2BalancesByUserByChain here, it relies on sePSP2 balances rather than scores. - const userSePSP2Balance: { - 1: string; - 10: string; - combined: string; - } = sePSP2BalancesByUserByChain[user.toLowerCase()]; + const userSePSP2Balance: SePSP2BalancesByNetwork = + sePSP2BalancesByUserByChain.balancesByAccount[user.toLowerCase()]; + + // if user is not found amongst sePSP2 holders, no Aura rewards then + if (!userSePSP2Balance) return '0'; const totalRewards = config[epochOldStyle] || '0'; const remainingRewards = new BigNumber(totalRewards) @@ -228,10 +411,15 @@ export async function composeWithAmountsByProgram( .flat() .concat(nonOptimismRefundsWithAmountsByProgram); - assert( - rewardsDistributionCounter.rewardsAllocated == BigInt(config[epoch]), - 'rewards distribution counter does not match the total rewards', - ); + try { + assert( + rewardsDistributionCounter.rewardsAllocated == BigInt(config[epoch]), + 'rewards distribution counter does not match the total rewards', + ); + } catch (e) { + debugger; + throw e; + } assert( rewardsDistributionCounter.sePSP2BalanceCleared === BigInt(totalSupplySePSP2), @@ -256,13 +444,23 @@ function constructRewardsDistributionCounter(): RewardsDistributionCounter { }, }; } + +// see if the stakers list is inclusive of all holders (throw exception) function assertSePSP2BalancesIntegrity( sePSP2StakersByAccountLowercase: Record, - sePSP2BalancesByUserByChain: Record< - string, - Record<1 | 10 | 'combined', string> - >, + sePSP2BalancesByUserByChain: Record, ) { - // see if the stakers list is inclusive of all holders (throw exception) - throw new Error('TODO: Function not implemented.'); + assert( + Object.keys(sePSP2StakersByAccountLowercase).length === + Object.keys(sePSP2BalancesByUserByChain).length, + 'sePSP2StakersByAccountLowercase and sePSP2BalancesByUserByChain should have the same length', + ); + Object.keys(sePSP2StakersByAccountLowercase).forEach(account => { + assert( + sePSP2BalancesByUserByChain[account.toLowerCase()], + `account ${account} is missing in sePSP2BalancesByUserByChain`, + ); + }); + // the above two checks should be enough to verify 2 sources return the same set of accounts? + // debugger; } diff --git a/src/lib/utils/covalent.ts b/src/lib/utils/covalent.ts index c481b2e5..2a8397a6 100644 --- a/src/lib/utils/covalent.ts +++ b/src/lib/utils/covalent.ts @@ -120,7 +120,7 @@ interface ErrorResponse { error_code: number; // e.g. 507 } -interface TokenItem { +export interface TokenItem { contract_decimals: number; contract_name: string; contract_ticker_symbol: string; From cf0a561db33fc88ab1bb68c4433ed873c0cd7712 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Fri, 17 May 2024 09:28:20 +0000 Subject: [PATCH 36/44] chore: move code around in more logical way to "merge" multiple programs --- .../lib/computeDistributionMerkleData.ts | 139 +++++++++++++++++- src/lib/utils/aura-rewards.ts | 127 +--------------- 2 files changed, 144 insertions(+), 122 deletions(-) diff --git a/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts b/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts index 835e6aac..c41c7c39 100644 --- a/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts +++ b/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts @@ -20,7 +20,144 @@ import { ChainRewardsMapping, MerkleTreeAndChain, } from './types'; -import { composeWithAmountsByProgram } from '../../../../src/lib/utils/aura-rewards'; +import { AddressRewardsWithAmountsByProgram } from '../../../../src/types'; + +function combinePrograms( + input: { programName: string; rewards: AddressRewards[] }[], +): AddressRewardsWithAmountsByProgram[] { + // TODO + return []; +} + +// accepts list of distribution entries +// returns [extended] list of distribution entries, the items of which are extended with "amount by program" and the amount adjusted respectively +export async function composeWithAmountsByProgram( + epoch: number, + originalGasRefundProgramItems: AddressRewards[], +): Promise { + // 1. compute AddressRewards[] rows for Aura Program + // 2. combined AddressRewardsWithAmountsByProgram[] = Aura + GasRefund + return combinePrograms([ + { + programName: 'paraswapGasRefund', + rewards: originalGasRefundProgramItems, + }, + { + programName: 'auraRewards', + rewards: [], // TODO: compute Aura rewards + }, + ]); + // // split optimism and non-optimism refunds + // const { optimismRefunds, nonOptimismRefunds } = data.reduce<{ + // optimismRefunds: AddressRewards[]; + // nonOptimismRefunds: AddressRewards[]; + // }>( + // (acc, curr) => { + // if (curr.chainId === CHAIN_ID_OPTIMISM) { + // acc.optimismRefunds.push(curr); + // } else { + // acc.nonOptimismRefunds.push(curr); + // } + // return acc; + // }, + // { + // optimismRefunds: [], + // nonOptimismRefunds: [], + // }, + // ); + // const optimismRefundEligibleStakers = new Set( + // optimismRefunds.map(v => v.account), + // ); + // // TODO: revisit going from byAccountLowercase to sePSP2StakersByAccountLowercase here + // const { sePSP2BalancesByUserByChain, totalSupplySePSP2 } = + // await fetchPastEpochData(epoch - GasRefundV2EpochFlip); + // // prepare list of stakers that don't have refund on optimism + // const stakersNotEligibleForOptimismRefund = new Set( + // // TODO: revisit going from byAccountLowercase to sePSP2StakersByAccountLowercase here + // // will need to change the approach to distributing blockchain-wise (ethereum vs optimism) + // // current code doesn't make sense any more and was updated just for the sake of interim commit + // Object.keys(sePSP2BalancesByUserByChain) + // // .map(v => v.account) + // .filter(account => !optimismRefundEligibleStakers.has(account)), + // ); + // const rewardsDistributionCounter = constructRewardsDistributionCounter(); + // // compute resulting array by adjusting optimism refunds + creating optimism refunds for those who don't have it + // const adjustedOptimismRefunds: Promise = + // Promise.all( + // optimismRefunds.map(async v => { + // const aura = await computeUserRewardWei( + // v.account, + // epoch, + // rewardsDistributionCounter, + // ); + // return { + // ...v, + // amount: v.amount.plus(aura), // add aura rewards to gas refunds json + // amountsByProgram: { + // aura, + // paraswapGasRefund: v.amount.toFixed(), + // }, + // }; + // }), + // ); + // const additionalOptimismRefunds: Promise< + // AddressRewardsWithAmountsByProgram[] + // > = Promise.all( + // Array.from(stakersNotEligibleForOptimismRefund).map< + // Promise + // >(async account => { + // const aura = await computeUserRewardWei( + // account, + // epoch, + // rewardsDistributionCounter, + // ); + // return { + // account, + // amount: new BigNumber(aura), + // chainId: CHAIN_ID_OPTIMISM, + // amountsByProgram: { + // aura, + // paraswapGasRefund: '0', // the value is chain specific, relates to the `amount` field, not to total amount accross all networks + // }, + // breakDownGRP: STAKING_CHAIN_IDS.reduce>( + // (acc, curr) => { + // acc[curr] = new BigNumber(0); + // return acc; + // }, + // {}, + // ), + // }; + // }), + // ); + // const nonOptimismRefundsWithAmountsByProgram: AddressRewardsWithAmountsByProgram[] = + // nonOptimismRefunds.map(v => ({ + // ...v, + // amountsByProgram: { + // aura: '0', + // paraswapGasRefund: v.amount.toFixed(), + // }, + // })); + // const newAllRefunds = ( + // await Promise.all([adjustedOptimismRefunds, additionalOptimismRefunds]) + // ) + // .flat() + // .concat(nonOptimismRefundsWithAmountsByProgram); + // try { + // assert( + // rewardsDistributionCounter.rewardsAllocated == BigInt(config[epoch]), + // 'rewards distribution counter does not match the total rewards', + // ); + // } catch (e) { + // debugger; + // throw e; + // } + // assert( + // rewardsDistributionCounter.sePSP2BalanceCleared === + // BigInt(totalSupplySePSP2), + // 'rewards distribution counter does not match the total supply of sePSP2', + // ); + // return newAllRefunds; +} function asserted(val: T) { assert(val !== null && val !== undefined, 'val should not be null or undef'); diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index 0cf94793..c9b21d77 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -305,128 +305,13 @@ export async function computeUserRewardWei( return userRewards; } -export async function composeWithAmountsByProgram( +export async function composeAuraRewards( epoch: number, - data: AddressRewards[], -): Promise { - // split optimism and non-optimism refunds - const { optimismRefunds, nonOptimismRefunds } = data.reduce<{ - optimismRefunds: AddressRewards[]; - nonOptimismRefunds: AddressRewards[]; - }>( - (acc, curr) => { - if (curr.chainId === CHAIN_ID_OPTIMISM) { - acc.optimismRefunds.push(curr); - } else { - acc.nonOptimismRefunds.push(curr); - } - return acc; - }, - { - optimismRefunds: [], - nonOptimismRefunds: [], - }, - ); - - const optimismRefundEligibleStakers = new Set( - optimismRefunds.map(v => v.account), - ); - - // TODO: revisit going from byAccountLowercase to sePSP2StakersByAccountLowercase here - const { sePSP2BalancesByUserByChain, totalSupplySePSP2 } = - await fetchPastEpochData(epoch - GasRefundV2EpochFlip); - // prepare list of stakers that don't have refund on optimism - const stakersNotEligibleForOptimismRefund = new Set( - // TODO: revisit going from byAccountLowercase to sePSP2StakersByAccountLowercase here - // will need to change the approach to distributing blockchain-wise (ethereum vs optimism) - // current code doesn't make sense any more and was updated just for the sake of interim commit - Object.keys(sePSP2BalancesByUserByChain) - // .map(v => v.account) - .filter(account => !optimismRefundEligibleStakers.has(account)), - ); - - const rewardsDistributionCounter = constructRewardsDistributionCounter(); - // compute resulting array by adjusting optimism refunds + creating optimism refunds for those who don't have it - const adjustedOptimismRefunds: Promise = - Promise.all( - optimismRefunds.map(async v => { - const aura = await computeUserRewardWei( - v.account, - epoch, - rewardsDistributionCounter, - ); - return { - ...v, - amount: v.amount.plus(aura), // add aura rewards to gas refunds json - amountsByProgram: { - aura, - paraswapGasRefund: v.amount.toFixed(), - }, - }; - }), - ); - - const additionalOptimismRefunds: Promise< - AddressRewardsWithAmountsByProgram[] - > = Promise.all( - Array.from(stakersNotEligibleForOptimismRefund).map< - Promise - >(async account => { - const aura = await computeUserRewardWei( - account, - epoch, - rewardsDistributionCounter, - ); - return { - account, - amount: new BigNumber(aura), - chainId: CHAIN_ID_OPTIMISM, - amountsByProgram: { - aura, - paraswapGasRefund: '0', // the value is chain specific, relates to the `amount` field, not to total amount accross all networks - }, - breakDownGRP: STAKING_CHAIN_IDS.reduce>( - (acc, curr) => { - acc[curr] = new BigNumber(0); - return acc; - }, - {}, - ), - }; - }), - ); - - const nonOptimismRefundsWithAmountsByProgram: AddressRewardsWithAmountsByProgram[] = - nonOptimismRefunds.map(v => ({ - ...v, - amountsByProgram: { - aura: '0', - paraswapGasRefund: v.amount.toFixed(), - }, - })); - - const newAllRefunds = ( - await Promise.all([adjustedOptimismRefunds, additionalOptimismRefunds]) - ) - .flat() - .concat(nonOptimismRefundsWithAmountsByProgram); - - try { - assert( - rewardsDistributionCounter.rewardsAllocated == BigInt(config[epoch]), - 'rewards distribution counter does not match the total rewards', - ); - } catch (e) { - debugger; - throw e; - } - assert( - rewardsDistributionCounter.sePSP2BalanceCleared === - BigInt(totalSupplySePSP2), - 'rewards distribution counter does not match the total supply of sePSP2', - ); - - return newAllRefunds; +): Promise { + // TODO: implement this function - refer to composeWithAmountsByProgram comments in there + // const { sePSP2BalancesByUserByChain, totalSupplySePSP2 } = + // await fetchPastEpochData(epoch - GasRefundV2EpochFlip); + return []; } type RewardsDistributionCounter = { From d222f653e75b150937e30219b34faa5a66568a98 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Fri, 17 May 2024 11:13:18 +0000 Subject: [PATCH 37/44] change: next step - plug Aura rewards --- .../lib/computeDistributionMerkleData.ts | 138 +++--------------- .../distribution/lib/merkle-tree.ts | 8 +- .../lib/storeDistributionDataInDB.ts | 6 +- .../distribution/lib/types.ts | 16 +- src/lib/gas-refund/gas-refund.ts | 2 +- src/lib/utils/aura-rewards.ts | 122 +++++++++++++++- src/types.ts | 16 +- 7 files changed, 163 insertions(+), 145 deletions(-) diff --git a/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts b/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts index c41c7c39..e307fce4 100644 --- a/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts +++ b/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts @@ -16,16 +16,26 @@ import { StakeV2Resolver } from '../../staking/2.0/StakeV2Resolver'; import { AddressChainRewardsMapping, AddressRewards, - AddressRewardsMapping, + AddressRewardsMappingWithMaybeGRP, ChainRewardsMapping, MerkleTreeAndChain, } from './types'; -import { AddressRewardsWithAmountsByProgram } from '../../../../src/types'; +import { + ProgramAgnosticAddressRewards, + AddressRewardsWithAmountsByProgramVariation, +} from '../../../../src/types'; +import { composeAuraRewards } from '../../../../src/lib/utils/aura-rewards'; function combinePrograms( - input: { programName: string; rewards: AddressRewards[] }[], -): AddressRewardsWithAmountsByProgram[] { + input: { programName: string; rewards: ProgramAgnosticAddressRewards[] }[], +): AddressRewardsWithAmountsByProgramVariation[] { // TODO + return input[0].rewards.map(reward => ({ + ...reward, + amountsByProgram: { + [input[0].programName]: reward.amount.toFixed(), + }, + })); return []; } @@ -33,8 +43,8 @@ function combinePrograms( // returns [extended] list of distribution entries, the items of which are extended with "amount by program" and the amount adjusted respectively export async function composeWithAmountsByProgram( epoch: number, - originalGasRefundProgramItems: AddressRewards[], -): Promise { + originalGasRefundProgramItems: ProgramAgnosticAddressRewards[], +): Promise { // 1. compute AddressRewards[] rows for Aura Program // 2. combined AddressRewardsWithAmountsByProgram[] = Aura + GasRefund return combinePrograms([ @@ -44,119 +54,9 @@ export async function composeWithAmountsByProgram( }, { programName: 'auraRewards', - rewards: [], // TODO: compute Aura rewards + rewards: await composeAuraRewards(epoch), // TODO: compute Aura rewards }, ]); - // // split optimism and non-optimism refunds - // const { optimismRefunds, nonOptimismRefunds } = data.reduce<{ - // optimismRefunds: AddressRewards[]; - // nonOptimismRefunds: AddressRewards[]; - // }>( - // (acc, curr) => { - // if (curr.chainId === CHAIN_ID_OPTIMISM) { - // acc.optimismRefunds.push(curr); - // } else { - // acc.nonOptimismRefunds.push(curr); - // } - // return acc; - // }, - // { - // optimismRefunds: [], - // nonOptimismRefunds: [], - // }, - // ); - // const optimismRefundEligibleStakers = new Set( - // optimismRefunds.map(v => v.account), - // ); - // // TODO: revisit going from byAccountLowercase to sePSP2StakersByAccountLowercase here - // const { sePSP2BalancesByUserByChain, totalSupplySePSP2 } = - // await fetchPastEpochData(epoch - GasRefundV2EpochFlip); - // // prepare list of stakers that don't have refund on optimism - // const stakersNotEligibleForOptimismRefund = new Set( - // // TODO: revisit going from byAccountLowercase to sePSP2StakersByAccountLowercase here - // // will need to change the approach to distributing blockchain-wise (ethereum vs optimism) - // // current code doesn't make sense any more and was updated just for the sake of interim commit - // Object.keys(sePSP2BalancesByUserByChain) - // // .map(v => v.account) - // .filter(account => !optimismRefundEligibleStakers.has(account)), - // ); - // const rewardsDistributionCounter = constructRewardsDistributionCounter(); - // // compute resulting array by adjusting optimism refunds + creating optimism refunds for those who don't have it - // const adjustedOptimismRefunds: Promise = - // Promise.all( - // optimismRefunds.map(async v => { - // const aura = await computeUserRewardWei( - // v.account, - // epoch, - // rewardsDistributionCounter, - // ); - // return { - // ...v, - // amount: v.amount.plus(aura), // add aura rewards to gas refunds json - // amountsByProgram: { - // aura, - // paraswapGasRefund: v.amount.toFixed(), - // }, - // }; - // }), - // ); - // const additionalOptimismRefunds: Promise< - // AddressRewardsWithAmountsByProgram[] - // > = Promise.all( - // Array.from(stakersNotEligibleForOptimismRefund).map< - // Promise - // >(async account => { - // const aura = await computeUserRewardWei( - // account, - // epoch, - // rewardsDistributionCounter, - // ); - // return { - // account, - // amount: new BigNumber(aura), - // chainId: CHAIN_ID_OPTIMISM, - // amountsByProgram: { - // aura, - // paraswapGasRefund: '0', // the value is chain specific, relates to the `amount` field, not to total amount accross all networks - // }, - // breakDownGRP: STAKING_CHAIN_IDS.reduce>( - // (acc, curr) => { - // acc[curr] = new BigNumber(0); - // return acc; - // }, - // {}, - // ), - // }; - // }), - // ); - // const nonOptimismRefundsWithAmountsByProgram: AddressRewardsWithAmountsByProgram[] = - // nonOptimismRefunds.map(v => ({ - // ...v, - // amountsByProgram: { - // aura: '0', - // paraswapGasRefund: v.amount.toFixed(), - // }, - // })); - // const newAllRefunds = ( - // await Promise.all([adjustedOptimismRefunds, additionalOptimismRefunds]) - // ) - // .flat() - // .concat(nonOptimismRefundsWithAmountsByProgram); - // try { - // assert( - // rewardsDistributionCounter.rewardsAllocated == BigInt(config[epoch]), - // 'rewards distribution counter does not match the total rewards', - // ); - // } catch (e) { - // debugger; - // throw e; - // } - // assert( - // rewardsDistributionCounter.sePSP2BalanceCleared === - // BigInt(totalSupplySePSP2), - // 'rewards distribution counter does not match the total supply of sePSP2', - // ); - // return newAllRefunds; } function asserted(val: T) { @@ -363,11 +263,11 @@ export async function computeDistributionMerkleData( const allChainsRefunds = await composeWithAmountsByProgram(epoch, withPIP38); const userGRPChainsBreakDowns = allChainsRefunds.reduce<{ - [stakeChainId: number]: AddressRewardsMapping; + [stakeChainId: number]: AddressRewardsMappingWithMaybeGRP; }>((acc, curr) => { if (!acc[curr.chainId]) acc[curr.chainId] = {}; acc[curr.chainId][curr.account] = { - byChain: curr.breakDownGRP, + byChain: 'breakDownGRP' in curr ? curr.breakDownGRP : null, amountsByProgram: curr.amountsByProgram, }; diff --git a/scripts/gas-refund-program/distribution/lib/merkle-tree.ts b/scripts/gas-refund-program/distribution/lib/merkle-tree.ts index 062fd304..1fe819fe 100644 --- a/scripts/gas-refund-program/distribution/lib/merkle-tree.ts +++ b/scripts/gas-refund-program/distribution/lib/merkle-tree.ts @@ -1,8 +1,4 @@ -import { - AddressRewardsMapping, - Claimable, - GasRefundMerkleProof, -} from './types'; +import { Claimable, RewardMerkleProof } from './types'; import { utils, logger } from 'ethers'; import { MerkleTree } from 'merkletreejs'; import { GasRefundTransaction } from '../../../../src/models/GasRefundTransaction'; @@ -91,7 +87,7 @@ function computeMerkleDataForChain({ const merkleRoot = merkleTree.getHexRoot(); - const merkleLeaves: GasRefundMerkleProof[] = allLeaves.map(leaf => { + const merkleLeaves: RewardMerkleProof[] = allLeaves.map(leaf => { const { address, amount } = hashedClaimabled[leaf]; const proofs = merkleTree.getHexProof(leaf); return { diff --git a/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts b/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts index ed109a53..f67104fe 100644 --- a/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts +++ b/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts @@ -5,11 +5,11 @@ import { sliceCalls } from '../../../../src/lib/utils/helpers'; import { GasRefundDistribution } from '../../../../src/models/GasRefundDistribution'; import { GasRefundParticipation } from '../../../../src/models/GasRefundParticipation'; import { GasRefundParticipantData } from '../../../../src/lib/gas-refund/gas-refund'; -import { GasRefundMerkleProof, GasRefundMerkleTree } from './types'; +import { RewardMerkleProof, RewardMerkleTree } from './types'; export async function storeDistributionDataInDB( chainId: number, - merkleTree: GasRefundMerkleTree, + merkleTree: RewardMerkleTree, ) { const { root: { epoch, merkleRoot, totalAmount }, @@ -30,7 +30,7 @@ export async function storeDistributionDataInDB( ); const epochDataToUpdate: GasRefundParticipantData[] = merkleProofs.map( - (leaf: GasRefundMerkleProof) => { + (leaf: RewardMerkleProof) => { const { address: account, proof: merkleProofs, diff --git a/scripts/gas-refund-program/distribution/lib/types.ts b/scripts/gas-refund-program/distribution/lib/types.ts index 2451f8f5..dc58da40 100644 --- a/scripts/gas-refund-program/distribution/lib/types.ts +++ b/scripts/gas-refund-program/distribution/lib/types.ts @@ -12,22 +12,22 @@ export type MerkleRoot = { epoch: number; }; -export type GasRefundMerkleProof = { +export type RewardMerkleProof = { proof: string[]; address: string; amount: string; epoch: number; - GRPChainBreakDown: { [chainId: number]: string }; - amountsByProgram: Record; + GRPChainBreakDown: { [chainId: number]: string } | null; // will be null if address is not eligible for GRP (could be if they are still eligible for Aura for example) + amountsByProgram: AmountsByProgram; }; -export type GasRefundMerkleTree = { +export type RewardMerkleTree = { root: MerkleRoot; - merkleProofs: GasRefundMerkleProof[]; + merkleProofs: RewardMerkleProof[]; }; export type MerkleTreeAndChain = { - merkleTree: GasRefundMerkleTree; + merkleTree: RewardMerkleTree; chainId: string; }; @@ -49,10 +49,10 @@ export type AddressRewards = { breakDownGRP: { [GRPChainId: number]: BigNumber }; }; -export type AddressRewardsMapping = { +export type AddressRewardsMappingWithMaybeGRP = { [account: string]: { amountsByProgram: AmountsByProgram; - byChain: { [grpChainId: number]: BigNumber }; + byChain: { [grpChainId: number]: BigNumber } | null; }; }; diff --git a/src/lib/gas-refund/gas-refund.ts b/src/lib/gas-refund/gas-refund.ts index 0b2d27b6..2499b698 100644 --- a/src/lib/gas-refund/gas-refund.ts +++ b/src/lib/gas-refund/gas-refund.ts @@ -116,7 +116,7 @@ export interface GasRefundParticipantData { chainId: number; merkleProofs: string[]; isCompleted: boolean; - GRPChainBreakDown: GRPChainBreakDown; + GRPChainBreakDown: GRPChainBreakDown | null; amountsByProgram: AmountsByProgram; } diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index c9b21d77..226a7075 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -7,11 +7,8 @@ import { getCurrentEpoch, getEpochStartCalcTime, } from '../gas-refund/epoch-helpers'; -import { CHAIN_ID_OPTIMISM, STAKING_CHAIN_IDS } from '../constants'; -import { - AddressRewards, - AddressRewardsWithAmountsByProgram, -} from '../../types'; +import { STAKING_CHAIN_IDS } from '../constants'; +import { ProgramAgnosticAddressRewards } from '../../types'; import { assert } from 'ts-essentials'; import * as ERC20ABI from '../abi/erc20.abi.json'; import { Contract } from '@ethersproject/contracts'; @@ -307,10 +304,123 @@ export async function computeUserRewardWei( export async function composeAuraRewards( epoch: number, -): Promise { +): Promise { // TODO: implement this function - refer to composeWithAmountsByProgram comments in there // const { sePSP2BalancesByUserByChain, totalSupplySePSP2 } = // await fetchPastEpochData(epoch - GasRefundV2EpochFlip); + + debugger; + + // // split optimism and non-optimism refunds + // const { optimismRefunds, nonOptimismRefunds } = data.reduce<{ + // optimismRefunds: AddressRewards[]; + // nonOptimismRefunds: AddressRewards[]; + // }>( + // (acc, curr) => { + // if (curr.chainId === CHAIN_ID_OPTIMISM) { + // acc.optimismRefunds.push(curr); + // } else { + // acc.nonOptimismRefunds.push(curr); + // } + // return acc; + // }, + // { + // optimismRefunds: [], + // nonOptimismRefunds: [], + // }, + // ); + // const optimismRefundEligibleStakers = new Set( + // optimismRefunds.map(v => v.account), + // ); + // // TODO: revisit going from byAccountLowercase to sePSP2StakersByAccountLowercase here + // const { sePSP2BalancesByUserByChain, totalSupplySePSP2 } = + // await fetchPastEpochData(epoch - GasRefundV2EpochFlip); + // // prepare list of stakers that don't have refund on optimism + // const stakersNotEligibleForOptimismRefund = new Set( + // // TODO: revisit going from byAccountLowercase to sePSP2StakersByAccountLowercase here + // // will need to change the approach to distributing blockchain-wise (ethereum vs optimism) + // // current code doesn't make sense any more and was updated just for the sake of interim commit + // Object.keys(sePSP2BalancesByUserByChain) + // // .map(v => v.account) + // .filter(account => !optimismRefundEligibleStakers.has(account)), + // ); + // const rewardsDistributionCounter = constructRewardsDistributionCounter(); + // // compute resulting array by adjusting optimism refunds + creating optimism refunds for those who don't have it + // const adjustedOptimismRefunds: Promise = + // Promise.all( + // optimismRefunds.map(async v => { + // const aura = await computeUserRewardWei( + // v.account, + // epoch, + // rewardsDistributionCounter, + // ); + // return { + // ...v, + // amount: v.amount.plus(aura), // add aura rewards to gas refunds json + // amountsByProgram: { + // aura, + // paraswapGasRefund: v.amount.toFixed(), + // }, + // }; + // }), + // ); + // const additionalOptimismRefunds: Promise< + // AddressRewardsWithAmountsByProgram[] + // > = Promise.all( + // Array.from(stakersNotEligibleForOptimismRefund).map< + // Promise + // >(async account => { + // const aura = await computeUserRewardWei( + // account, + // epoch, + // rewardsDistributionCounter, + // ); + // return { + // account, + // amount: new BigNumber(aura), + // chainId: CHAIN_ID_OPTIMISM, + // amountsByProgram: { + // aura, + // paraswapGasRefund: '0', // the value is chain specific, relates to the `amount` field, not to total amount accross all networks + // }, + // breakDownGRP: STAKING_CHAIN_IDS.reduce>( + // (acc, curr) => { + // acc[curr] = new BigNumber(0); + // return acc; + // }, + // {}, + // ), + // }; + // }), + // ); + // const nonOptimismRefundsWithAmountsByProgram: AddressRewardsWithAmountsByProgram[] = + // nonOptimismRefunds.map(v => ({ + // ...v, + // amountsByProgram: { + // aura: '0', + // paraswapGasRefund: v.amount.toFixed(), + // }, + // })); + // const newAllRefunds = ( + // await Promise.all([adjustedOptimismRefunds, additionalOptimismRefunds]) + // ) + // .flat() + // .concat(nonOptimismRefundsWithAmountsByProgram); + // try { + // assert( + // rewardsDistributionCounter.rewardsAllocated == BigInt(config[epoch]), + // 'rewards distribution counter does not match the total rewards', + // ); + // } catch (e) { + // debugger; + // throw e; + // } + // assert( + // rewardsDistributionCounter.sePSP2BalanceCleared === + // BigInt(totalSupplySePSP2), + // 'rewards distribution counter does not match the total supply of sePSP2', + // ); + // return newAllRefunds; return []; } diff --git a/src/types.ts b/src/types.ts index bc7d4a1d..52eab6a0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,13 +6,25 @@ export interface ChainBalanceMapping { export type AmountsByProgram = Record; -export type AddressRewards = { +export type ProgramAgnosticAddressRewards = { account: string; amount: BigNumber; chainId: number; +}; + +type AddressRewardsGRP = ProgramAgnosticAddressRewards & { breakDownGRP: { [GRPChainId: number]: BigNumber }; }; -export type AddressRewardsWithAmountsByProgram = AddressRewards & { +type AmountsByProgramField = { amountsByProgram: AmountsByProgram; }; + +type ItemGRP = AddressRewardsGRP & AmountsByProgramField; +type ItemAura = ProgramAgnosticAddressRewards & AmountsByProgramField; +export type AddressRewardsWithAmountsByProgramVariation = ItemGRP | ItemAura; + +type ProgramAgnosticAddressRewardsWithAmountsByProgramWithTemporaryGRP = + AddressRewardsGRP & { + amountsByProgram: AmountsByProgram; + }; From 933a846e2683fbf25e17f9f8e63edba384342999 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Sun, 19 May 2024 05:02:57 +0000 Subject: [PATCH 38/44] adjust GRP + Aura composability --- .../lib/computeDistributionMerkleData.ts | 97 +++++++++++++++++-- src/models/GasRefundParticipation.ts | 4 + src/types.ts | 9 +- 3 files changed, 103 insertions(+), 7 deletions(-) diff --git a/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts b/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts index e307fce4..c76f2cf2 100644 --- a/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts +++ b/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts @@ -23,6 +23,8 @@ import { import { ProgramAgnosticAddressRewards, AddressRewardsWithAmountsByProgramVariation, + isGRPItem, + AddressRewardsGRP, } from '../../../../src/types'; import { composeAuraRewards } from '../../../../src/lib/utils/aura-rewards'; @@ -30,13 +32,96 @@ function combinePrograms( input: { programName: string; rewards: ProgramAgnosticAddressRewards[] }[], ): AddressRewardsWithAmountsByProgramVariation[] { // TODO - return input[0].rewards.map(reward => ({ - ...reward, - amountsByProgram: { - [input[0].programName]: reward.amount.toFixed(), + // return input[0].rewards.map(reward => ({ + // ...reward, + // amountsByProgram: { + // [input[0].programName]: reward.amount.toFixed(), + // }, + // })); + // return []; + + const mergedByChainByUserByProgram: { + [chainId: number]: { + [user: string]: { + srcItems: { + programName: string; + item: ProgramAgnosticAddressRewards; + }[]; + totalAmount: BigNumber; + }; + }; + } = {}; + + input.forEach(({ programName, rewards }) => { + rewards.forEach(reward => { + if (!mergedByChainByUserByProgram[reward.chainId]) + mergedByChainByUserByProgram[reward.chainId] = {}; + + if (!mergedByChainByUserByProgram[reward.chainId][reward.account]) + mergedByChainByUserByProgram[reward.chainId][reward.account] = { + srcItems: [], + totalAmount: new BigNumber(0), + }; + + mergedByChainByUserByProgram[reward.chainId][ + reward.account + ].srcItems.push({ + programName, + item: reward, + }); + + mergedByChainByUserByProgram[reward.chainId][reward.account].totalAmount = + mergedByChainByUserByProgram[reward.chainId][ + reward.account + ].totalAmount.plus(reward.amount); + }); + }); + + const result: AddressRewardsWithAmountsByProgramVariation[] = []; + + Object.entries(mergedByChainByUserByProgram).forEach( + ([chainId, usersData]) => { + Object.entries(usersData).forEach(([user, { srcItems, totalAmount }]) => { + const amountsByProgram: { [program: string]: string } = {}; + + let breakDownGRP: AddressRewardsGRP['breakDownGRP'] | null = null; + srcItems.forEach(srcItem => { + amountsByProgram[srcItem.programName] = srcItem.item.amount.toFixed(); + + const itm = srcItem.item; + if (isGRPItem(itm)) { + breakDownGRP = itm.breakDownGRP; + } + }); + + input.forEach(({ programName }) => { + if (!amountsByProgram[programName]) { + amountsByProgram[programName] = '0'; + } + }); + + result.push({ + account: user, + chainId: +chainId, + amount: totalAmount, + amountsByProgram, + debugInfo: srcItems.map(({ item }) => item.debugInfo).filter(Boolean), + breakDownGRP: breakDownGRP || {}, + }); + }); }, - })); - return []; + ); + + // console.log(result); + // TODO: cleanup this + require('fs').writeFileSync( + 'tmp.json', + JSON.stringify(result, null, 2), + 'utf-8', + ); + + // debugger; + return result; } // accepts list of distribution entries diff --git a/src/models/GasRefundParticipation.ts b/src/models/GasRefundParticipation.ts index eae4fc80..c095800f 100644 --- a/src/models/GasRefundParticipation.ts +++ b/src/models/GasRefundParticipation.ts @@ -58,4 +58,8 @@ export class GasRefundParticipation extends Model { @AllowNull(true) @Column(DataType.DECIMAL) amount: string; + + @AllowNull(true) + @Column({ type: DataType.JSONB }) + debugInfo: any; } diff --git a/src/types.ts b/src/types.ts index 52eab6a0..c30033d4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,9 +10,16 @@ export type ProgramAgnosticAddressRewards = { account: string; amount: BigNumber; chainId: number; + debugInfo?: any; }; -type AddressRewardsGRP = ProgramAgnosticAddressRewards & { +export function isGRPItem( + input: ProgramAgnosticAddressRewards, +): input is AddressRewardsGRP { + return (input as AddressRewardsGRP).breakDownGRP !== undefined; +} + +export type AddressRewardsGRP = ProgramAgnosticAddressRewards & { breakDownGRP: { [GRPChainId: number]: BigNumber }; }; From e7fa16df6c55e790045b8351fe8d264b95dc75e6 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Sun, 19 May 2024 05:03:22 +0000 Subject: [PATCH 39/44] adjust aura rewards computation logic --- src/lib/utils/aura-rewards.ts | 95 +++++++++++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 9 deletions(-) diff --git a/src/lib/utils/aura-rewards.ts b/src/lib/utils/aura-rewards.ts index 226a7075..158e7030 100644 --- a/src/lib/utils/aura-rewards.ts +++ b/src/lib/utils/aura-rewards.ts @@ -19,9 +19,21 @@ import { TokenItem, getTokenHolders } from './covalent'; const config: Record = { // @TODO: update this config - 47: new BigNumber(1e18).multipliedBy(1000_000).toFixed(), + // 47: new BigNumber(1e18).multipliedBy(1000_000).toFixed(), + 47: [ + '40192039080861009090485', + '98303121571430634340377', + '323916692371019838183554', + '887432256209381684726927', + '284291987025675888319487', + '25346262768415038383677', + '342732427701091938796041', + '80296630684899870281772', + ] + .reduce((acc, curr) => acc.plus(curr), new BigNumber(0)) + .toFixed(), }; - +// debugger; const AURA_REWARDS_START_EPOCH_OLD_STYLE = Math.min( ...Object.keys(config).map(Number), ); @@ -55,7 +67,7 @@ async function fetchTotalSupplySePSP2OnTheChain( // from covalent // 10: 2248200733537860836142862 // 1: 36493144844863467534634286 - debugger; + // debugger; return result; } catch (e) { debugger; @@ -255,7 +267,7 @@ const fetchPastEpochData = pMemoize(_fetchEpochData, { // is used during when executing distribution script. // is intended to compute user rewards for the current epoch without remainder -export async function computeUserRewardWei( +async function computeUserRewardWei( user: string, epochOldStyle: number, counter: RewardsDistributionCounter, @@ -303,13 +315,78 @@ export async function computeUserRewardWei( } export async function composeAuraRewards( - epoch: number, + epochOldStyle: number, ): Promise { - // TODO: implement this function - refer to composeWithAmountsByProgram comments in there - // const { sePSP2BalancesByUserByChain, totalSupplySePSP2 } = - // await fetchPastEpochData(epoch - GasRefundV2EpochFlip); + const { sePSP2BalancesByUserByChain, totalSupplySePSP2 } = + await fetchPastEpochData(epochOldStyle - GasRefundV2EpochFlip); + + // init containers for rewards items + const itemsByNetwork: Record = + STAKING_CHAIN_IDS.reduce( + (acc, chainId) => ({ + ...acc, + [chainId]: [], + }), + {}, + ); + + // init counter + const auraRewardDistributionCounter = constructRewardsDistributionCounter(); + + await Promise.all( + Object.entries(sePSP2BalancesByUserByChain.balancesByAccount).map( + async ([account, balances]) => { + const totalUserRewardWei = await computeUserRewardWei( + account, + epochOldStyle, + auraRewardDistributionCounter, + ); + + let rewardsRemainder = new BigNumber(totalUserRewardWei); + const byNetworkEntries = Object.entries(balances.byNetwork); + for (const [idx, [chainId, balance]] of byNetworkEntries.entries()) { + const isLastItem = idx === byNetworkEntries.length - 1; + + // if it's last item -- allocate full remainder, to make sure it's 1:1 + const amount = isLastItem + ? rewardsRemainder + : new BigNumber( + new BigNumber(totalUserRewardWei) + .multipliedBy(balance) + .dividedBy(balances.combined) + + .toFixed(0), + ); + + if (amount.isZero()) continue; + + itemsByNetwork[chainId].push({ + account, + amount, + chainId: Number(chainId), + debugInfo: { + chainId, + totalUserRewardWei, + chainSePSP2Balance: balance, + chainReward: amount.toFixed(0), + combinedTotalSupplySePSP2: totalSupplySePSP2, + }, + }); + + rewardsRemainder = rewardsRemainder.minus(amount); + } + // console.log('totalUserRewardWei', totalUserRewardWei); + // console.log('balances', balances); + // debugger; + }, + ), + ); + + const result = Object.values(itemsByNetwork).flat(); + + // debugger; - debugger; + return result; // // split optimism and non-optimism refunds // const { optimismRefunds, nonOptimismRefunds } = data.reduce<{ From a8fe9accf7510e46ef3bd912564a3edc924e61fc Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Sun, 19 May 2024 05:04:21 +0000 Subject: [PATCH 40/44] add debugInfo to distributions --- .../lib/computeDistributionMerkleData.ts | 2 ++ .../lib/storeDistributionDataInDB.ts | 2 ++ .../gas-refund-program/distribution/lib/types.ts | 4 +++- sequelize/migrations/20240517000000-debugInfo.js | 16 ++++++++++++++++ src/lib/gas-refund/gas-refund.ts | 1 + 5 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 sequelize/migrations/20240517000000-debugInfo.js diff --git a/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts b/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts index c76f2cf2..3d1afc89 100644 --- a/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts +++ b/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts @@ -354,6 +354,7 @@ export async function computeDistributionMerkleData( acc[curr.chainId][curr.account] = { byChain: 'breakDownGRP' in curr ? curr.breakDownGRP : null, amountsByProgram: curr.amountsByProgram, + debugInfo: curr.debugInfo, }; return acc; @@ -374,6 +375,7 @@ export async function computeDistributionMerkleData( l.GRPChainBreakDown = stringifyGRPChainBreakDown(GRPChainBreakDown); l.amountsByProgram = userGRPChainsBreakDowns[+chainId][l.address].amountsByProgram; + l.debugInfo = userGRPChainsBreakDowns[+chainId][l.address].debugInfo; } }); }); diff --git a/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts b/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts index f67104fe..553fdad4 100644 --- a/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts +++ b/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts @@ -37,6 +37,7 @@ export async function storeDistributionDataInDB( amount, GRPChainBreakDown, amountsByProgram, + debugInfo, } = leaf; assert( account == account.toLowerCase(), @@ -51,6 +52,7 @@ export async function storeDistributionDataInDB( amount, GRPChainBreakDown, amountsByProgram, + debugInfo, }; }, ); diff --git a/scripts/gas-refund-program/distribution/lib/types.ts b/scripts/gas-refund-program/distribution/lib/types.ts index dc58da40..ca42c7c3 100644 --- a/scripts/gas-refund-program/distribution/lib/types.ts +++ b/scripts/gas-refund-program/distribution/lib/types.ts @@ -19,6 +19,7 @@ export type RewardMerkleProof = { epoch: number; GRPChainBreakDown: { [chainId: number]: string } | null; // will be null if address is not eligible for GRP (could be if they are still eligible for Aura for example) amountsByProgram: AmountsByProgram; + debugInfo?: any; }; export type RewardMerkleTree = { @@ -52,7 +53,8 @@ export type AddressRewards = { export type AddressRewardsMappingWithMaybeGRP = { [account: string]: { amountsByProgram: AmountsByProgram; - byChain: { [grpChainId: number]: BigNumber } | null; + byChain: { [grpChainId: number]: BigNumber } | null; // only exist in GRP-inclusive items + debugInfo?: any; }; }; diff --git a/sequelize/migrations/20240517000000-debugInfo.js b/sequelize/migrations/20240517000000-debugInfo.js new file mode 100644 index 00000000..eb1762d0 --- /dev/null +++ b/sequelize/migrations/20240517000000-debugInfo.js @@ -0,0 +1,16 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + return queryInterface.addColumn( + 'GasRefundParticipations', + 'debugInfo', + Sequelize.JSONB, + ); + }, + + async down(queryInterface) { + queryInterface.removeColumn('GasRefundParticipations', 'debugInfo'); + }, +}; diff --git a/src/lib/gas-refund/gas-refund.ts b/src/lib/gas-refund/gas-refund.ts index 2499b698..1445e4e0 100644 --- a/src/lib/gas-refund/gas-refund.ts +++ b/src/lib/gas-refund/gas-refund.ts @@ -118,6 +118,7 @@ export interface GasRefundParticipantData { isCompleted: boolean; GRPChainBreakDown: GRPChainBreakDown | null; amountsByProgram: AmountsByProgram; + debugInfo?: any; } export enum TransactionStatus { From 238ef43abc5ef88b4d24da9dfcff63c8494bbcd7 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Sun, 19 May 2024 05:04:35 +0000 Subject: [PATCH 41/44] chore: handier re-distribution with cleanup prior to insertion --- .../distribution/lib/storeDistributionDataInDB.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts b/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts index 553fdad4..671ac5d1 100644 --- a/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts +++ b/scripts/gas-refund-program/distribution/lib/storeDistributionDataInDB.ts @@ -17,6 +17,17 @@ export async function storeDistributionDataInDB( } = merkleTree; await database.sequelize?.transaction(async t => { + // TODO: revisit + await database.sequelize.query( + ` + DELETE FROM "GasRefundDistributions" WHERE epoch = ${epoch} and "chainId"=${chainId}; + DELETE FROM "GasRefundParticipations" WHERE epoch = ${epoch} and "chainId"=${chainId}; + + `, + { + transaction: t, + }, + ); await GasRefundDistribution.create( { epoch, From 9796bc0f5e84d40a9bef90fa547ab48c3483feda Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Sun, 19 May 2024 05:04:59 +0000 Subject: [PATCH 42/44] add epoch-47 db-persist script to package json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 975c6eb0..920a8af3 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "gas-refund:dev:compute-gas-refund-save-db": "patch-package && NODE_ENV=development ts-node scripts/gas-refund-program/computeGasRefund", "gas-refund:prod:compute-gas-refund-save-db": "node scripts/gas-refund-program/computeGasRefund.js", "gas-refund:computeDistributionDataAndPersistDB": "patch-package && NODE_ENV=development ts-node scripts/gas-refund-program/distribution/computeDistributionDataAndPersistDB", + "gas-refund:computeDistributionDataAndPersistDB-epoch-47": "DISTRIBUTED_EPOCH=47 yarn gas-refund:computeDistributionDataAndPersistDB", "gas-refund:computeDistributionFilesAndPersistIPFS": "patch-package && NODE_ENV=development ts-node scripts/gas-refund-program/distribution/computeDistributionFilesAndPersistIPFS", "migrate:up": "source .env && DATABASE_URL=$DATABASE_URL npx sequelize-cli db:migrate # <- executes any new migrations that are not in sequalize meta table yet, sorted alphabetically", "migrate:undo": "source .env && DATABASE_URL=$DATABASE_URL npx sequelize-cli db:migrate:undo # <- undoes the last migration from sequalize meta table, sorted alphabetically", From 9af74c262dc361ebe93de2e4df7c01de7fef0f49 Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Tue, 21 May 2024 07:04:45 +0000 Subject: [PATCH 43/44] chore: final adjustments before distribution (also adjust logistics) --- .../lib/computeDistributionMerkleData.ts | 4 +-- .../lib/computeDistributionSafeProposal.ts | 35 ++++++++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts b/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts index 3d1afc89..770c097a 100644 --- a/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts +++ b/scripts/gas-refund-program/distribution/lib/computeDistributionMerkleData.ts @@ -354,7 +354,7 @@ export async function computeDistributionMerkleData( acc[curr.chainId][curr.account] = { byChain: 'breakDownGRP' in curr ? curr.breakDownGRP : null, amountsByProgram: curr.amountsByProgram, - debugInfo: curr.debugInfo, + // debugInfo: curr.debugInfo, }; return acc; @@ -375,8 +375,8 @@ export async function computeDistributionMerkleData( l.GRPChainBreakDown = stringifyGRPChainBreakDown(GRPChainBreakDown); l.amountsByProgram = userGRPChainsBreakDowns[+chainId][l.address].amountsByProgram; - l.debugInfo = userGRPChainsBreakDowns[+chainId][l.address].debugInfo; } + l.debugInfo = userGRPChainsBreakDowns[+chainId][l.address].debugInfo; }); }); return merkleTreeData; diff --git a/scripts/gas-refund-program/distribution/lib/computeDistributionSafeProposal.ts b/scripts/gas-refund-program/distribution/lib/computeDistributionSafeProposal.ts index 431e47b6..f05676e6 100644 --- a/scripts/gas-refund-program/distribution/lib/computeDistributionSafeProposal.ts +++ b/scripts/gas-refund-program/distribution/lib/computeDistributionSafeProposal.ts @@ -5,6 +5,7 @@ import { MerkleRedeemAddressSePSP1 } from '../../../../src/lib/gas-refund/gas-re import { Contract } from 'ethers'; import { Provider } from '../../../../src/lib/provider'; import { ERC20Interface, MerkleRedeemIface, SePSPIface } from './abis'; +import { CHAIN_ID_OPTIMISM } from '../../../../src/lib/constants'; export async function computeDistributionSafeProposal( merkleDistributionData: MerkleTreeAndChain, @@ -38,19 +39,27 @@ export async function computeDistributionSafeProposal( const PSPAddress = await sePSP1.callStatic.asset(); const txs = [ - { - to: PSPAddress, - data: ERC20Interface.encodeFunctionData('approve', [ - sePSP1Address, - totalAmountRefunded, - ]), - value: '0', - }, - { - to: sePSP1Address, - data: SePSPIface.encodeFunctionData('deposit', [totalAmountRefunded]), - value: '0', - }, + // on optimism no need to obtain sePSP1, as we already have enough from aura rewards + // (was true for EPOCH #016 (47)) + ...(+chainId === CHAIN_ID_OPTIMISM + ? [] + : [ + { + to: PSPAddress, + data: ERC20Interface.encodeFunctionData('approve', [ + sePSP1Address, + totalAmountRefunded, + ]), + value: '0', + }, + { + to: sePSP1Address, + data: SePSPIface.encodeFunctionData('deposit', [ + totalAmountRefunded, + ]), + value: '0', + }, + ]), { to: sePSP1Address, data: ERC20Interface.encodeFunctionData('approve', [ From d0f96d251786a5381fb223167ed79135bb16bc3a Mon Sep 17 00:00:00 2001 From: alexshchur <60445720+alexshchur@users.noreply.github.com> Date: Tue, 21 May 2024 07:05:14 +0000 Subject: [PATCH 44/44] chore: early throw exception in generating scripts --- .../computeDistributionFilesAndPersistIPFS.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/gas-refund-program/distribution/computeDistributionFilesAndPersistIPFS.ts b/scripts/gas-refund-program/distribution/computeDistributionFilesAndPersistIPFS.ts index 5f09fb8c..e7b99968 100644 --- a/scripts/gas-refund-program/distribution/computeDistributionFilesAndPersistIPFS.ts +++ b/scripts/gas-refund-program/distribution/computeDistributionFilesAndPersistIPFS.ts @@ -12,6 +12,21 @@ import { persistDirectoryToPinata } from './utils/pinata'; import { GasRefundGenesisEpoch } from '../../../src/lib/gas-refund/gas-refund'; import { computeDistributionSimulation } from './lib/computeDistributionSimulation'; +assert( + process.env.DISTRIBUTED_EPOCH, + 'DISTRIBUTED_EPOCH env variable is required', +); +assert(process.env.TENDERLY_TOKEN, 'TENDERLY_TOKEN env variable is required'); +assert( + process.env.TENDERLY_PROJECT, + 'TENDERLY_PROJECT env variable is required', +); +assert(process.env.PINATA_API_KEY, 'PINATA_API_KEY env variable is required'); +assert( + process.env.PINATE_API_SECRET, + 'PINATE_API_SECRET env variable is required', +); + const constructBasePath = (epoch: number) => path.join(__dirname, `data/grp-distribution-epoch-${epoch}`);