From 83b361dc7aa1fa8b345db911d7cb77103653176f Mon Sep 17 00:00:00 2001 From: woody <125113430+woodenfurniture@users.noreply.github.com> Date: Thu, 30 May 2024 11:14:45 +1000 Subject: [PATCH] fix: use anvil for local rewards distribution script (#49) --- foundry/script/DeployMockFoxToken.s.sol | 15 +++ scripts/rewards-distribution/constants.ts | 16 ++- scripts/rewards-distribution/events.ts | 115 +++++++----------- scripts/rewards-distribution/index.ts | 68 +++++------ .../rewards-distribution/simulateStaking.ts | 88 +++++++------- simulate-rewards-distribution.sh | 38 ++++++ 6 files changed, 177 insertions(+), 163 deletions(-) create mode 100644 foundry/script/DeployMockFoxToken.s.sol create mode 100755 simulate-rewards-distribution.sh diff --git a/foundry/script/DeployMockFoxToken.s.sol b/foundry/script/DeployMockFoxToken.s.sol new file mode 100644 index 0000000..f5bccc8 --- /dev/null +++ b/foundry/script/DeployMockFoxToken.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import "forge-std/Script.sol"; +import {MockFOXToken} from "../test/utils/MockFOXToken.sol"; + +contract DeployMockFoxToken is Script { + function run() public { + vm.startBroadcast(); + MockFOXToken mockFoxToken = new MockFOXToken(); + vm.stopBroadcast(); + + console.log("Contract deployed at:", address(mockFoxToken)); + } +} diff --git a/scripts/rewards-distribution/constants.ts b/scripts/rewards-distribution/constants.ts index e186d6b..7f1b481 100644 --- a/scripts/rewards-distribution/constants.ts +++ b/scripts/rewards-distribution/constants.ts @@ -1,19 +1,25 @@ import { createPublicClient, createWalletClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; import { localhost } from "viem/chains"; -const ANVIL_JSON_RPC_URL = "http://127.0.0.1:8545"; - export const localChain = { ...localhost, id: 31337, } as const; -export const localWalletClient = createWalletClient({ +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 localUserWalletClient = createWalletClient({ chain: localChain, - transport: http(ANVIL_JSON_RPC_URL), + account: privateKeyToAccount(process.env.USER_PRIVATE_KEY as `0x${string}`), + transport: http(process.env.ANVIL_JSON_RPC_URL), }); export const localPublicClient = createPublicClient({ chain: localChain, - transport: http(ANVIL_JSON_RPC_URL), + transport: http(process.env.ANVIL_JSON_RPC_URL), }); diff --git a/scripts/rewards-distribution/events.ts b/scripts/rewards-distribution/events.ts index bad4690..4a87baf 100644 --- a/scripts/rewards-distribution/events.ts +++ b/scripts/rewards-distribution/events.ts @@ -1,81 +1,52 @@ -import { AbiEvent, Address, Log, parseEventLogs } from "viem"; +import { Log, getAbiItem } from "viem"; +import { stakingV1Abi } from "./generated/abi-types"; -type StakingEventName = "Stake" | "Unstake"; +export const stakeEvent = getAbiItem({ abi: stakingV1Abi, name: "Stake" }); +export const unstakeEvent = getAbiItem({ abi: stakingV1Abi, name: "Unstake" }); +export const updateCooldownPeriodEvent = getAbiItem({ + abi: stakingV1Abi, + name: "UpdateCooldownPeriod", +}); +export const withdrawEvent = getAbiItem({ + abi: stakingV1Abi, + name: "Withdraw", +}); +export const setRuneAddressEvent = getAbiItem({ + abi: stakingV1Abi, + name: "SetRuneAddress", +}); -type StakingAbiEvent = { - type: "event"; - anonymous: false; - inputs: [ - { - name: "account"; - type: "address"; - indexed: true; - }, - { - name: "amount"; - type: "uint256"; - indexed: false; - }, - // TOOD(gomes): if runeAddress is part of the staking fn, then it should be part of the Stake event too and should be reflected here - ]; - name: T; -}; +export const rFoxEvents = [ + stakeEvent, + unstakeEvent, + updateCooldownPeriodEvent, + withdrawEvent, + setRuneAddressEvent, +] as const; -type GenericStakingEventLog = Log< +export type RFoxEvent = (typeof rFoxEvents)[number]; + +export type StakeLog = Log; +export type UnstakeLog = Log; +export type UpdateCooldownPeriodLog = Log< bigint, number, false, - AbiEvent, - true, - StakingAbiEvent[], - T + typeof updateCooldownPeriodEvent, + true >; - -// explicit union of all possible event logs to ensure event args are correctly parsed by ts (fixes defaulting to unknown) -export type StakingEventLog = - | GenericStakingEventLog<"Stake"> - | GenericStakingEventLog<"Unstake">; - -const addressA: Address = "0xA"; -const addressB: Address = "0xB"; -const addressC: Address = "0xC"; -const addressD: Address = "0xD"; -const addressE: Address = "0xE"; - -export type StakingLog = Pick< - StakingEventLog, - "blockNumber" | "eventName" | "args" +export type WithdrawLog = Log; +export type SetRuneAddressLog = Log< + bigint, + number, + false, + typeof setRuneAddressEvent, + true >; -export const logs: StakingLog[] = [ - { - blockNumber: 20n, - eventName: "Stake", - args: { account: addressA, amount: 100n }, - }, - { - blockNumber: 25n, - eventName: "Stake", - args: { account: addressB, amount: 150n }, - }, - { - blockNumber: 32n, - eventName: "Stake", - args: { account: addressC, amount: 10000n }, - }, - { - blockNumber: 33n, - eventName: "Stake", - args: { account: addressD, amount: 1200n }, - }, - { - blockNumber: 60n, - eventName: "Unstake", - args: { account: addressA, amount: 100n }, - }, - { - blockNumber: 65n, - eventName: "Stake", - args: { account: addressE, amount: 500n }, - }, -]; +export type RFoxLog = + | StakeLog + | UnstakeLog + | UpdateCooldownPeriodLog + | WithdrawLog + | SetRuneAddressLog; diff --git a/scripts/rewards-distribution/index.ts b/scripts/rewards-distribution/index.ts index 1e6edca..625d1e4 100644 --- a/scripts/rewards-distribution/index.ts +++ b/scripts/rewards-distribution/index.ts @@ -1,10 +1,11 @@ -import { Address, parseAbiItem } from "viem"; -import { StakingLog } from "./events"; +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 getLogs = async ({ +const getStakingLogs = async ({ fromBlock, toBlock, }: { @@ -12,27 +13,22 @@ const getLogs = async ({ toBlock: bigint; }) => { const logs = await localPublicClient.getLogs({ - // address: '0x' - events: [ - parseAbiItem( - "event Stake(address indexed account, uint256 amount, string runeAddress)", - ), - parseAbiItem("event Unstake(address indexed user, uint256 amount)"), - ], + events: [stakeEvent, unstakeEvent], fromBlock, toBlock, }); - return logs as StakingLog[]; + return logs; }; -const getStakingAmount = (log: StakingLog): bigint => { - switch (log.eventName) { +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(log.eventName); + assertUnreachable(eventName); } throw Error("should be unreachable"); @@ -49,29 +45,30 @@ const getEpochBlockReward = (_epochEndBlockNumber: bigint) => { 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 = 5n; + const currentBlockNumber = 500n; return { - fromBlockNumber: previousEpochEndBlockNumber, - toBlockNumber: currentBlockNumber, + fromBlock: previousEpochEndBlockNumber, + toBlock: currentBlockNumber, }; }; // TODO: this should only process 1 epoch at a time const main = async () => { await simulateStaking(); - // While testing, and with the current simulation flow we only need logs from block 1 to 5 but this may change - const logs = await getLogs({ fromBlock: 0n, toBlock: 5n }); + + // 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; - }, - {}, - ); + 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; @@ -80,17 +77,10 @@ const main = async () => { // this must be initialized to empty const epochRewardByAccount: Record = {}; - // iterate all blocks for the current epoch - const { fromBlockNumber, toBlockNumber } = getEpochBlockRange(); - - const epochBlockReward = getEpochBlockReward(toBlockNumber); + const epochBlockReward = getEpochBlockReward(toBlock); - for ( - let blockNumber = fromBlockNumber; - blockNumber <= toBlockNumber; - blockNumber++ - ) { - const incomingLogs: StakingLog[] | undefined = + for (let blockNumber = fromBlock; blockNumber <= toBlock; blockNumber++) { + const incomingLogs: (StakeLog | UnstakeLog)[] | undefined = logsByBlockNumber[blockNumber.toString()]; // process logs if there are any diff --git a/scripts/rewards-distribution/simulateStaking.ts b/scripts/rewards-distribution/simulateStaking.ts index c4e0281..8b971c0 100644 --- a/scripts/rewards-distribution/simulateStaking.ts +++ b/scripts/rewards-distribution/simulateStaking.ts @@ -1,57 +1,35 @@ import { Address, formatUnits, parseUnits } from "viem"; -import { Hex } from "viem"; -import StakingV1 from "../../foundry/out/StakingV1.sol/StakingV1.json"; -import MockFOXToken from "../../foundry/out/MockFOXToken.sol/MockFOXToken.json"; -import { localPublicClient, localWalletClient } from "./constants"; +import { + localPublicClient, + localOwnerWalletClient, + localUserWalletClient, +} from "./constants"; import { stakingV1Abi, mockFoxTokenAbi } from "./generated/abi-types"; export const simulateStaking = async () => { - const walletClient = localWalletClient; + const ownerWalletClient = localOwnerWalletClient; + const userWalletClient = localUserWalletClient; const publicClient = localPublicClient; - // Deploy the MockFOXToken contract from Alice's wallet - const [alice, bob] = await walletClient.getAddresses(); - const mockFoxtokenDeployHash = await walletClient.deployContract({ - abi: mockFoxTokenAbi, - account: alice, - bytecode: MockFOXToken.bytecode.object as Hex, - }); - - const { contractAddress: mockFoxtokenContractAddress } = - await publicClient.waitForTransactionReceipt({ - hash: mockFoxtokenDeployHash, - }); - console.log(`MockFOXToken deployed to: ${mockFoxtokenContractAddress}`); + const bobRuneAddress = "thor17gw75axcnr8747pkanye45pnrwk7p9c3cqncsv"; - // Deploy the Staking contract with the address of the deployed MockFOXToken as FOX + const [bob] = await localUserWalletClient.getAddresses(); - const mockStakingDeployHash = await walletClient.deployContract({ - abi: stakingV1Abi, - account: alice, - bytecode: StakingV1.bytecode.object as Hex, - args: [], // The contructor of the Staking contract does not take any arguments - }); - - const { contractAddress: mockStakingContractAddress } = - await publicClient.waitForTransactionReceipt({ - hash: mockStakingDeployHash, - }); - - if (!mockStakingContractAddress) { - throw new Error("Staking contract address not found"); - } - console.log(`Staking deployed to: ${mockStakingContractAddress}`); + 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 as Address, + address: mockFoxtokenContractAddress, abi: mockFoxTokenAbi, functionName: "decimals", args: [], }); // Make FOX rain to Bob - const makeItRainTxHash = await walletClient.writeContract({ - address: mockFoxtokenContractAddress as Address, + const makeItRainTxHash = await ownerWalletClient.writeContract({ + address: mockFoxtokenContractAddress, abi: mockFoxTokenAbi, account: bob, functionName: "makeItRain", @@ -61,6 +39,18 @@ export const simulateStaking = async () => { 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, @@ -68,9 +58,8 @@ export const simulateStaking = async () => { ); // Approve FOX to be spent by the Staking contract - - const approveTxHash = await walletClient.writeContract({ - address: mockFoxtokenContractAddress as Address, + const approveTxHash = await userWalletClient.writeContract({ + address: mockFoxtokenContractAddress, abi: mockFoxTokenAbi, account: bob, functionName: "approve", @@ -84,22 +73,27 @@ export const simulateStaking = async () => { `Granted allowance for ${amountToStakeCryptoPrecision} FOX tokens to be spent by Staking contract: ${transactionHash}`, ); - const stakeTxHash = await walletClient.writeContract({ - address: mockStakingContractAddress as Address, + // 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, ""], // FIXME: add the runeAddress], + args: [amountToStakeCryptoBaseUnit, bobRuneAddress], }); - const { transactionHash: stakeTransactionHash } = - await publicClient.waitForTransactionReceipt({ hash: stakeTxHash }); + 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 as Address, + address: mockStakingContractAddress, abi: stakingV1Abi, functionName: "balanceOf", args: [bob], diff --git a/simulate-rewards-distribution.sh b/simulate-rewards-distribution.sh new file mode 100755 index 0000000..72206ee --- /dev/null +++ b/simulate-rewards-distribution.sh @@ -0,0 +1,38 @@ +#!/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