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:
- 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.
- No votes received — The entire period allocation becomes leftover since no voters are eligible.
Hook Lifecycle
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;
}| Parameter | Description |
|---|---|
campaignId | Unique identifier of the campaign |
chainId | Chain ID where the campaign's gauge lives |
rewardToken | Address of the leftover reward token (on the Votemarket chain) |
epoch | The period/epoch that just ended |
amount | Amount of leftover rewards being handled |
hookData | Arbitrary 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.
- 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. - 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. - 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.
Once rewards land in the CampaignRewardsDistributor, an off-chain pipeline handles the actual distribution to strategy holders.
- Incentive registration — When rewards arrive (via bridge or direct deposit), the CampaignRewardsDistributor stores the incentive and emits an
IncentiveAddedevent with the gauge, token, amount, duration, and manager. - Incentive detection — An automated off-chain script (merkl-toolkit) runs every ~6 hours. It polls the contract for new incentives via
nbIncentives()andincentives(i), then matches each gauge to its corresponding Stake DAO strategy (Curve v2, Balancer v2, or Pendle). - 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.
- 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.
- 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. - 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. - 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.
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.
The addIncentive() function is permissionless — any address can deposit incentives for any gauge. Each incentive specifies:
| Parameter | Description |
|---|---|
gauge | Target gauge address for distribution |
token | Reward token to distribute |
amount | Total reward amount |
duration | Distribution period in seconds |
manager | Address 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.
| Parameter | Set by | Description |
|---|---|---|
duration | Governance | Default distribution period for hook-based incentives |
merklDistributor | Governance | Address of the Merkle-tree distribution contract on Ethereum |
| Votemarket whitelist | Governance | Which Votemarket instances can trigger the hook |
IncentiveGaugeHook (L2)
| Event | Emitted when |
|---|---|
IncentiveSent | An incentive is staged by doSomething() |
IncentivesBridged | Batched incentives are sent via LaPoste |
EnabledVotemarket | A Votemarket instance is whitelisted |
DisabledVotemarket | A Votemarket instance is removed from whitelist |
CampaignRewardsDistributor (Ethereum)
| Event | Emitted when |
|---|---|
IncentiveAdded | An incentive is deposited (via bridge or direct addIncentive()) |
NewDuration | Distribution duration is updated |
NewMerkl | Merkle-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).
- When
doSomething()is called with leftover rewards, the hook looks up a custom recipient for that specific campaign. - If no custom recipient is set, it falls back to the campaign's manager address.
- If neither is found, the transaction reverts.
- Tokens are transferred directly to the resolved recipient in a single step.
| Parameter | Set by | Description |
|---|---|---|
| Votemarket whitelist | Governance | Which Votemarket instances can trigger the hook |
| Custom recipient | Manager or current recipient | Per-campaign destination address override |
Governance can also override any recipient via overrideLeftOverRecipient().
| Event | Emitted when |
|---|---|
LeftOverSent | Leftover rewards sent to recipient |
LeftOverRecipientSet | Custom recipient configured for a campaign |
EnabledVotemarket | A Votemarket instance is whitelisted |
DisabledVotemarket | A 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.
- When
doSomething()is called with leftover rewards, the hook cannot call back into Votemarket in the same transaction due to Votemarket'snonReentrantguard. Instead, it queues the leftover data inpendingRollovers. - Anyone can then call
executeRollovers()permissionlessly to process the queued rollovers in a separate transaction. - 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.
- Above threshold (active periods remain): leftovers are rolled over into the campaign via
| Parameter | Set by | Description |
|---|---|---|
| Votemarket whitelist | Governance | Which Votemarket instances can trigger the hook |
rolloverThreshold | Governance | Global default: minimum remaining periods before switching to refund (default: 1) |
| Per-campaign threshold | Governance | Override the global threshold for a specific campaign |
| Event | Emitted when |
|---|---|
LeftoverReceived | Leftover queued in pendingRollovers (classifies as rollover or refund) |
RolloverExecuted | Leftover successfully rolled over into campaign budget |
RefundExecuted | Leftover refunded to campaign manager |
ThresholdUpdated | Global rollover threshold changed |
CampaignThresholdUpdated | Per-campaign threshold override changed |
Comparison
| Feature | Liquidity Mining | Leftover Distribution | Rollover & Refund |
|---|---|---|---|
| Leftover destination | Merkle-tree distribution on Ethereum | Custom recipient or manager | Back into campaign or manager |
| Cross-chain | Yes (Arbitrum → Ethereum) | No | No |
| Execution pattern | Two-step (stage + bridge) | Single-step | Two-step (queue + execute) |
| Permissionless calls | bridgeIncentives() | None | executeRollovers() |
| Best for | Protocols wanting to reward their LPs with unused incentives | Simple forwarding to a treasury or multisig | Maximizing campaign utilization over time |
Deployments
All hooks are deployed at the same address across every supported chain.
| Hook | Address | Polygon | Base | Arbitrum | Optimism |
|---|---|---|---|---|---|
| Liquidity Mining | 0x68654D460fDF3231B49B25817cBBD72d8d291Fcf | ↗ | ↗ | ↗ | ↗ |
| Rollover & Refund | 0x9BEa5D2B38BEc04e7Bea821006798455F924fc8E | ↗ | ↗ | ↗ | ↗ |
| Leftover Distribution | 0x7a3830C1383312985cc2256F22ba6a0ce25c4304 | ↗ | ↗ | ↗ | ↗ |
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
nonReentrantmodifier prevents hooks from calling back into the Votemarket contract duringdoSomething(). The RolloverRefundHook handles this with a queue-and-execute pattern. - Rescue function: All hooks include a
rescueERC20()governance function for emergency token recovery.