Skip to content

Commit

Permalink
Merge pull request #88 from paraswap/BACK-1201
Browse files Browse the repository at this point in the history
feat(BACK-1201): adapt gas refund distribution for multichain
  • Loading branch information
mwamedacen authored Sep 11, 2023
2 parents c5a1614 + d9b167e commit 6b3d964
Show file tree
Hide file tree
Showing 21 changed files with 1,923 additions and 240 deletions.
3 changes: 3 additions & 0 deletions migrations/add_MCS_columns_GasRefundParticipation.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
alter table "GasRefundParticipations"
add column amount varchar,
add column "GRPChainBreakDown" jsonb;
265 changes: 216 additions & 49 deletions scripts/gas-refund-program/computeMerkleTree.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import '../../src/lib/log4js';
import * as dotenv from 'dotenv';
import { Op } from 'sequelize';
dotenv.config();

import { Op } from 'sequelize';
import { computeMerkleData } from './refund/merkle-tree';
import {
fetchLastEpochRefunded,
merkleRootExists,
saveMerkleTreeInDB,
} from './persistance/db-persistance';

import { assert } from 'ts-essentials';
import { Sequelize } from 'sequelize-typescript';
import {
GasRefundGenesisEpoch,
GRP_SUPPORTED_CHAINS,
TransactionStatus,
} from '../../src/lib/gas-refund/gas-refund';
import { GasRefundTransaction } from '../../src/models/GasRefundTransaction';
Expand All @@ -25,39 +23,49 @@ import {
loadEpochMetaData,
resolveEpochCalcTimeInterval,
} from '../../src/lib/gas-refund/epoch-helpers';
import { STAKING_CHAIN_IDS } from '../../src/lib/constants';
import { StakeV2Resolver } from './staking/2.0/StakeV2Resolver';
import BigNumber from 'bignumber.js';
import { isTruthy } from '../../src/lib/utils';
import {
AddressChainRewardsMapping,
AddressRewards,
AddressRewardsMapping,
ChainRewardsMapping,
} from './types';
import { composeRefundWithPIP38Refunds } from './pip38';

const logger = global.LOGGER('GRP:COMPUTE_MERKLE_TREE');

const skipCheck = process.env.SKIP_CHECKS === 'true';
const saveFile = process.env.SAVE_FILE === 'true';

export async function computeAndStoreMerkleTreeForChain({
chainId,
epoch,
}: {
chainId: number;
epoch: number;
}) {
if (!skipCheck && (await merkleRootExists({ chainId, epoch })))
return logger.warn(
`merkle root for chainId=${chainId} epoch=${epoch} already exists`,
);
function asserted<T>(val: T) {
assert(val !== null && val !== undefined, 'val should not be null or undef');

return val;
}

type RefundableTransaction = {
address: string;
timestamp: number;
chainId: number;
refundedAmountPSP: string;
};
export async function getRefundableTransactionData(
epoch: number,
): Promise<RefundableTransaction[]> {
const numOfIdleTxs = await GasRefundTransaction.count({
where: { epoch, chainId, status: TransactionStatus.IDLE },
where: { epoch, status: TransactionStatus.IDLE },
});
assert(
numOfIdleTxs === 0,
`there should be 0 idle transactions for epoch=${epoch} chainId=${chainId}`,
`there should be 0 idle transactions for epoch=${epoch}`,
);

const refundableTransactions: {
address: string;
refundedAmountPSP: string;
}[] = await GasRefundTransaction.findAll({
return GasRefundTransaction.findAll({
where: {
epoch,
chainId,
status: TransactionStatus.VALIDATED,
...(epoch >= 32
? {
Expand All @@ -67,37 +75,203 @@ export async function computeAndStoreMerkleTreeForChain({
}
: {}),
},
attributes: [
'address',
[
Sequelize.fn('SUM', Sequelize.col('refundedAmountPSP')),
'refundedAmountPSP',
],
],
group: ['address'],
attributes: ['address', 'timestamp', 'refundedAmountPSP', 'chainId'],
raw: true,
});
}

const merkleTree = await computeMerkleData({
chainId,
export async function computeAndStoreMerkleTree(epoch: number) {
const userRewardsOnStakingChains = await computeStakingChainsRefundedAmounts(
epoch,
refundableTransactions,
});
);

const _userRewards: AddressRewards[] = Object.keys(userRewardsOnStakingChains)
.map(account =>
Object.entries(userRewardsOnStakingChains[account]).map(
([chainId, { amount, breakDownGRP }]) => ({
chainId: +chainId,
amount,
breakDownGRP,
account,
}),
),
)
.flat()
.filter(entry => !entry.amount.eq(0));

const userRewards = composeRefundWithPIP38Refunds(epoch, _userRewards);

const userGRPChainsBreakDowns = userRewards.reduce<{
[stakeChainId: number]: AddressRewardsMapping;
}>((acc, curr) => {
if (!acc[curr.chainId]) acc[curr.chainId] = {};
acc[curr.chainId][curr.account] = curr.breakDownGRP;

return acc;
}, {});

const merkleTreeData = await computeMerkleData({ userRewards, epoch });

return Promise.all(
merkleTreeData.map(async ({ chainId, merkleTree }) => {
const chainBreakdowns = userGRPChainsBreakDowns[Number(chainId)];
if (saveFile) {
logger.info('saving merkle tree in file');
await saveMerkleTreeInFile({
chainId: +chainId,
epoch,
merkleTree,
userGRPChainsBreakDowns: chainBreakdowns,
});
} else {
logger.info('saving merkle tree in db');
await saveMerkleTreeInDB({
chainId: +chainId,
epoch,
merkleTree,
userGRPChainsBreakDowns: chainBreakdowns,
});
}
}),
);
}

async function computeStakingChainsRefundedAmounts(epoch: number) {
const refundableTransactions = (
await getRefundableTransactionData(epoch)
).flat();

const refundableTransactionsByAccount = refundableTransactions.reduce<{
[account: string]: RefundableTransaction[];
}>((acc, curr) => {
if (!acc[curr.address]) acc[curr.address] = [];

if (saveFile) {
logger.info('saving merkle tree in file');
await saveMerkleTreeInFile({ chainId, epoch, merkleTree });
} else {
logger.info('saving merkle tree in db');
await saveMerkleTreeInDB({ chainId, epoch, merkleTree });
acc[curr.address].push(curr);

return acc;
}, {});

const stakeResolvers: { [chainId: number]: StakeV2Resolver } = {};

const { startCalcTime, endCalcTime } = await resolveEpochCalcTimeInterval(
epoch,
);
for (const chainId of STAKING_CHAIN_IDS) {
stakeResolvers[chainId] = StakeV2Resolver.getInstance(chainId);

await stakeResolvers[chainId].loadWithinInterval(
startCalcTime,
endCalcTime,
);
}

const userRewardsOnStakingChains: AddressChainRewardsMapping = {};

Object.entries(refundableTransactionsByAccount).forEach(
([account, refundTransactions]) => {
userRewardsOnStakingChains[account] =
refundTransactions.reduce<ChainRewardsMapping>((acc, curr) => {
let refundTransactionRemainingRefundableAmount = new BigNumber(
curr.refundedAmountPSP,
);
const stakesForTimestamp = STAKING_CHAIN_IDS.map(chainId => {
if (
stakeResolvers[chainId].startTimestamp <= curr.timestamp &&
stakeResolvers[chainId].endTimestamp >= curr.timestamp
) {
return {
chainId,
stake: stakeResolvers[chainId].getStakeForRefund(
curr.timestamp,
account,
),
};
}

return null;
}).filter(isTruthy);

const totalStakesAtTimestamp = Object.values(
stakesForTimestamp,
).reduce(
(sum, e) => sum.plus(asserted(e.stake?.stakeScore) || 0),
new BigNumber(0),
);

stakesForTimestamp.forEach(entry => {
if (!acc[entry.chainId])
acc[entry.chainId] = {
amount: new BigNumber(0),
breakDownGRP: {},
};
const refundAmountForChain = new BigNumber(
asserted(entry.stake?.stakeScore) || 0,
)
.multipliedBy(curr.refundedAmountPSP)
.dividedBy(totalStakesAtTimestamp)
.decimalPlaces(0, BigNumber.ROUND_DOWN);

refundTransactionRemainingRefundableAmount =
refundTransactionRemainingRefundableAmount.minus(
refundAmountForChain,
);

acc[entry.chainId].amount =
acc[entry.chainId].amount.plus(refundAmountForChain);
if (!acc[entry.chainId].breakDownGRP[curr.chainId]) {
acc[entry.chainId].breakDownGRP[curr.chainId] = new BigNumber(
refundAmountForChain,
);
} else {
acc[entry.chainId].breakDownGRP[curr.chainId] =
acc[entry.chainId].breakDownGRP[curr.chainId].plus(
refundAmountForChain,
);
}
});

if (!refundTransactionRemainingRefundableAmount.eq(0)) {
for (const entry of stakesForTimestamp) {
if (asserted(entry.stake?.stakeScore) !== '0') {
if (acc[entry.chainId].amount.eq(0)) {
acc[entry.chainId].amount = new BigNumber(
refundTransactionRemainingRefundableAmount,
);
} else {
acc[entry.chainId].amount = acc[entry.chainId].amount.plus(
refundTransactionRemainingRefundableAmount,
);
}

if (!acc[entry.chainId].breakDownGRP[curr.chainId]) {
acc[entry.chainId].breakDownGRP[curr.chainId] = new BigNumber(
refundTransactionRemainingRefundableAmount,
);
} else {
acc[entry.chainId].breakDownGRP[curr.chainId] = acc[
entry.chainId
].breakDownGRP[curr.chainId].plus(
refundTransactionRemainingRefundableAmount,
);
}
break;
}
}
}

return acc;
}, {});
},
);

return userRewardsOnStakingChains;
}

async function startComputingMerkleTreesAllChains() {
await Database.connectAndSync();
await loadEpochMetaData();

const latestEpochRefunded = await fetchLastEpochRefunded(false);
const latestEpochRefunded = await fetchLastEpochRefunded(skipCheck);
let startEpoch = latestEpochRefunded
? latestEpochRefunded + 1
: GasRefundGenesisEpoch;
Expand All @@ -118,14 +292,7 @@ async function startComputingMerkleTreesAllChains() {
);
}

await Promise.all(
GRP_SUPPORTED_CHAINS.map(chainId =>
computeAndStoreMerkleTreeForChain({
chainId,
epoch,
}),
),
);
await computeAndStoreMerkleTree(epoch);
}
}

Expand Down
Loading

0 comments on commit 6b3d964

Please sign in to comment.