Skip to content

Commit

Permalink
test: add agora vote example
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathandiep committed Oct 2, 2024
1 parent 895368f commit 35e786b
Show file tree
Hide file tree
Showing 9 changed files with 336 additions and 2 deletions.
1 change: 1 addition & 0 deletions examples/agora-vote/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_ALCHEMY_API_KEY=
15 changes: 15 additions & 0 deletions examples/agora-vote/hardhat.config.cjs
Original file line number Diff line number Diff line change
@@ -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],
},
},
},
};
16 changes: 16 additions & 0 deletions examples/agora-vote/package.json
Original file line number Diff line number Diff line change
@@ -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:*"
}
}
267 changes: 267 additions & 0 deletions examples/agora-vote/src/agora-vote.test.ts
Original file line number Diff line number Diff line change
@@ -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<Hex, AbiEvent>)[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') },
);
});
});
16 changes: 16 additions & 0 deletions examples/agora-vote/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
15 changes: 15 additions & 0 deletions examples/agora-vote/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -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,
},
});
3 changes: 3 additions & 0 deletions packages/sdk/src/Actions/EventAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions packages/signatures/manifests/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
4 changes: 2 additions & 2 deletions test/src/viem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) || {
Expand All @@ -35,7 +35,7 @@ export type TestClient = ReturnType<typeof makeTestClient>;
export function setupConfig(walletClient = makeTestClient()) {
return createConfig({
ssr: true,
chains: [hardhat, base, sepolia],
chains: [hardhat, base, sepolia, optimism],
client: () => walletClient,
});
}

0 comments on commit 35e786b

Please sign in to comment.