diff --git a/packages/affiliates/gas-rebate/README.md b/packages/affiliates/gas-rebate/README.md new file mode 100644 index 0000000000..120db11a9d --- /dev/null +++ b/packages/affiliates/gas-rebate/README.md @@ -0,0 +1,95 @@ +# UMA Voter Gas Rebate Scripts + +This directory contains scripts for calculating gas rebates for UMA protocol voters. + +## VoterGasRebateV2.ts + +The main script for calculating gas rebates for UMA 2.0 voters. It finds all `VoteCommitted` and `VoteRevealed` events in a specified time range and calculates the gas used for each event. The script aggregates gas rebates by voter and saves the results to a JSON file. + +### How It Works + +1. Determines the previous month's date range (designed to run monthly) +2. Fetches all `VoteCommitted` and `VoteRevealed` events from the VotingV2 contract +3. Filters out voters with less than the minimum staked tokens +4. Deduplicates commit events (only the first commit per voter per round is refunded) +5. Matches commit events with corresponding reveal events +6. Calculates gas rebates (with optional priority fee cap) +7. Saves results to `rebates/Rebate_.json` + +### Usage + +```bash +# Run from the affiliates package directory +cd packages/affiliates + +# Basic usage (uses defaults, calculates for previous month) +yarn hardhat run gas-rebate/VoterGasRebateV2.ts --network mainnet + +# With custom configuration +OVERRIDE_FROM_BLOCK=18000000 \ +OVERRIDE_TO_BLOCK=18500000 \ +MIN_STAKED_TOKENS=1000 \ +yarn hardhat run gas-rebate/VoterGasRebateV2.ts --network mainnet + +# With priority fee cap (optional) +MAX_PRIORITY_FEE_GWEI=0.001 \ +yarn hardhat run gas-rebate/VoterGasRebateV2.ts --network mainnet +``` + +### Environment Variables + +| Variable | Description | Default | +| ------------------------- | ------------------------------------------------------------------------ | ----------------------------------------- | +| `OVERRIDE_FROM_BLOCK` | Start block number (overrides automatic date-based calculation) | Auto-calculated from previous month start | +| `OVERRIDE_TO_BLOCK` | End block number (overrides automatic date-based calculation) | Auto-calculated from previous month end | +| `MIN_STAKED_TOKENS` | Minimum UMA tokens staked to be eligible for rebate | `500` | +| `MAX_PRIORITY_FEE_GWEI` | Maximum priority fee to refund (in gwei). If not set, no cap is applied. | None (no cap) | +| `MAX_BLOCK_LOOK_BACK` | Maximum block range for paginated event queries | `20000` | +| `TRANSACTION_CONCURRENCY` | Number of concurrent RPC requests for fetching transactions/blocks | `50` | +| `MAX_RETRIES` | Maximum retry attempts for failed RPC calls | `10` | +| `RETRY_DELAY` | Delay between retries in milliseconds | `1000` | + +### Output Format + +The script outputs a JSON file to `rebates/Rebate_.json` with the following structure: + +```json +{ + "votingContractAddress": "0x004395edb43EFca9885CEdad51EC9fAf93Bd34ac", + "rebate": 63, + "fromBlock": 23914921, + "toBlock": 24136052, + "countVoters": 813, + "totalRebateAmount": 3.733496328767278, + "shareholderPayout": { + "0x156527BC2e57610c23Ac795A1252cAc56453e320": 0.01473407276015984, + "0x226DAce98e689118D9199246f8DfBc9115d8B034": 0.003304298094274105 + } +} +``` + +| Field | Description | +| ----------------------- | ------------------------------------------------------- | +| `votingContractAddress` | Address of the VotingV2 contract | +| `rebate` | Sequential rebate number | +| `fromBlock` | Starting block of the rebate period | +| `toBlock` | Ending block of the rebate period | +| `countVoters` | Number of voters receiving rebates | +| `totalRebateAmount` | Total ETH amount to be rebated | +| `shareholderPayout` | Map of voter addresses to their rebate amounts (in ETH) | + +### Priority Fee Capping (Optional) + +By default, the script rebates the full gas cost including any priority fee. You can optionally cap the priority fee (tip) portion to prevent rebating excessive tips by setting `MAX_PRIORITY_FEE_GWEI`. + +For example, with `MAX_PRIORITY_FEE_GWEI=0.001`: + +- If a voter paid a 0.0005 gwei priority fee, they get rebated the full 0.0005 gwei +- If a voter paid a 0.002 gwei priority fee, they only get rebated 0.001 gwei + +The base fee is always fully rebated. Enabling a cap can encourage voters to use reasonable gas settings while still covering network costs. + +## Legacy Scripts + +- **VoterGasRebate.js** - Original gas rebate script for UMA 1.0 +- **FindBlockAtTimeStamp.js** - Utility to find block numbers at specific timestamps diff --git a/packages/affiliates/gas-rebate/VoterGasRebateV2.ts b/packages/affiliates/gas-rebate/VoterGasRebateV2.ts index 50b6acf633..f8441b8c94 100644 --- a/packages/affiliates/gas-rebate/VoterGasRebateV2.ts +++ b/packages/affiliates/gas-rebate/VoterGasRebateV2.ts @@ -27,6 +27,8 @@ const { MAX_RETRIES, RETRY_DELAY, MIN_STAKED_TOKENS, + MAX_PRIORITY_FEE_GWEI, + MAX_BLOCK_LOOK_BACK, } = process.env; async function retryAsyncOperation( @@ -86,10 +88,11 @@ export async function run(): Promise { // Fetch all commit and reveal events. const voting = await hre.ethers.getContractAt("VotingV2", await getAddress("VotingV2", 1)); + const maxBlockLookBack = MAX_BLOCK_LOOK_BACK ? Number(MAX_BLOCK_LOOK_BACK) : 20000; const searchConfig = { fromBlock, toBlock, - maxBlockLookBack: 20000, + maxBlockLookBack, }; // Find resolved events @@ -169,11 +172,51 @@ export async function run(): Promise { ` for a total of ${transactionsToRefund.length} transactions` ); + // Max priority fee to refund (optional - if not set, no cap is applied) + const maxPriorityFee = MAX_PRIORITY_FEE_GWEI ? ethers.utils.parseUnits(MAX_PRIORITY_FEE_GWEI, "gwei") : null; + if (maxPriorityFee) { + console.log("Max priority fee to refund:", ethers.utils.formatUnits(maxPriorityFee, "gwei"), "gwei"); + } else { + console.log("No priority fee cap applied"); + } + + // Get unique block numbers from transactions to fetch block data + const uniqueBlockNumbers = [...new Set(transactionsToRefund.map((tx) => tx.blockNumber))]; + console.log(`Fetching base fee data for ${uniqueBlockNumbers.length} unique blocks...`); + + // Fetch block data in parallel to get base fees + const blockDataMap = new Map(); + await Bluebird.map( + uniqueBlockNumbers, + async (blockNumber) => { + const block = await retryAsyncOperation(async () => await voting.provider.getBlock(blockNumber)); + if (block.baseFeePerGas) { + blockDataMap.set(blockNumber, block.baseFeePerGas); + } + }, + { concurrency: transactionConcurrency } + ); + const shareholderPayoutBN: { [address: string]: BigNumber } = {}; // Now, traverse all transactions and calculate the rebate for each. for (const transaction of transactionsToRefund) { - // Eth used is the gas used * the gas price. - const resultantRebate = transaction.gasUsed.mul(transaction.effectiveGasPrice); + const baseFee = blockDataMap.get(transaction.blockNumber); + let effectiveGasPriceForRebate: BigNumber; + + if (baseFee) { + // Calculate the actual priority fee paid + const actualPriorityFee = transaction.effectiveGasPrice.sub(baseFee); + // Cap the priority fee at maxPriorityFee if set + const cappedPriorityFee = + maxPriorityFee && actualPriorityFee.gt(maxPriorityFee) ? maxPriorityFee : actualPriorityFee; + // Rebate = gasUsed * (baseFee + cappedPriorityFee) + effectiveGasPriceForRebate = baseFee.add(cappedPriorityFee); + } else { + // Fallback for pre-EIP-1559 transactions (shouldn't happen on mainnet post-London) + effectiveGasPriceForRebate = transaction.effectiveGasPrice; + } + + const resultantRebate = transaction.gasUsed.mul(effectiveGasPriceForRebate); // Save the output to the shareholderPayout object. Append to existing value if it exists. if (!shareholderPayoutBN[transaction.from]) shareholderPayoutBN[transaction.from] = BigNumber.from(0); shareholderPayoutBN[transaction.from] = shareholderPayoutBN[transaction.from].add(resultantRebate);