From 35e786b0002d301d18d7280468ba19b351dcae71 Mon Sep 17 00:00:00 2001 From: Jonathan Diep Date: Wed, 2 Oct 2024 14:12:17 -0700 Subject: [PATCH 1/2] test: add agora vote example --- examples/agora-vote/.env.sample | 1 + examples/agora-vote/hardhat.config.cjs | 15 ++ examples/agora-vote/package.json | 16 ++ examples/agora-vote/src/agora-vote.test.ts | 267 +++++++++++++++++++++ examples/agora-vote/tsconfig.json | 16 ++ examples/agora-vote/vitest.config.ts | 15 ++ packages/sdk/src/Actions/EventAction.ts | 3 + packages/signatures/manifests/events.json | 1 + test/src/viem.ts | 4 +- 9 files changed, 336 insertions(+), 2 deletions(-) create mode 100644 examples/agora-vote/.env.sample create mode 100644 examples/agora-vote/hardhat.config.cjs create mode 100644 examples/agora-vote/package.json create mode 100644 examples/agora-vote/src/agora-vote.test.ts create mode 100644 examples/agora-vote/tsconfig.json create mode 100644 examples/agora-vote/vitest.config.ts diff --git a/examples/agora-vote/.env.sample b/examples/agora-vote/.env.sample new file mode 100644 index 00000000..4eec7d61 --- /dev/null +++ b/examples/agora-vote/.env.sample @@ -0,0 +1 @@ +VITE_ALCHEMY_API_KEY= diff --git a/examples/agora-vote/hardhat.config.cjs b/examples/agora-vote/hardhat.config.cjs new file mode 100644 index 00000000..5ea0e5af --- /dev/null +++ b/examples/agora-vote/hardhat.config.cjs @@ -0,0 +1,15 @@ +require('dotenv').config(); +const { optimism } = require('viem/chains'); + +module.exports = { + networks: { + hardhat: { + // We might not need the mine() function if we use this code https://github.com/NomicFoundation/hardhat/pull/5394/files + chainId: optimism.id, + //hardfork: 'cancun', + forking: { + url: optimism.rpcUrls.default.http[0], + }, + }, + }, +}; diff --git a/examples/agora-vote/package.json b/examples/agora-vote/package.json new file mode 100644 index 00000000..1a1206ce --- /dev/null +++ b/examples/agora-vote/package.json @@ -0,0 +1,16 @@ +{ + "name": "agora-vote", + "version": "1.0.0", + "scripts": { + "test": "npx vitest", + "test:ci": "CI=true npx vitest" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@boostxyz/sdk": "workspace:*", + "@boostxyz/signatures": "workspace:*", + "@boostxyz/test": "workspace:*" + } +} diff --git a/examples/agora-vote/src/agora-vote.test.ts b/examples/agora-vote/src/agora-vote.test.ts new file mode 100644 index 00000000..1e9c1b66 --- /dev/null +++ b/examples/agora-vote/src/agora-vote.test.ts @@ -0,0 +1,267 @@ +import { + type ActionStep, + FilterType, + PrimitiveType, + SignatureType, +} from '@boostxyz/sdk'; +import events from '@boostxyz/signatures/events'; +import { accounts } from '@boostxyz/test/accounts'; +import { + type BudgetFixtures, + type Fixtures, + deployFixtures, + fundBudget, +} from '@boostxyz/test/helpers'; +import { setupConfig, testAccount } from '@boostxyz/test/viem'; +import { + loadFixture, + mine, + reset, +} from '@nomicfoundation/hardhat-toolbox-viem/network-helpers'; +import { + http, + type AbiEvent, + type Address, + type Hex, + createTestClient, + encodeAbiParameters, + parseEther, + publicActions, + toHex, + walletActions, +} from 'viem'; +import { optimism } from 'viem/chains'; +import { beforeAll, describe, expect, test } from 'vitest'; + +const walletClient = createTestClient({ + transport: http('http://127.0.0.1:8545'), + chain: optimism, + mode: 'hardhat', +}) + .extend(publicActions) + .extend(walletActions); + +const defaultOptions = { + account: testAccount, + config: setupConfig(walletClient), +}; + +let fixtures: Fixtures, budgets: BudgetFixtures; +// This is the Agora contract we're going to push a transaction against +const targetContract: Address = '0xcDF27F107725988f2261Ce2256bDfCdE8B382B10'; +// We take the raw inputData off of an existing historical transaction +// https://optimistic.etherscan.io/tx/0x3d281344e4d0578dfc5af517c59e87770d4ded9465456cee0bd1e93484976e88 +const inputData = + '0x5678138877d106504340c0bb50e5748cc9bd714e946816c7726ae7b15f132a7daa0705c40000000000000000000000000000000000000000000000000000000000000001'; +// This is only for a single incentive boost +const incentiveQuantity = 1; +const referrer = accounts[1].account; + +// We take the address of the imposter from the transaction above +const boostImpostor: Address = '0xc47F2266b6076b79C0a6a9906C6592b34C03c914'; +const trustedSigner = accounts[0]; +const OPT_CHAIN_BLOCK = BigInt('125541463'); +const selector = events.selectors[ + 'VoteCast(address indexed,uint256,uint8,uint256,string)' +] as Hex; + +describe('Boost with Voting Incentive', () => { + beforeAll(async () => { + await walletClient.reset({ + jsonRpcUrl: optimism.rpcUrls.default.http[0], + blockNumber: OPT_CHAIN_BLOCK - 1n, + }); + fixtures = await loadFixture(deployFixtures(defaultOptions, optimism.id)); + budgets = await loadFixture(fundBudget(defaultOptions, fixtures)); + }); + + test('should create a boost for incentivizing votes', async () => { + const { budget, erc20 } = budgets; + const { core } = fixtures; + + const owner = defaultOptions.account.address; + + // Step defining the action for VoteCast event + const eventActionStepOne: ActionStep = { + chainid: optimism.id, + signature: selector, // VoteCast event signature + signatureType: SignatureType.EVENT, // We're working with an event + actionType: 0, // Custom action type (set as 0 for now) + targetContract: targetContract, // Address of the ERC20 contract + // We want to target the ProposalId property on the VoteCast event + actionParameter: { + filterType: FilterType.EQUAL, // Filter to check for equality + fieldType: PrimitiveType.UINT, // The field we're filtering is a uint + fieldIndex: 1, // Targeting the 'proposalId' uint + filterData: toHex( + BigInt( + '54194543592303757979358957212312678549449891089859364558242427871997305750980', + ), + ), // Filtering based on the proposal id + }, + }; + + const eventActionStepTwo: ActionStep = { + chainid: optimism.id, + signature: selector, // VoteCast event signature + signatureType: SignatureType.EVENT, // We're working with an event + actionType: 0, // Custom action type (set as 0 for now) + targetContract: targetContract, // Address of the ERC20 contract + // We want to target the Support property on the VoteCast event + actionParameter: { + filterType: FilterType.EQUAL, // Filter to check for equality + fieldType: PrimitiveType.UINT, // The field we're filtering is a uint + fieldIndex: 2, // Targeting the 'support' uint + filterData: toHex(1n, { size: 1 }), // Filtering based on the support value (uint8 is 1 byte) + }, + }; + + // Define EventActionPayload manually + const eventActionPayload = { + actionClaimant: { + chainid: optimism.id, + signatureType: SignatureType.EVENT, + signature: selector, // VoteCast(address,uint256,uint8,uint256,string) event signature + fieldIndex: 0, // Targeting the 'voter' address + targetContract: targetContract, // The Agora vote contract we're monitoring + }, + actionSteps: [eventActionStepOne, eventActionStepTwo], + }; + // Initialize EventAction with the custom payload + const eventAction = core.EventAction(eventActionPayload); + // Create the boost using the custom EventAction + await core.createBoost({ + protocolFee: 1n, + referralFee: 2n, + maxParticipants: 100n, + budget: budget, // Use the ManagedBudget + action: eventAction, // Pass the manually created EventAction + validator: core.SignerValidator({ + signers: [owner, trustedSigner.account], // Whichever account we're going to sign with needs to be a signer + validatorCaller: fixtures.core.assertValidAddress(), // Only core should be calling into the validate otherwise it's possible to burn signatures + }), + allowList: core.SimpleAllowList({ + owner: owner, + allowed: [owner], + }), + incentives: [ + core.ERC20VariableIncentive({ + asset: erc20.assertValidAddress(), + reward: parseEther('0.1'), + limit: parseEther('1'), + }), + ], + }); + + // Make sure the boost was created as expected + expect(await core.getBoostCount()).toBe(1n); + const boost = await core.getBoost(0n); + const action = boost.action; + expect(action).toBeDefined(); + + // Use viem to send the transaction from the impersonated account + await walletClient.impersonateAccount({ + address: boostImpostor, + }); + await walletClient.setBalance({ + address: boostImpostor, + value: parseEther('10'), + }); + + const txHash = await walletClient.sendTransaction({ + data: inputData, + account: boostImpostor, + to: targetContract, + }); + const txReceipt = await walletClient.getTransactionReceipt({ + hash: txHash, + }); + await walletClient.mine({ blocks: 1 }); + + // Make sure that the transaction was sent as expected and validates the action + expect(txHash).toBeDefined(); + + const event = (events.abi as Record)[selector] as AbiEvent; + + if (!event) { + throw new Error(`No known ABI for given event signature: ${selector}`); + } + + const logs = await walletClient.getLogs({ + address: targetContract, + event, + fromBlock: OPT_CHAIN_BLOCK, + toBlock: 'latest', + }); + const validation = await action.validateActionSteps({ + logs, + }); + expect(validation).toBe(true); + + const amountOfVotes = await walletClient.readContract({ + address: '0xcdf27f107725988f2261ce2256bdfcde8b382b10', + abi: [ + { + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address', + }, + { + internalType: 'uint256', + name: 'blockNumber', + type: 'uint256', + }, + ], + name: 'getVotes', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + ], + functionName: 'getVotes', + args: [boostImpostor, txReceipt.blockNumber], + }); + + console.log({ amountOfVotes}) + + // If the amountOfVotes is greater than 100, then the reward should be 0.1 ETH, otherwise it will be 0.01 ETH + const rewardAmount = + amountOfVotes >= parseEther('100') + ? parseEther('0.1') + : parseEther('0.01'); + + // Generate the signature using the trusted signer + const claimDataPayload = await boost.validator.encodeClaimData({ + signer: trustedSigner, + incentiveData: encodeAbiParameters( + [{ name: '', type: 'uint256' }], + [rewardAmount], + ), + chainId: optimism.id, + incentiveQuantity, + claimant: boostImpostor, + boostId: boost.id, + }); + + + // TODO: claim with data payload for votes that aren't very much + + // Claim the incentive for the imposter + await core.claimIncentiveFor( + boost.id, + 0n, + referrer, + claimDataPayload, + boostImpostor, + { value: parseEther('0.000075') }, + ); + }); +}); diff --git a/examples/agora-vote/tsconfig.json b/examples/agora-vote/tsconfig.json new file mode 100644 index 00000000..754a8ef6 --- /dev/null +++ b/examples/agora-vote/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "rootDir": "./src", + "outDir": "dist", + "module": "Preserve", + "moduleResolution": "Bundler" + }, + "exclude": ["dist", "node_modules"], + "references": [ + { + "path": "../../test" + } + ] +} diff --git a/examples/agora-vote/vitest.config.ts b/examples/agora-vote/vitest.config.ts new file mode 100644 index 00000000..fd8cef1b --- /dev/null +++ b/examples/agora-vote/vitest.config.ts @@ -0,0 +1,15 @@ +import { loadEnv } from 'vite'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + define: { + __DEFAULT_CHAIN_ID__: 10, + }, + test: { + fileParallelism: false, + env: loadEnv('', process.cwd(), ''), + globalSetup: ['../../test/src/setup.hardhat.ts'], + hookTimeout: 30_000, + testTimeout: 15_000, + }, +}); diff --git a/packages/sdk/src/Actions/EventAction.ts b/packages/sdk/src/Actions/EventAction.ts index 3305da33..b26694a2 100644 --- a/packages/sdk/src/Actions/EventAction.ts +++ b/packages/sdk/src/Actions/EventAction.ts @@ -681,6 +681,9 @@ export class EventAction extends DeployableTarget< if (criteria.fieldType === PrimitiveType.ADDRESS) { return isAddressEqual(criteria.filterData, fieldValue as Address); } + if (criteria.fieldType === PrimitiveType.UINT) { + return BigInt(fieldValue) === BigInt(criteria.filterData); + } return fieldValue === criteria.filterData; case FilterType.NOT_EQUAL: diff --git a/packages/signatures/manifests/events.json b/packages/signatures/manifests/events.json index 36c3ed05..a8a83fec 100644 --- a/packages/signatures/manifests/events.json +++ b/packages/signatures/manifests/events.json @@ -4,6 +4,7 @@ "Purchased(address indexed,address indexed,uint256 indexed,uint256,uint256)", "NameRegistered(string,bytes32,address,uint256,uint256,uint256)", "DelegateChanged(address indexed,address indexed,address indexed)", + "VoteCast(address indexed,uint256,uint8,uint256,string)", "// Test signatures", "InfoIndexed(address indexed,string indexed)", "Info(address,string)" diff --git a/test/src/viem.ts b/test/src/viem.ts index 2e302b27..04a7b850 100644 --- a/test/src/viem.ts +++ b/test/src/viem.ts @@ -8,7 +8,7 @@ import { zeroHash, } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; -import { base, hardhat, sepolia } from 'viem/chains'; +import { base, hardhat, optimism, sepolia } from 'viem/chains'; import { accounts } from './accounts'; const { account, key } = accounts.at(0) || { @@ -35,7 +35,7 @@ export type TestClient = ReturnType; export function setupConfig(walletClient = makeTestClient()) { return createConfig({ ssr: true, - chains: [hardhat, base, sepolia], + chains: [hardhat, base, sepolia, optimism], client: () => walletClient, }); } From 32cad7dfead8636579b40eea31cadb05f716cb40 Mon Sep 17 00:00:00 2001 From: Jonathan Diep Date: Wed, 2 Oct 2024 14:25:22 -0700 Subject: [PATCH 2/2] chore: update pnpm-lock to include agora-vote --- pnpm-lock.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e320d8c..fb44a61d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,6 +90,18 @@ importers: specifier: ^2.1.1 version: 2.1.1(@types/node@20.16.10)(terser@5.31.1) + examples/agora-vote: + dependencies: + '@boostxyz/sdk': + specifier: workspace:* + version: link:../../packages/sdk + '@boostxyz/signatures': + specifier: workspace:* + version: link:../../packages/signatures + '@boostxyz/test': + specifier: workspace:* + version: link:../../test + examples/delegate-action: dependencies: '@boostxyz/sdk':