Skip to content

Votemarket Hooks

Your Incentives, Your Rules

Overview

Hooks automatically handle undistributed incentives at the end of campaign periods. When a campaign's max price per vote caps the reward-per-vote, or when a gauge receives no votes, the surplus rewards are sent to the hook contract for custom processing.

How Leftovers Occur

Leftover rewards are generated in two scenarios:

  1. Max price cap reached — The campaign allocates more rewards per period than what the max price per vote allows given the actual votes received. The excess becomes leftover.
  2. No votes received — The entire period allocation becomes leftover since no voters are eligible.

Hook Lifecycle

Mermaid diagram

When an epoch closes with leftover rewards, Votemarket transfers the tokens to the hook contract and calls doSomething(). The hook then processes the rewards according to its own logic.

The IHook Interface

All hook contracts must implement the following interface:

interface IHook {
    function doSomething(
        uint256 campaignId,
        uint256 chainId,
        address rewardToken,
        uint256 epoch,
        uint256 amount,
        bytes calldata hookData
    ) external;
}
ParameterDescription
campaignIdUnique identifier of the campaign
chainIdChain ID where the campaign's gauge lives
rewardTokenAddress of the leftover reward token (on the Votemarket chain)
epochThe period/epoch that just ended
amountAmount of leftover rewards being handled
hookDataArbitrary data passed for hook-specific logic. Currently unused by all built-in hooks.

Available Hooks

Votemarket ships three hooks covering the most common leftover management strategies. Each hook is whitelisted per Votemarket instance by governance.


Liquidity Mining

The Liquidity Mining hook captures surplus rewards and redirects them to liquidity providers. Leftover rewards are bridged from Arbitrum to Ethereum via LaPoste, then distributed through a Merkle-tree based distribution system to holders of the strategy (the vault share token representing LP positions) that corresponds to the campaign's gauge.

Mermaid diagram

How it works (hook-based flow)
  1. When an epoch closes with leftover rewards, Votemarket calls doSomething(). The hook validates the reward token against LaPoste's TokenFactory and stages the incentive data (campaign, token, amount, gauge) in storage.
  2. Once incentives are staged, anyone can call bridgeIncentives() to batch them and bridge via LaPoste (Chainlink CCIP) to Ethereum. This call is permissionless but requires ETH for bridging fees.
  3. On Ethereum, the CampaignRewardsDistributor (0xd4898a378ea555595c4e7dbde722b134a3f346d1) receives the tokens and distributes them through a Merkle-tree based distribution system over a configurable duration to holders of the strategy corresponding to the campaign's gauge.
Distribution Process (on Ethereum)

Once rewards land in the CampaignRewardsDistributor, an off-chain pipeline handles the actual distribution to strategy holders.

Mermaid diagram

  1. Incentive registration — When rewards arrive (via bridge or direct deposit), the CampaignRewardsDistributor stores the incentive and emits an IncentiveAdded event with the gauge, token, amount, duration, and manager.
  2. Incentive detection — An automated off-chain script (merkl-toolkit) runs every ~6 hours. It polls the contract for new incentives via nbIncentives() and incentives(i), then matches each gauge to its corresponding Stake DAO strategy (Curve v2, Balancer v2, or Pendle).
  3. TWAB calculation — For each active incentive, the script computes a Time-Weighted Average Balance (TWAB) of all vault share holders over the incentive window. It replays every Transfer event on the vault token to reconstruct balances over time, ensuring rewards are proportional to how long and how much each user held.
  4. Wrapper expansion — If a vault share holder is a wrapper contract (e.g. a Morpho vault), the script discovers the underlying depositors and runs a sub-TWAB on the wrapper's deposit/withdrawal events. The wrapper's allocation is redistributed proportionally to its depositors.
  5. Merkle tree generation — A cumulative Merkle tree is built: each new root includes all prior rewards plus the new distribution. Leaves are double-hashed (keccak256(keccak256(abi.encode(user, token, amount)))) for second preimage protection.
  6. Verification — Before publishing, the script checks that the contract balance covers all pending claims (solvency check) and simulates every claim() call on-chain to ensure no claim would revert.
  7. Root publication — The new Merkle root is set on-chain via setRoot(). Strategy holders can then claim their accumulated rewards at any time using Merkle proofs.
Direct Incentives

The CampaignRewardsDistributor also accepts direct incentives deposited on Ethereum, without going through the Votemarket hook flow. Anyone can call addIncentive() to deposit reward tokens directly for a specific gauge.

Mermaid diagram

The addIncentive() function is permissionless — any address can deposit incentives for any gauge. Each incentive specifies:

ParameterDescription
gaugeTarget gauge address for distribution
tokenReward token to distribute
amountTotal reward amount
durationDistribution period in seconds
managerAddress that receives unclaimed funds after the incentive ends

The depositor must approve the CampaignRewardsDistributor for the reward token before calling addIncentive(). Distribution starts immediately at the current block timestamp.

Configuration
ParameterSet byDescription
durationGovernanceDefault distribution period for hook-based incentives
merklDistributorGovernanceAddress of the Merkle-tree distribution contract on Ethereum
Votemarket whitelistGovernanceWhich Votemarket instances can trigger the hook
Events

IncentiveGaugeHook (L2)

EventEmitted when
IncentiveSentAn incentive is staged by doSomething()
IncentivesBridgedBatched incentives are sent via LaPoste
EnabledVotemarketA Votemarket instance is whitelisted
DisabledVotemarketA Votemarket instance is removed from whitelist

CampaignRewardsDistributor (Ethereum)

EventEmitted when
IncentiveAddedAn incentive is deposited (via bridge or direct addIncentive())
NewDurationDistribution duration is updated
NewMerklMerkle-tree distribution contract address is updated

Leftover Distribution

The simplest hook — sends unspent rewards directly to a designated recipient. Useful when a campaign manager wants leftover rewards forwarded to a specific address (e.g., a treasury, a DAO multisig, or another contract).

Mermaid diagram

How it works
  1. When doSomething() is called with leftover rewards, the hook looks up a custom recipient for that specific campaign.
  2. If no custom recipient is set, it falls back to the campaign's manager address.
  3. If neither is found, the transaction reverts.
  4. Tokens are transferred directly to the resolved recipient in a single step.
Configuration
ParameterSet byDescription
Votemarket whitelistGovernanceWhich Votemarket instances can trigger the hook
Custom recipientManager or current recipientPer-campaign destination address override

Governance can also override any recipient via overrideLeftOverRecipient().

Events
EventEmitted when
LeftOverSentLeftover rewards sent to recipient
LeftOverRecipientSetCustom recipient configured for a campaign
EnabledVotemarketA Votemarket instance is whitelisted
DisabledVotemarketA Votemarket instance is removed from whitelist

Rollover & Refund

The Rollover & Refund hook intelligently routes leftover rewards based on campaign timing. During active periods, leftovers are rolled over back into the campaign to increase its budget. At the last period(s), leftovers are refunded to the campaign manager.

Mermaid diagram

How it works
  1. When doSomething() is called with leftover rewards, the hook cannot call back into Votemarket in the same transaction due to Votemarket's nonReentrant guard. Instead, it queues the leftover data in pendingRollovers.
  2. Anyone can then call executeRollovers() permissionlessly to process the queued rollovers in a separate transaction.
  3. During execution, the hook checks the campaign's remaining periods against a configurable threshold:
    • Above threshold (active periods remain): leftovers are rolled over into the campaign via increaseTotalRewardAmount(), increasing the budget for future epochs.
    • At or below threshold (last period): leftovers are refunded directly to the campaign manager.
Configuration
ParameterSet byDescription
Votemarket whitelistGovernanceWhich Votemarket instances can trigger the hook
rolloverThresholdGovernanceGlobal default: minimum remaining periods before switching to refund (default: 1)
Per-campaign thresholdGovernanceOverride the global threshold for a specific campaign
Events
EventEmitted when
LeftoverReceivedLeftover queued in pendingRollovers (classifies as rollover or refund)
RolloverExecutedLeftover successfully rolled over into campaign budget
RefundExecutedLeftover refunded to campaign manager
ThresholdUpdatedGlobal rollover threshold changed
CampaignThresholdUpdatedPer-campaign threshold override changed

Comparison

FeatureLiquidity MiningLeftover DistributionRollover & Refund
Leftover destinationMerkle-tree distribution on EthereumCustom recipient or managerBack into campaign or manager
Cross-chainYes (Arbitrum → Ethereum)NoNo
Execution patternTwo-step (stage + bridge)Single-stepTwo-step (queue + execute)
Permissionless callsbridgeIncentives()NoneexecuteRollovers()
Best forProtocols wanting to reward their LPs with unused incentivesSimple forwarding to a treasury or multisigMaximizing campaign utilization over time

Deployments

All hooks are deployed at the same address across every supported chain.

HookAddressPolygonBaseArbitrumOptimism
Liquidity Mining0x68654D460fDF3231B49B25817cBBD72d8d291Fcf
Rollover & Refund0x9BEa5D2B38BEc04e7Bea821006798455F924fc8E
Leftover Distribution0x7a3830C1383312985cc2256F22ba6a0ce25c4304

See the Contract Addresses page for the full registry.

Security

  • Whitelisted access: Only Votemarket instances explicitly whitelisted by governance can call a hook's doSomething() function. Unauthorized contracts cannot trigger hooks.
  • Governance model: All hooks use a two-step governance transfer pattern (transferGovernance()acceptGovernance()) to prevent accidental ownership changes.
  • NonReentrant safety: Votemarket's nonReentrant modifier prevents hooks from calling back into the Votemarket contract during doSomething(). The RolloverRefundHook handles this with a queue-and-execute pattern.
  • Rescue function: All hooks include a rescueERC20() governance function for emergency token recovery.