From 1a1fa9cee0054ab536e93aac4481b22ea8656610 Mon Sep 17 00:00:00 2001 From: woody <125113430+woodenfurniture@users.noreply.github.com> Date: Fri, 31 May 2024 12:21:47 +1000 Subject: [PATCH] chore: initial rewards distribution cli scaffolding (#50) * feat: added input prompts for rewards distribution * wip: rewards distribution cli * wip: wiring up staking rewards calculation * wip: staking rewards progression * feat: staking rewards accounting with on-chain validation * feat: add progress bar to rewards validation * chore: remove unused rewards simulation --- package.json | 3 + .../calculateRewards/calculateRewards.ts | 263 +++++++++++++++ scripts/rewards-distribution/client.ts | 11 + scripts/rewards-distribution/constants.ts | 30 +- scripts/rewards-distribution/helpers.ts | 176 ++++++++++ scripts/rewards-distribution/index.ts | 241 +++++++------- scripts/rewards-distribution/package.json | 5 + .../rewards-distribution/simulateStaking.ts | 105 ------ scripts/rewards-distribution/validation.ts | 66 ++++ scripts/rewards-distribution/yarn.lock | 306 +++++++++++++++++- simulate-rewards-distribution.sh | 38 --- yarn.lock | 5 + 12 files changed, 971 insertions(+), 278 deletions(-) create mode 100644 scripts/rewards-distribution/calculateRewards/calculateRewards.ts create mode 100644 scripts/rewards-distribution/client.ts delete mode 100644 scripts/rewards-distribution/simulateStaking.ts create mode 100644 scripts/rewards-distribution/validation.ts delete mode 100755 simulate-rewards-distribution.sh diff --git a/package.json b/package.json index bbdb3db..3f41ca9 100644 --- a/package.json +++ b/package.json @@ -17,5 +17,8 @@ }, "simple-git-hooks": { "pre-commit": "yarn lint:sol && yarn pretty-quick --staged" + }, + "dependencies": { + "bignumber.js": "^9.1.2" } } diff --git a/scripts/rewards-distribution/calculateRewards/calculateRewards.ts b/scripts/rewards-distribution/calculateRewards/calculateRewards.ts new file mode 100644 index 0000000..912f540 --- /dev/null +++ b/scripts/rewards-distribution/calculateRewards/calculateRewards.ts @@ -0,0 +1,263 @@ +import { Address, Block } from "viem"; +import { RFoxLog, StakeLog, UnstakeLog } from "../events"; +import { getStakingAmount, isLogType } from "../helpers"; +import { REWARD_RATE, WAD } from "../constants"; +import assert from "assert"; + +type StakingInfo = { + stakingBalance: bigint; + earnedRewards: bigint; + rewardPerTokenStored: bigint; + runeAddress: String; +}; + +const getEmptyStakingInfo = () => { + return { + stakingBalance: 0n, + earnedRewards: 0n, + rewardPerTokenStored: 0n, + runeAddress: "", + }; +}; + +const rewardPerToken = ( + rewardPerTokenStored: bigint, + totalStaked: bigint, + currentTimestamp: bigint, + lastUpdateTimestamp: bigint, +) => { + if (totalStaked == 0n) { + return rewardPerTokenStored; + } + return ( + rewardPerTokenStored + + ((currentTimestamp - lastUpdateTimestamp) * REWARD_RATE * WAD) / totalStaked + ); +}; + +const earned = ( + stakingInfo: StakingInfo, + rewardPerTokenStored: bigint, + totalStaked: bigint, + currentTimestamp: bigint, + lastUpdateTimestamp: bigint, +) => { + return ( + (stakingInfo.stakingBalance * + (rewardPerToken( + rewardPerTokenStored, + totalStaked, + currentTimestamp, + lastUpdateTimestamp, + ) - + stakingInfo.rewardPerTokenStored)) / + WAD + + stakingInfo.earnedRewards + ); +}; + +const updateReward = ( + stakingInfo: StakingInfo, + rewardPerTokenStored: bigint, + totalStaked: bigint, + currentTimestamp: bigint, + lastUpdateTimestamp: bigint, +) => { + rewardPerTokenStored = rewardPerToken( + rewardPerTokenStored, + totalStaked, + currentTimestamp, + lastUpdateTimestamp, + ); + lastUpdateTimestamp = currentTimestamp; + stakingInfo.earnedRewards = earned( + stakingInfo, + rewardPerTokenStored, + totalStaked, + currentTimestamp, + lastUpdateTimestamp, + ); + stakingInfo.rewardPerTokenStored = rewardPerTokenStored; + return { stakingInfo, rewardPerTokenStored, lastUpdateTimestamp }; +}; + +const stake = ( + amount: bigint, + runeAddress: String, + stakingInfo: StakingInfo, + rewardPerTokenStored: bigint, + totalStaked: bigint, + currentTimestamp: bigint, + lastUpdateTimestamp: bigint, +) => { + ({ stakingInfo, rewardPerTokenStored, lastUpdateTimestamp } = updateReward( + stakingInfo, + rewardPerTokenStored, + totalStaked, + currentTimestamp, + lastUpdateTimestamp, + )); + + stakingInfo.stakingBalance += amount; + stakingInfo.runeAddress = runeAddress; + totalStaked += amount; + + return { + stakingInfo, + rewardPerTokenStored, + lastUpdateTimestamp, + totalStaked, + }; +}; + +const unstake = ( + amount: bigint, + stakingInfo: StakingInfo, + rewardPerTokenStored: bigint, + totalStaked: bigint, + currentTimestamp: bigint, + lastUpdateTimestamp: bigint, +) => { + ({ stakingInfo, rewardPerTokenStored, lastUpdateTimestamp } = updateReward( + stakingInfo, + rewardPerTokenStored, + totalStaked, + currentTimestamp, + lastUpdateTimestamp, + )); + + stakingInfo.stakingBalance -= amount; + totalStaked -= amount; + + return { + stakingInfo, + rewardPerTokenStored, + lastUpdateTimestamp, + totalStaked, + }; +}; + +export const calculateRewards = ( + contractCreationBlock: Block, + // The reward is computed as an all-time value, so we need to subtract the rewards at the end of + // the previous epoch. This prevents us missing rewards for the first block in the epoch. + previousEpochEndBlock: Block, + epochEndBlock: Block, + logs: { log: RFoxLog; timestamp: bigint }[], +) => { + let totalStaked = 0n; + let rewardPerTokenStored = 0n; + let lastUpdateTimestamp = contractCreationBlock.timestamp; + const stakingInfoByAccount: Record = {}; + + const stakingLogs = logs.filter( + ( + logWithTimestamp, + ): logWithTimestamp is { log: StakeLog | UnstakeLog; timestamp: bigint } => + isLogType("Stake", logWithTimestamp.log) || + isLogType("Unstake", logWithTimestamp.log), + ); + + const epochStartRewardsByAccount: Record = {}; + const epochEndRewardsByAccount: Record = {}; + + const previousEpochEndBlockNumber = previousEpochEndBlock.number; + const epochEndBlockNumber = epochEndBlock.number; + + assert( + previousEpochEndBlockNumber !== null, + "Epoch start block number is null", + ); + assert(epochEndBlockNumber !== null, "Epoch end block number is null"); + + let hasCalcedStartRewards = false; + + // process logs + for (const { log, timestamp: currentTimestamp } of stakingLogs) { + // Paranoia in case we get logs past the end of the epoch + assert(log.blockNumber <= epochEndBlockNumber); + + // When the block number passes the start of the epoch, assign the reward values for the start of the epoch + if ( + !hasCalcedStartRewards && + log.blockNumber > previousEpochEndBlockNumber + ) { + for (const [account, stakingInfo] of Object.entries( + stakingInfoByAccount, + )) { + epochStartRewardsByAccount[account as Address] = earned( + stakingInfo, + rewardPerTokenStored, + totalStaked, + previousEpochEndBlock.timestamp, + lastUpdateTimestamp, + ); + + hasCalcedStartRewards = true; + } + } + + let stakingInfo = + stakingInfoByAccount[log.args.account] ?? getEmptyStakingInfo(); + + switch (true) { + case isLogType("Stake", log): + ({ + stakingInfo, + rewardPerTokenStored, + lastUpdateTimestamp, + totalStaked, + } = stake( + log.args.amount, + log.args.runeAddress, + stakingInfo, + rewardPerTokenStored, + totalStaked, + currentTimestamp, + lastUpdateTimestamp, + )); + break; + case isLogType("Unstake", log): + ({ + stakingInfo, + rewardPerTokenStored, + lastUpdateTimestamp, + totalStaked, + } = unstake( + log.args.amount, + stakingInfo, + rewardPerTokenStored, + totalStaked, + currentTimestamp, + lastUpdateTimestamp, + )); + break; + default: + break; + } + + stakingInfoByAccount[log.args.account] = stakingInfo; + } + + // Grab the reward values for the end of the epoch + for (const [account, stakingInfo] of Object.entries(stakingInfoByAccount)) { + epochEndRewardsByAccount[account as Address] = earned( + stakingInfo, + rewardPerTokenStored, + totalStaked, + epochEndBlock.timestamp, + lastUpdateTimestamp, + ); + } + + const earnedRewardsByAccount: Record = {}; + + for (const [account, epochEndReward] of Object.entries( + epochEndRewardsByAccount, + )) { + earnedRewardsByAccount[account as Address] = + epochEndReward - (epochStartRewardsByAccount[account as Address] ?? 0n); + } + + return earnedRewardsByAccount; +}; diff --git a/scripts/rewards-distribution/client.ts b/scripts/rewards-distribution/client.ts new file mode 100644 index 0000000..65edca4 --- /dev/null +++ b/scripts/rewards-distribution/client.ts @@ -0,0 +1,11 @@ +import assert from "assert"; +import { createPublicClient, http } from "viem"; +import { arbitrum } from "viem/chains"; + +// TODO: dotenv or similar +assert(process.env.ARBITRUM_JSON_RPC_URL, "ARBITRUM_JSON_RPC_URL is required"); + +export const publicClient = createPublicClient({ + chain: arbitrum, + transport: http(process.env.ARBITRUM_JSON_RPC_URL), +}); diff --git a/scripts/rewards-distribution/constants.ts b/scripts/rewards-distribution/constants.ts index 7f1b481..a347f17 100644 --- a/scripts/rewards-distribution/constants.ts +++ b/scripts/rewards-distribution/constants.ts @@ -1,25 +1,13 @@ -import { createPublicClient, createWalletClient, http } from "viem"; -import { privateKeyToAccount } from "viem/accounts"; -import { localhost } from "viem/chains"; +import { Address } from "viem"; -export const localChain = { - ...localhost, - id: 31337, -} as const; +export const RUNE_DECIMALS = 8; -export const localOwnerWalletClient = createWalletClient({ - chain: localChain, - account: privateKeyToAccount(process.env.OWNER_PRIVATE_KEY as `0x${string}`), - transport: http(process.env.ANVIL_JSON_RPC_URL), -}); +export const WAD = 1n * 10n ** 18n; +export const REWARD_RATE = 1_000_000_000n; -export const localUserWalletClient = createWalletClient({ - chain: localChain, - account: privateKeyToAccount(process.env.USER_PRIVATE_KEY as `0x${string}`), - transport: http(process.env.ANVIL_JSON_RPC_URL), -}); +// The number of blocks to query at a time, when fetching logs +export const GET_LOGS_BLOCK_STEP_SIZE = 20000n; -export const localPublicClient = createPublicClient({ - chain: localChain, - transport: http(process.env.ANVIL_JSON_RPC_URL), -}); +// RFOX on Arbitrum ERC1967Proxy contract address +export const ARBITRUM_RFOX_PROXY_CONTRACT_ADDRESS: Address = + "0xd612B64A134f3D4830542B7463CE8ca8a29D7268"; diff --git a/scripts/rewards-distribution/helpers.ts b/scripts/rewards-distribution/helpers.ts index fd07336..a022725 100644 --- a/scripts/rewards-distribution/helpers.ts +++ b/scripts/rewards-distribution/helpers.ts @@ -1,3 +1,179 @@ +import BigNumber from "bignumber.js"; +import { + ARBITRUM_RFOX_PROXY_CONTRACT_ADDRESS, + GET_LOGS_BLOCK_STEP_SIZE, +} from "./constants"; +import { AbiEvent, Block, Log, PublicClient } from "viem"; +import cliProgress from "cli-progress"; +import colors from "ansi-colors"; +import { RFoxEvent, RFoxLog, StakeLog, UnstakeLog } from "./events"; +import { stakingV1Abi } from "./generated/abi-types"; + +// we cache promises to prevent async race conditions hydrating the cache +const blockNumberToTimestampCache: Record> = {}; + +export const getBlockTimestamp = async ( + publicClient: PublicClient, + blockNumber: bigint, +): Promise => { + if (blockNumberToTimestampCache[blockNumber.toString()] !== undefined) { + return blockNumberToTimestampCache[blockNumber.toString()]; + } + + const timestampPromise = publicClient + .getBlock({ blockNumber }) + .then((block) => block.timestamp); + + blockNumberToTimestampCache[blockNumber.toString()] = timestampPromise; + + return timestampPromise; +}; + export const assertUnreachable = (x: never): never => { throw Error(`unhandled case: ${x}`); }; + +export const toBaseUnit = ( + amountPrecision: number, + decimals: number, +): bigint => { + const amountBaseUnit = BigInt( + new BigNumber(amountPrecision) + .times(BigNumber(10).pow(decimals)) + .toFixed(0), + ); + + return amountBaseUnit; +}; + +export const fromBaseUnit = ( + amountBaseUnit: bigint, + decimals: number, +): number => { + return new BigNumber(amountBaseUnit.toString()) + .div(BigNumber(10).pow(decimals)) + .toNumber(); +}; + +// Get logs from the blockchain in chunks +export const getLogsChunked = async ( + publicClient: PublicClient, + fromBlock: bigint, + toBlock: bigint, +): Promise<{ log: RFoxLog; timestamp: bigint }[]> => { + const logs = []; + + const progressBar = new cliProgress.SingleBar({ + format: + "Fetch Logs Progress |" + + colors.cyan("{bar}") + + "| {percentage}% || {value}/{total} Pages", + barCompleteChar: "\u2588", + barIncompleteChar: "\u2591", + hideCursor: true, + }); + + progressBar.start( + Math.ceil(Number(toBlock - fromBlock) / Number(GET_LOGS_BLOCK_STEP_SIZE)), + 0, + { + speed: "N/A", + }, + ); + + let fromBlockInner = fromBlock; + + while (fromBlockInner < toBlock) { + let toBlockInner = fromBlockInner + GET_LOGS_BLOCK_STEP_SIZE; + + // Limit toBlockInner to toBlock so we never go past the upper range + if (toBlockInner > toBlock) { + toBlockInner = toBlock; + } + + const logsChunk = await publicClient.getContractEvents({ + address: ARBITRUM_RFOX_PROXY_CONTRACT_ADDRESS, + abi: stakingV1Abi, + fromBlock: fromBlockInner, + toBlock: toBlockInner, + }); + + // Attach the block timestamp for each log + /** + TEMP: Free-tier rate limiting means we cant do a promise.all here + const logsChunkWithTimestamp: { log: RFoxLog; timestamp: bigint }[] = + await Promise.all( + logsChunk.map(async (log) => { + const timestamp = await getBlockTimestamp( + publicClient, + log.blockNumber, + ); + return { + log: log as RFoxLog, + timestamp, + }; + }), + ); + */ + const logsChunkWithTimestamp: { log: RFoxLog; timestamp: bigint }[] = []; + for (const log of logsChunk) { + const timestamp = await getBlockTimestamp(publicClient, log.blockNumber); + logsChunkWithTimestamp.push({ + log: log as RFoxLog, + timestamp, + }); + } + + logs.push(...logsChunkWithTimestamp); + + progressBar.increment(); + + fromBlockInner = toBlockInner; + } + + progressBar.stop(); + + return logs; +}; + +export const indexBy = (values: T[], key: K) => { + return values.reduce( + (acc, value) => { + const innerKey = `${value[key]}`; + if (!acc[innerKey]) { + acc[innerKey] = []; + } + acc[innerKey].push(value); + return acc; + }, + {} as Record, + ); +}; + +export const isEventType = ( + name: Event["name"], + event: AbiEvent, +): event is Event => { + return event.name === name; +}; + +export const isLogType = ( + eventName: L["eventName"], + log: Log, +): log is L => { + return (log as L).eventName === eventName; +}; + +export const getStakingAmount = (log: StakeLog | UnstakeLog): bigint => { + const eventName = log.eventName; + switch (eventName) { + case "Stake": + return log.args.amount; + case "Unstake": + return -log.args.amount; + default: + assertUnreachable(eventName); + } + + throw Error("should be unreachable"); +}; diff --git a/scripts/rewards-distribution/index.ts b/scripts/rewards-distribution/index.ts index 625d1e4..9939810 100644 --- a/scripts/rewards-distribution/index.ts +++ b/scripts/rewards-distribution/index.ts @@ -1,124 +1,143 @@ -import { Address } from "viem"; -import { StakeLog, UnstakeLog, stakeEvent, unstakeEvent } from "./events"; -import { assertUnreachable } from "./helpers"; -import { simulateStaking } from "./simulateStaking"; -import { localPublicClient } from "./constants"; -import { assert } from "console"; - -const getStakingLogs = async ({ - fromBlock, - toBlock, -}: { +import { prompt, type QuestionCollection } from "inquirer"; +import { + ARBITRUM_RFOX_PROXY_CONTRACT_ADDRESS, + RUNE_DECIMALS, +} from "./constants"; +import { fromBaseUnit, getLogsChunked, toBaseUnit } from "./helpers"; +import { publicClient } from "./client"; +import { calculateRewards } from "./calculateRewards/calculateRewards"; +import { stakingV1Abi } from "./generated/abi-types"; +import assert from "assert"; +import { validateRewardsDistribution } from "./validation"; + +const inquireBlockRange = async (): Promise<{ fromBlock: bigint; toBlock: bigint; -}) => { - const logs = await localPublicClient.getLogs({ - events: [stakeEvent, unstakeEvent], - fromBlock, - toBlock, - }); - return logs; +}> => { + const questions: QuestionCollection<{ + fromBlock: number; + toBlock: number; + }> = [ + { + type: "number", + name: "fromBlock", + message: "What is the START block number of this epoch?", + default: 216083216, // TODO: remove this default + }, + { + type: "number", + name: "toBlock", + message: "What is the END block number of this epoch?", + default: 216092990, // TODO: remove this default + }, + ]; + + const { fromBlock, toBlock } = await prompt(questions); + assert(fromBlock < toBlock, "Start block must be less than end block"); + return { fromBlock: BigInt(fromBlock), toBlock: BigInt(toBlock) }; }; -const getStakingAmount = (log: StakeLog | UnstakeLog): bigint => { - const eventName = log.eventName; - switch (eventName) { - case "Stake": - return log.args.amount; - case "Unstake": - return -log.args.amount; - default: - assertUnreachable(eventName); - } - - throw Error("should be unreachable"); +const inquireTotalRuneAmountToDistroBaseUnit = async (): Promise => { + const questions: QuestionCollection<{ totalRuneAmountPrecision: number }> = [ + { + type: "number", + name: "totalRuneAmountPrecision", + message: + "What is the total amount of RUNE to distribute this epoch? Enter this amount in RUNE, not in base units (RUNE*10^8).", + }, + ]; + + const { totalRuneAmountPrecision } = await prompt(questions); + console.log(totalRuneAmountPrecision); + const totalRuneAmountBaseUnit = toBaseUnit( + totalRuneAmountPrecision, + RUNE_DECIMALS, + ); + + return totalRuneAmountBaseUnit; }; -// get the epoch and block reward for a given block number -// TODO: this is a placeholder function matching the spreadsheet logic -const getEpochBlockReward = (_epochEndBlockNumber: bigint) => { - // TODO: blockReward is calculated as half the total rune earned by the DAO divided by the number of blocks in the epoch - return 10n; -}; - -// get the block range for the current epoch -const getEpochBlockRange = () => { - // Monkey-patched to 0 and 5 for testing for now since the current simulation only goes up to block 5 - const previousEpochEndBlockNumber = 0n; - const currentBlockNumber = 500n; - return { - fromBlock: previousEpochEndBlockNumber, - toBlock: currentBlockNumber, - }; +const confirmResponses = async ( + fromBlock: bigint, + toBlock: bigint, + totalRuneAmountToDistroBaseUnit: number, +) => { + const questions: QuestionCollection<{ confirm: boolean }> = [ + { + type: "confirm", + name: "confirm", + message: [ + "Do you want to proceed with these values?", + `* Start block: ${fromBlock}`, + `* End block: ${toBlock}`, + `* Total RUNE to distribute: ${totalRuneAmountToDistroBaseUnit} RUNE`, + ].join("\n"), + }, + ]; + + const { confirm } = await prompt(questions); + + if (!confirm) { + console.log("Exiting..."); + process.exit(0); + } }; -// TODO: this should only process 1 epoch at a time const main = async () => { - await simulateStaking(); - - // iterate all blocks for the current epoch - const { fromBlock, toBlock } = getEpochBlockRange(); - - // Grab the first 500 or so blocks so we can simulate rewards distribution without worrying about how many blocks elapsed during contract deployment - const logs = await getStakingLogs({ fromBlock, toBlock }); - // index logs by block number - const logsByBlockNumber = logs.reduce>((acc, log) => { - if (!acc[log.blockNumber.toString()]) { - acc[log.blockNumber.toString()] = []; - } - acc[log.blockNumber.toString()].push(log); - return acc; - }, {}); - - // TODO: these will be initialized from the last epoch's state - let totalStaked = 0n; - const balanceByAccountBaseUnit: Record = {}; - - // this must be initialized to empty - const epochRewardByAccount: Record = {}; - - const epochBlockReward = getEpochBlockReward(toBlock); - - for (let blockNumber = fromBlock; blockNumber <= toBlock; blockNumber++) { - const incomingLogs: (StakeLog | UnstakeLog)[] | undefined = - logsByBlockNumber[blockNumber.toString()]; - - // process logs if there are any - if (incomingLogs !== undefined) { - for (const log of incomingLogs) { - const account = log.args.account; - if (!balanceByAccountBaseUnit[account]) { - balanceByAccountBaseUnit[account] = 0n; - } - const stakingAmountBaseUnit = getStakingAmount(log); - balanceByAccountBaseUnit[account] += stakingAmountBaseUnit; - totalStaked += stakingAmountBaseUnit; - - // clear empty balances - if (balanceByAccountBaseUnit[account] === 0n) { - delete balanceByAccountBaseUnit[account]; - } - } - } - - for (const account of Object.keys(balanceByAccountBaseUnit) as Address[]) { - // calculate rewards for the current block - // TODO: Bignumber math should be used here to allow for more precision with floating point numbers - const proportionOfTotalStaked = - totalStaked > 0n - ? Number(balanceByAccountBaseUnit[account]) / Number(totalStaked) - : 0; - const reward = Number(epochBlockReward) * proportionOfTotalStaked; - - if (epochRewardByAccount[account] == undefined) { - epochRewardByAccount[account] = 0; - } - epochRewardByAccount[account] += reward; - } - } + const { fromBlock, toBlock } = await inquireBlockRange(); + + const totalRuneAmountToDistroBaseUnit = + await inquireTotalRuneAmountToDistroBaseUnit(); + + await confirmResponses( + fromBlock, + toBlock, + fromBaseUnit(totalRuneAmountToDistroBaseUnit, RUNE_DECIMALS), + ); + + const [previousEpochEndBlock, epochEndBlock, [initLog]] = await Promise.all([ + publicClient.getBlock({ + blockNumber: fromBlock - 1n, + }), + publicClient.getBlock({ + blockNumber: toBlock, + }), + publicClient.getContractEvents({ + address: ARBITRUM_RFOX_PROXY_CONTRACT_ADDRESS, + abi: stakingV1Abi, + eventName: "Initialized", + fromBlock: "earliest", + toBlock: "latest", + }), + ]); + + const contractCreationBlockNumber = initLog.blockNumber; + + const contractCreationBlock = await publicClient.getBlock({ + blockNumber: contractCreationBlockNumber, + }); + + const logs = await getLogsChunked( + publicClient, + contractCreationBlockNumber, + toBlock, + ); + + const earnedRewardsByAccount = calculateRewards( + contractCreationBlock, + previousEpochEndBlock, + epochEndBlock, + logs, + ); + + await validateRewardsDistribution( + publicClient, + earnedRewardsByAccount, + fromBlock, + toBlock, + ); - console.log("rewards to be distributed:"); - console.log(epochRewardByAccount); + // TODO: Confirm details again before proceeding }; main(); diff --git a/scripts/rewards-distribution/package.json b/scripts/rewards-distribution/package.json index f9dd114..6824d4a 100644 --- a/scripts/rewards-distribution/package.json +++ b/scripts/rewards-distribution/package.json @@ -4,12 +4,17 @@ "main": "index.ts", "license": "MIT", "devDependencies": { + "@types/cli-progress": "^3.11.5", + "@types/inquirer": "^8.1.3", "@types/node": "^20.12.4", "ts-node": "^10.9.2", "typescript": "^5.4.4" }, "dependencies": { "@wagmi/cli": "^2.1.4", + "ansi-colors": "^4.1.3", + "cli-progress": "^3.12.0", + "inquirer": "8.2.4", "viem": "^2.9.9" } } diff --git a/scripts/rewards-distribution/simulateStaking.ts b/scripts/rewards-distribution/simulateStaking.ts deleted file mode 100644 index 8b971c0..0000000 --- a/scripts/rewards-distribution/simulateStaking.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Address, formatUnits, parseUnits } from "viem"; - -import { - localPublicClient, - localOwnerWalletClient, - localUserWalletClient, -} from "./constants"; -import { stakingV1Abi, mockFoxTokenAbi } from "./generated/abi-types"; - -export const simulateStaking = async () => { - const ownerWalletClient = localOwnerWalletClient; - const userWalletClient = localUserWalletClient; - const publicClient = localPublicClient; - const bobRuneAddress = "thor17gw75axcnr8747pkanye45pnrwk7p9c3cqncsv"; - - const [bob] = await localUserWalletClient.getAddresses(); - - const mockFoxtokenContractAddress = process.env - .STAKING_TOKEN_ADDRESS as Address; - const mockStakingContractAddress = process.env - .STAKING_PROXY_ADDRESS as Address; - - const foxDecimals = await publicClient.readContract({ - address: mockFoxtokenContractAddress, - abi: mockFoxTokenAbi, - functionName: "decimals", - args: [], - }); - - // Make FOX rain to Bob - const makeItRainTxHash = await ownerWalletClient.writeContract({ - address: mockFoxtokenContractAddress, - abi: mockFoxTokenAbi, - account: bob, - functionName: "makeItRain", - args: [bob, parseUnits("1000", foxDecimals)], - }); - - await publicClient.waitForTransactionReceipt({ hash: makeItRainTxHash }); - console.log(`1000 FOX tokens sent to Bob`); - - // Check Bob's FOX balance - const bobFoxBalance = await publicClient.readContract({ - address: mockFoxtokenContractAddress, - abi: mockFoxTokenAbi, - functionName: "balanceOf", - args: [bob], - }); - - console.log( - `Bob's FOX balance: ${formatUnits(bobFoxBalance, foxDecimals)} FOX`, - ); - - const amountToStakeCryptoPrecision = "100"; - const amountToStakeCryptoBaseUnit = parseUnits( - amountToStakeCryptoPrecision, - foxDecimals, - ); - - // Approve FOX to be spent by the Staking contract - const approveTxHash = await userWalletClient.writeContract({ - address: mockFoxtokenContractAddress, - abi: mockFoxTokenAbi, - account: bob, - functionName: "approve", - args: [mockStakingContractAddress, amountToStakeCryptoBaseUnit], - }); - const { transactionHash } = await publicClient.waitForTransactionReceipt({ - hash: approveTxHash, - }); - - console.log( - `Granted allowance for ${amountToStakeCryptoPrecision} FOX tokens to be spent by Staking contract: ${transactionHash}`, - ); - - // Simulate the staking of FOX tokens so if we see a revert it will be thrown with a reason - const { request } = await publicClient.simulateContract({ - address: mockStakingContractAddress, - abi: stakingV1Abi, - account: bob, - functionName: "stake", - args: [amountToStakeCryptoBaseUnit, bobRuneAddress], - }); - - const stakeTxHash = await userWalletClient.writeContract(request); - - const transactionReceipt = await publicClient.waitForTransactionReceipt({ - hash: stakeTxHash, - }); - const { transactionHash: stakeTransactionHash } = transactionReceipt; - console.log( - `Staked ${amountToStakeCryptoPrecision} FOX from Bob to Staking contract: ${stakeTransactionHash}`, - ); - - const bobStakedBalance = await publicClient.readContract({ - address: mockStakingContractAddress, - abi: stakingV1Abi, - functionName: "balanceOf", - args: [bob], - }); - - console.log( - `Bob's staked balance: ${formatUnits(bobStakedBalance, foxDecimals)} FOX`, - ); -}; diff --git a/scripts/rewards-distribution/validation.ts b/scripts/rewards-distribution/validation.ts new file mode 100644 index 0000000..4cd84be --- /dev/null +++ b/scripts/rewards-distribution/validation.ts @@ -0,0 +1,66 @@ +import cliProgress from "cli-progress"; +import colors from "ansi-colors"; +import { Address, PublicClient } from "viem"; +import { ARBITRUM_RFOX_PROXY_CONTRACT_ADDRESS } from "./constants"; +import { stakingV1Abi } from "./generated/abi-types"; +import assert from "assert"; + +export const validateRewardsDistribution = async ( + publicClient: PublicClient, + earnedRewardsByAccount: Record, + fromBlock: bigint, + toBlock: bigint, +) => { + const progressBar = new cliProgress.SingleBar({ + format: + "Validation Progress |" + + colors.cyan("{bar}") + + "| {percentage}% || {value}/{total} Accounts", + barCompleteChar: "\u2588", + barIncompleteChar: "\u2591", + hideCursor: true, + }); + + progressBar.start(Object.keys(earnedRewardsByAccount).length, 0, { + speed: "N/A", + }); + + // validate rewards per account against the contract + for (const [account, calculatedReward] of Object.entries( + earnedRewardsByAccount, + )) { + const [previousTotalEarnedForAccount, currentTotalEarnedForAccount] = + await Promise.all([ + publicClient.readContract({ + // TODO: dotenv or similar for contract addresses + address: ARBITRUM_RFOX_PROXY_CONTRACT_ADDRESS, + abi: stakingV1Abi, + functionName: "earned", + args: [account as Address], + blockNumber: fromBlock - 1n, // The end of the previous epoch + }), + publicClient.readContract({ + // TODO: dotenv or similar for contract addresses + address: ARBITRUM_RFOX_PROXY_CONTRACT_ADDRESS, + abi: stakingV1Abi, + functionName: "earned", + args: [account as Address], + blockNumber: toBlock, + }), + ]); + + const onChainReward = + currentTotalEarnedForAccount - previousTotalEarnedForAccount; + + assert( + calculatedReward === onChainReward, + `Expected reward for ${account} to be ${onChainReward}, got ${calculatedReward}`, + ); + + progressBar.increment(); + } + + progressBar.stop(); + + console.log("Validation passed."); +}; diff --git a/scripts/rewards-distribution/yarn.lock b/scripts/rewards-distribution/yarn.lock index c245335..3020531 100644 --- a/scripts/rewards-distribution/yarn.lock +++ b/scripts/rewards-distribution/yarn.lock @@ -227,6 +227,28 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== +"@types/cli-progress@^3.11.5": + version "3.11.5" + resolved "https://registry.yarnpkg.com/@types/cli-progress/-/cli-progress-3.11.5.tgz#9518c745e78557efda057e3f96a5990c717268c3" + integrity sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g== + dependencies: + "@types/node" "*" + +"@types/inquirer@^8.1.3": + version "8.2.10" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-8.2.10.tgz#9444dce2d764c35bc5bb4d742598aaa4acb6561b" + integrity sha512-IdD5NmHyVjWM8SHWo/kPBgtzXatwPkfwzyP3fN1jF2g9BWt5WO+8hL2F4o2GKIYsU40PpqeevuUWvkS/roXJkA== + dependencies: + "@types/through" "*" + rxjs "^7.2.0" + +"@types/node@*": + version "20.12.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.12.tgz#7cbecdf902085cec634fdb362172dfe12b8f2050" + integrity sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw== + dependencies: + undici-types "~5.26.4" + "@types/node@^20.12.4": version "20.12.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.4.tgz#af5921bd75ccdf3a3d8b3fa75bf3d3359268cd11" @@ -234,6 +256,13 @@ dependencies: undici-types "~5.26.4" +"@types/through@*": + version "0.0.33" + resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.33.tgz#14ebf599320e1c7851e7d598149af183c6b9ea56" + integrity sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ== + dependencies: + "@types/node" "*" + "@wagmi/cli@^2.1.4": version "2.1.4" resolved "https://registry.npmjs.org/@wagmi/cli/-/cli-2.1.4.tgz#a5427d3f11a52473eba1811b23b498305af03378" @@ -279,11 +308,35 @@ acorn@^8.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== +ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + ansi-regex@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + anymatch@~3.1.2: version "3.1.3" resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -307,6 +360,15 @@ binary-extensions@^2.0.0: resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + bl@^5.0.0: version "5.1.0" resolved "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz#183715f678c7188ecef9fe475d90209400624273" @@ -323,6 +385,14 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + buffer@^6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" @@ -360,6 +430,14 @@ capital-case@^1.0.4: tslib "^2.0.3" upper-case-first "^2.0.2" +chalk@^4.1.0, chalk@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^5.0.0: version "5.3.0" resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" @@ -383,6 +461,11 @@ change-case@^4.1.2: snake-case "^3.0.4" tslib "^2.0.3" +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + chokidar@^3.5.3: version "3.6.0" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -398,6 +481,13 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + cli-cursor@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" @@ -405,16 +495,40 @@ cli-cursor@^4.0.0: dependencies: restore-cursor "^4.0.0" -cli-spinners@^2.6.1: +cli-progress@^3.12.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.12.0.tgz#807ee14b66bcc086258e444ad0f19e7d42577942" + integrity sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A== + dependencies: + string-width "^4.2.3" + +cli-spinners@^2.5.0, cli-spinners@^2.6.1: version "2.9.2" resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + clone@^1.0.2: version "1.0.4" resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + constant-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1" @@ -480,6 +594,11 @@ dotenv@^16.3.1: resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + esbuild@^0.19.0: version "0.19.12" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04" @@ -509,6 +628,11 @@ esbuild@^0.19.0: "@esbuild/win32-ia32" "0.19.12" "@esbuild/win32-x64" "0.19.12" +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + execa@^8.0.1: version "8.0.1" resolved "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" @@ -524,6 +648,15 @@ execa@^8.0.1: signal-exit "^4.1.0" strip-final-newline "^3.0.0" +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + fast-glob@^3.3.0: version "3.3.2" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" @@ -542,6 +675,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -599,6 +739,11 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0: resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + header-case@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063" @@ -612,7 +757,14 @@ human-signals@^5.0.0: resolved "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== -ieee754@^1.2.1: +iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -627,6 +779,27 @@ inherits@^2.0.3, inherits@^2.0.4: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inquirer@8.2.4: + version "8.2.4" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" + integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.5.5" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + wrap-ansi "^7.0.0" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -639,6 +812,11 @@ is-extglob@^2.1.1: resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -646,6 +824,11 @@ is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + is-interactive@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz#40c57614593826da1100ade6059778d597f16e90" @@ -661,6 +844,11 @@ is-stream@^3.0.0: resolved "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-unicode-supported@^1.1.0: version "1.3.0" resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz#d824984b616c292a2e198207d4a609983842f714" @@ -697,6 +885,19 @@ locate-path@^7.1.0: dependencies: p-locate "^6.0.0" +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + log-symbols@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz#a20e3b9a5f53fac6aeb8e2bb22c07cf2c8f16d93" @@ -745,6 +946,11 @@ mimic-fn@^4.0.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + no-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" @@ -779,6 +985,21 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" +ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + ora@^6.3.1: version "6.3.1" resolved "https://registry.npmjs.org/ora/-/ora-6.3.1.tgz#a4e9e5c2cf5ee73c259e8b410273e706a2ad3ed6" @@ -794,6 +1015,11 @@ ora@^6.3.1: strip-ansi "^7.0.1" wcwidth "^1.0.1" +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + p-limit@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" @@ -893,6 +1119,14 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + restore-cursor@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" @@ -906,6 +1140,11 @@ reusify@^1.0.4: resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -913,11 +1152,23 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rxjs@^7.2.0, rxjs@^7.5.5: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + sentence-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz#3645a7b8c117c787fde8702056225bb62a45131f" @@ -969,6 +1220,15 @@ stdin-discarder@^0.1.0: dependencies: bl "^5.0.0" +string-width@^4.1.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -976,6 +1236,13 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -988,6 +1255,25 @@ strip-final-newline@^3.0.0: resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -1014,11 +1300,16 @@ ts-node@^10.9.2: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tslib@^2.0.3: +tslib@^2.0.3, tslib@^2.1.0: version "2.6.2" resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + typescript@^5.4.4: version "5.4.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.4.tgz#eb2471e7b0a5f1377523700a21669dce30c2d952" @@ -1100,6 +1391,15 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + ws@8.13.0: version "8.13.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" diff --git a/simulate-rewards-distribution.sh b/simulate-rewards-distribution.sh deleted file mode 100755 index 72206ee..0000000 --- a/simulate-rewards-distribution.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -set -ex - -forge clean --root foundry - -# Default private keys from anvil, assuming the default mnemonic -# "test test test test test test test test test test test junk" -export OWNER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -export USER_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d -export ANVIL_JSON_RPC_URL="http://127.0.0.1:8545" - -# Deploy the mock FOX token as the staking token -stakingTokenDeployOutput=$( - forge script foundry/script/DeployMockFoxToken.s.sol:DeployMockFoxToken \ - --root foundry \ - --broadcast \ - --rpc-url http://127.0.0.1:8545 \ - --private-key $OWNER_PRIVATE_KEY \ - -vvv -) -stakingTokenAddress=$(echo "$stakingTokenDeployOutput" | grep "Contract deployed at:" | awk '{print $4}') -export STAKING_TOKEN_ADDRESS=$stakingTokenAddress - -# Deploy the staking proxy -stakingProxyDeployOutput=$( - forge script foundry/script/DeployStaking.s.sol:DeployStaking \ - --root foundry \ - --broadcast \ - --rpc-url http://127.0.0.1:8545 \ - --private-key $OWNER_PRIVATE_KEY \ - -vvv -) -stakingProxyAddress=$(echo "$stakingProxyDeployOutput" | grep "Contract deployed at:" | awk '{print $4}') -export STAKING_PROXY_ADDRESS=$stakingProxyAddress - -# Run the rewards distribution simulation -ts-node scripts/rewards-distribution/index.ts diff --git a/yarn.lock b/yarn.lock index f3b1bb8..89ab5bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -198,6 +198,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +bignumber.js@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"