In this example, you want a certain proposal to pass/fail, and want to incentivize users to swing votes in your favor by offering additional rewards.

See the code

See technical documentation for Contract.getLogs

Make sure you’ve completed the quick start guide before proceeding with this example.

Set up a Managed Budget

First you’ll need to either initialize a new budget, or use an existing one, and allow the budget to spend a certain amount of your selected ERC20.

Define The Action

Next you’ll need to define the action that qualifies a user to claim the reward.

For Agora, we want to reward users that vote for a specific proposal on Agora. To do this, we’ll key off of the VoteCast event, which has the event signature VoteCast(address indexed,uint256,uint8,uint256,string).

There is also a VoteCastWithParams event. The SDK can only validate one at a time (for now), so this tutorial will just check for the VoteCast event.

A workaround for this is to create one boost for VoteCast and another boost for VoteCastWithParams.

'VoteCast' Event Structure. Take note of the proposalId and support fields which are at index 1 and 2.

Here is how you can structure the EventActionPayload to target the VoteCast event.

If you’re using a known event, you can use the selectors package to get the signature.

import { selectors } from '@boostxyz/signatures/events'
const selector = selectors[
  'VoteCast(address indexed,uint256,uint8,uint256,string)'
] as Hex;
import {
  EventActionPayload,
  ActionStep,
  ActionClaimant,
  SignatureType,
  FilterType,
  PrimitiveType
} from '@boostxyz/sdk/Actions/EventAction'
import { selectors } from '@boostxyz/signatures/events'
import { toHex } from 'viem'

const targetContract = '0xcdf27f107725988f2261ce2256bdfcde8b382b10'
// If one exists, use the signature from the selectors package
const selector = selectors[
  'VoteCast(address indexed,uint256,uint8,uint256,string)'
] as Hex;

// These EventAction steps outlines the criteria that the validator uses to determine eligibility for reward redemption.
const filterProposalIdStep: ActionStep = {
  chainid: optimism.id,
  signature: selector, // VoteCast event signature
  signatureType: SignatureType.EVENT, // We're working with an event
  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 filterSupportStep: ActionStep = {
  chainid: optimism.id,
  signature: selector, // VoteCast event signature
  signatureType: SignatureType.EVENT, // We're working with an event
  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)
  },
};

Next, we need to define the action claimant and create the payload for the new event action.

The eventAction payload consists of the actionClaimant and the actionSteps we defined previously. The purpose of the eventAction is to track and reward users based on their interactions with the specified event.

  1. actionClaimant: This object defines the conditions under which a user is eligible to claim rewards. It includes:

    • signatureType: Specifies that the signature type is an event.
    • signature: The event signature we are targeting, in this case, the VoteCast event.
    • fieldIndex: Indicates which field in the event data we are interested in; here, it targets the ‘voter’ address (the address that initiated the vote).
    • targetContract: The address of the contract we are monitoring for the event.
  2. actionSteps: This array can contain up to four action steps that outline the specific actions or conditions that must be met for the event action to be valid. In this example, we include the previously defined eventActionStepOne and eventActionStepTwo.

Next, we use core.EventAction to set up the event action with our constructed eventActionPayload.

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: [filterProposalIdStep, filterSupportStep] // use can place up to 4 action steps
};

// Initialize EventAction with the custom payload
const eventAction = core.EventAction(eventActionPayload);

Deploy the Boost

Once the event action is created, we can set up the incentives and deploy our boost.

// This allows for participants to be rewarded with up to 10 wei of the reward asset. 
//The maximum amount of the rewards distributed by this incentive in this boost would be 100 wei
const incentives = [
    core.ERC20VariableIncentive({
      asset: erc20.assertValidAddress(),
      reward: parseEther('0.1'),
      limit: parseEther('1'),
    }),
  ],
// Deploy the boost
const boost = await core.createBoost({
  maxParticipants: 100n, // Set a max number of participants
  budget: budget, // Use the ManagedBudget we set up earlier
  action: eventAction, // Pass the manually created EventAction
  incentives: incentives,
  allowList: core.OpenAllowList(),
});

core.OpenAllowList() makes this boost available to all addresses. If you want to limit it to a specific address, you can use a SimpleAllowList.

Claiming Incentives

Once a claimant has successfully voted, you can claim the incentive for that claimant. You will need the address of the user that cast the vote, the hash of the vote cast transaction, and the boost id. But first, you’ll need to implement some logic to determine the variable reward amount.

ERC20VariableIncentive uses off-chain logic to determine the reward amount. If you would like to base your reward on on-chain logic, you can use the ERC20VariableCriteriaIncentive type.

// Check the amount of votes the claimant has at the time they made the tx to vote
const getVotesAbi = functions.abi[functions.selectors['getVotes(address account, uint256 blockNumber) view returns (uint256)'] as '0x00000000000000000000000000000000000000000000000000000000eb9019d4']
const amountOfVotes = (await walletClient.readContract({
  address: '0xcdf27f107725988f2261ce2256bdfcde8b382b10',
  abi: [getVotesAbi],
  functionName: 'getVotes',
  args: [claimant, 127417170n], // claimant is the address of the user that cast the vote
})) as bigint;

// 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');

Once the reward amount is determined, we can generate the signature payload to allow us to claim the reward for the claimant. To generate the signature payload you will need to call the Boost /signatures api endpoint with the following params:

  • boostId: The id of the boost where the action was completed. The format is chainId:boostId (e.g. 8453:69)
  • txHash: The hash of the transaction that completed the action.
  • claimData: This is only nessecary for variableIncentives. For this parameter you would pass in the rewardAmount which is used to determine the amount of the incentive to claim. If you have more than one variable incentive you can pass in a comma-separated list of claimData values.

The signatures api will return an array of signatures, one for each available incentive on the boost.

  import axios from 'axios';

  const { data } = await axios.get('/signatures', {
    params: { 
      boostId: `${chainId}:${boost.id}`, 
      txHash,
      claimData: rewardAmount.toString(),
    }
  });

  for (const item of data) {
    const { signature, incentiveId, claimant } = item;

    // Claim the incentive for the claimant
    await core.claimIncentiveFor(
      boost.id,
      incentiveId,
      referrer,
      signature,
      claimant,
    );
  }

There can be multiple incentives to claim in a single boost. The example shown only has one incentive.

The boost will stay active until the maxParticipants set for the boost is reached, or the incentive budget is fully spent.