Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c45875c
[CurvePB] Enhance pool reward calculation by adding gauge and LP toke…
clement-ux Feb 6, 2026
5477540
[CurvePB] Improve CurvePoolBoosterBribesModule
clement-ux Feb 6, 2026
3cee873
[CurvePB] Make manageBribes fully configurable per pool
clement-ux Feb 9, 2026
7573f65
[CurvePB] Rename POOLS to pools
clement-ux Feb 9, 2026
94262a8
[CurvePB] Revert when removing a pool that does not exist
clement-ux Feb 9, 2026
aea558b
[CurvePB] Add duplicate check and use calldata for pool lists
clement-ux Feb 9, 2026
3d9c56d
[CurvePB] Make additionalGasLimit configurable
clement-ux Feb 9, 2026
5df3be9
[CurvePB] Use calldata for manageBribes parameters
clement-ux Feb 9, 2026
c7fef2a
[CurvePB] Add additionalGasLimit to deployment script
clement-ux Feb 9, 2026
c11f2bf
[CurvePB] Fix lint: shorten inline comment exceeding max line length
clement-ux Feb 9, 2026
affc595
[CurvePB] Update poolBooster task for new manageBribes signature
clement-ux Feb 9, 2026
84b65ef
[CurvePB] Add zero address check in _addPoolBoosterAddress
clement-ux Feb 9, 2026
b8548e7
[CurvePB] Add max value check for bridge fee
clement-ux Feb 9, 2026
99f7335
[CurvePB] Add max value check for additional gas limit
clement-ux Feb 9, 2026
245b8e9
[CurvePB] Rename length to pbCount in _manageBribes
clement-ux Feb 9, 2026
36e39d4
[CurvePB] Rename pools to poolBoosters to avoid confusion with AMM pools
clement-ux Feb 9, 2026
ac9652b
Merge branch 'master' into clement/improve-CurvePBBribeModule
clement-ux Feb 11, 2026
fed1b11
Reorder deployment number
clement-ux Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
288 changes: 189 additions & 99 deletions contracts/contracts/automation/CurvePoolBoosterBribesModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,151 +4,241 @@ pragma solidity ^0.8.0;
import { AbstractSafeModule } from "./AbstractSafeModule.sol";

interface ICurvePoolBooster {
function manageTotalRewardAmount(
uint256 bridgeFee,
function manageCampaign(
uint256 totalRewardAmount,
uint8 numberOfPeriods,
uint256 maxRewardPerVote,
uint256 additionalGasLimit
) external;

function manageNumberOfPeriods(
uint8 extraNumberOfPeriods,
uint256 bridgeFee,
uint256 additionalGasLimit
) external;

function manageRewardPerVote(
uint256 newMaxRewardPerVote,
uint256 bridgeFee,
uint256 additionalGasLimit
) external;
) external payable;
}

/// @title CurvePoolBoosterBribesModule
/// @author Origin Protocol
/// @notice Gnosis Safe module that automates the management of VotemarketV2 bribe campaigns
/// across multiple CurvePoolBooster contracts. It instructs the Safe to call `manageCampaign`
/// on each registered pool booster, forwarding ETH from the Safe's balance to cover
/// bridge fees. Campaign parameters (reward amount, duration, reward rate) can be
/// configured per pool or left to sensible defaults.
contract CurvePoolBoosterBribesModule is AbstractSafeModule {
address[] public POOLS;
////////////////////////////////////////////////////
/// --- Storage
////////////////////////////////////////////////////

/// @notice List of CurvePoolBooster addresses managed by this module
address[] public poolBoosters;

/// @notice ETH amount sent per pool booster to cover the L1 -> L2 bridge fee
uint256 public bridgeFee;

/// @notice Gas limit passed to manageCampaign for cross-chain execution
uint256 public additionalGasLimit;

////////////////////////////////////////////////////
/// --- Events
////////////////////////////////////////////////////

event BridgeFeeUpdated(uint256 newFee);
event AdditionalGasLimitUpdated(uint256 newGasLimit);
event PoolBoosterAddressAdded(address pool);
event PoolBoosterAddressRemoved(address pool);

////////////////////////////////////////////////////
/// --- Constructor
////////////////////////////////////////////////////

/// @param _safeContract Address of the Gnosis Safe this module is attached to
/// @param _operator Address authorized to call operator-restricted functions
/// @param _poolBoosters Initial list of CurvePoolBooster addresses to manage
/// @param _bridgeFee ETH amount to send per pool booster for bridge fees
/// @param _additionalGasLimit Gas limit for cross-chain execution in manageCampaign
constructor(
address _safeContract,
address _operator,
address[] memory _pools
address[] memory _poolBoosters,
uint256 _bridgeFee,
uint256 _additionalGasLimit
) AbstractSafeModule(_safeContract) {
_grantRole(OPERATOR_ROLE, _operator);
_addPoolBoosterAddress(_pools);
for (uint256 i = 0; i < _poolBoosters.length; i++) {
_addPoolBoosterAddress(_poolBoosters[i]);
}
_setBridgeFee(_bridgeFee);
_setAdditionalGasLimit(_additionalGasLimit);
}

function addPoolBoosterAddress(address[] memory pools)
////////////////////////////////////////////////////
/// --- External Mutative Functions
////////////////////////////////////////////////////

/// @notice Add new CurvePoolBooster addresses to the managed list
/// @param _poolBoosters Addresses to add
function addPoolBoosterAddress(address[] calldata _poolBoosters)
external
onlyOperator
{
_addPoolBoosterAddress(pools);
}

function _addPoolBoosterAddress(address[] memory pools) internal {
for (uint256 i = 0; i < pools.length; i++) {
POOLS.push(pools[i]);
emit PoolBoosterAddressAdded(pools[i]);
for (uint256 i = 0; i < _poolBoosters.length; i++) {
_addPoolBoosterAddress(_poolBoosters[i]);
}
}

function removePoolBoosterAddress(address[] calldata pools)
/// @notice Remove CurvePoolBooster addresses from the managed list
/// @param _poolBoosters Addresses to remove
function removePoolBoosterAddress(address[] calldata _poolBoosters)
external
onlyOperator
{
for (uint256 i = 0; i < pools.length; i++) {
_removePoolBoosterAddress(pools[i]);
for (uint256 i = 0; i < _poolBoosters.length; i++) {
_removePoolBoosterAddress(_poolBoosters[i]);
}
}

function _removePoolBoosterAddress(address pool) internal {
uint256 length = POOLS.length;
for (uint256 i = 0; i < length; i++) {
if (POOLS[i] == pool) {
POOLS[i] = POOLS[length - 1];
POOLS.pop();
emit PoolBoosterAddressRemoved(pool);
break;
}
}
/// @notice Update the ETH bridge fee sent per pool booster
/// @param newFee New bridge fee amount in wei
function setBridgeFee(uint256 newFee) external onlyOperator {
_setBridgeFee(newFee);
}

/// @notice Update the additional gas limit for cross-chain execution
/// @param newGasLimit New gas limit value
function setAdditionalGasLimit(uint256 newGasLimit) external onlyOperator {
_setAdditionalGasLimit(newGasLimit);
}

/// @notice Default entry point to manage bribe campaigns for all registered pool boosters.
/// Applies the same behavior to every pool:
/// - totalRewardAmount = type(uint256).max → use all available reward tokens
/// - numberOfPeriods = 1 → extend by one period (week)
/// - maxRewardPerVote = 0 → no update
/// @dev Calls `manageCampaign` on each pool booster via the Safe. The Safe must hold
/// enough ETH to cover `bridgeFee * poolBoosters.length`.
function manageBribes() external onlyOperator {
uint256[] memory rewardsPerVote = new uint256[](POOLS.length);
_manageBribes(rewardsPerVote);
uint256[] memory totalRewardAmounts = new uint256[](
poolBoosters.length
);
uint8[] memory extraDuration = new uint8[](poolBoosters.length);
uint256[] memory rewardsPerVote = new uint256[](poolBoosters.length);
for (uint256 i = 0; i < poolBoosters.length; i++) {
totalRewardAmounts[i] = type(uint256).max; // use all available rewards
extraDuration[i] = 1; // extend by 1 period (week)
rewardsPerVote[i] = 0; // no update to maxRewardPerVote
}
_manageBribes(totalRewardAmounts, extraDuration, rewardsPerVote);
}

function manageBribes(uint256[] memory rewardsPerVote)
external
onlyOperator
{
require(POOLS.length == rewardsPerVote.length, "Length mismatch");
_manageBribes(rewardsPerVote);
/// @notice Fully configurable entry point to manage bribe campaigns. Allows setting
/// reward amounts, durations, and reward rates individually for each pool.
/// Each array must have the same length as the poolBoosters array.
/// @param totalRewardAmounts Total reward amount per pool (0 = no update, type(uint256).max = use all available)
/// @param extraDuration Number of periods to extend per pool (0 = no update, 1 = +1 week)
/// @param rewardsPerVote Max reward per vote per pool (0 = no update)
function manageBribes(
uint256[] calldata totalRewardAmounts,
uint8[] calldata extraDuration,
uint256[] calldata rewardsPerVote
) external onlyOperator {
require(
poolBoosters.length == totalRewardAmounts.length,
"Length mismatch"
);
require(poolBoosters.length == extraDuration.length, "Length mismatch");
require(
poolBoosters.length == rewardsPerVote.length,
"Length mismatch"
);
_manageBribes(totalRewardAmounts, extraDuration, rewardsPerVote);
}

function _manageBribes(uint256[] memory rewardsPerVote)
internal
onlyOperator
{
uint256 length = POOLS.length;
for (uint256 i = 0; i < length; i++) {
address poolBoosterAddress = POOLS[i];
////////////////////////////////////////////////////
/// --- External View Functions
////////////////////////////////////////////////////

// PoolBooster need to have a balance of at least 0.003 ether to operate
// 0.001 ether are used for the bridge fee
require(
poolBoosterAddress.balance > 0.003 ether,
"Insufficient balance for bribes"
);
/// @notice Get the full list of managed CurvePoolBooster addresses
/// @return Array of pool booster addresses
function getPoolBoosters() external view returns (address[] memory) {
return poolBoosters;
}

require(
safeContract.execTransactionFromModule(
poolBoosterAddress,
0, // Value
abi.encodeWithSelector(
ICurvePoolBooster.manageNumberOfPeriods.selector,
1, // extraNumberOfPeriods
0.001 ether, // bridgeFee
1000000 // additionalGasLimit
),
0
),
"Manage number of periods failed"
);
////////////////////////////////////////////////////
/// --- Internal Functions
////////////////////////////////////////////////////

/// @notice Internal logic to add a single pool booster address
/// @dev Reverts if the address is already in the poolBoosters array
/// @param _pool Address to append to the poolBoosters array
function _addPoolBoosterAddress(address _pool) internal {
require(_pool != address(0), "Zero address");
for (uint256 j = 0; j < poolBoosters.length; j++) {
require(poolBoosters[j] != _pool, "Pool already added");
}
poolBoosters.push(_pool);
emit PoolBoosterAddressAdded(_pool);
}

require(
safeContract.execTransactionFromModule(
poolBoosterAddress,
0, // Value
abi.encodeWithSelector(
ICurvePoolBooster.manageTotalRewardAmount.selector,
0.001 ether, // bridgeFee
1000000 // additionalGasLimit
),
0
),
"Manage total reward failed"
);
/// @notice Internal logic to remove a pool booster address
/// @dev Swaps the target with the last element and pops to avoid gaps
/// @param pool Address to remove from the poolBoosters array
function _removePoolBoosterAddress(address pool) internal {
uint256 length = poolBoosters.length;
for (uint256 i = 0; i < length; i++) {
if (poolBoosters[i] == pool) {
poolBoosters[i] = poolBoosters[length - 1];
poolBoosters.pop();
emit PoolBoosterAddressRemoved(pool);
return;
}
}
revert("Pool not found");
}

/// @notice Internal logic to set the bridge fee
/// @param newFee New bridge fee amount in wei
function _setBridgeFee(uint256 newFee) internal {
require(newFee <= 0.01 ether, "Bridge fee too high");
bridgeFee = newFee;
emit BridgeFeeUpdated(newFee);
}

// Skip setting reward per vote if it's zero
if (rewardsPerVote[i] == 0) continue;
/// @notice Internal logic to set the additional gas limit
/// @param newGasLimit New gas limit value
function _setAdditionalGasLimit(uint256 newGasLimit) internal {
require(newGasLimit <= 10_000_000, "Gas limit too high");
additionalGasLimit = newGasLimit;
emit AdditionalGasLimitUpdated(newGasLimit);
}

/// @notice Internal logic to manage bribe campaigns for all registered pool boosters
/// @dev Iterates over all pool boosters and instructs the Safe to call `manageCampaign`
/// on each one, sending `bridgeFee` ETH from the Safe's balance per call.
/// @param totalRewardAmounts Total reward amount per pool (0 = no update, type(uint256).max = use all available)
/// @param extraDuration Number of periods to extend per pool (0 = no update)
/// @param rewardsPerVote Max reward per vote per pool (0 = no update)
function _manageBribes(
uint256[] memory totalRewardAmounts,
uint8[] memory extraDuration,
uint256[] memory rewardsPerVote
) internal {
uint256 pbCount = poolBoosters.length;
require(
address(safeContract).balance >= bridgeFee * pbCount,
"Not enough ETH for bridge fees"
);
for (uint256 i = 0; i < pbCount; i++) {
address poolBoosterAddress = poolBoosters[i];
require(
safeContract.execTransactionFromModule(
poolBoosterAddress,
0, // Value
bridgeFee, // ETH value to cover bridge fee
abi.encodeWithSelector(
ICurvePoolBooster.manageRewardPerVote.selector,
rewardsPerVote[i], // newMaxRewardPerVote
0.001 ether, // bridgeFee
1000000 // additionalGasLimit
ICurvePoolBooster.manageCampaign.selector,
totalRewardAmounts[i], // 0 = no update, max = use all
extraDuration[i], // numberOfPeriods, 0 = no update, 1 = +1 period (week)
rewardsPerVote[i], // maxRewardPerVote, 0 = no update
additionalGasLimit
),
0
),
"Set reward per vote failed"
"Manage campaign failed"
);
}
}

function getPools() external view returns (address[] memory) {
return POOLS;
}
}
44 changes: 44 additions & 0 deletions contracts/deploy/mainnet/173_improve_curve_pb_module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const addresses = require("../../utils/addresses");
const { deploymentWithGovernanceProposal } = require("../../utils/deploy");

module.exports = deploymentWithGovernanceProposal(
{
deployName: "173_improve_curve_pb_module",
forceDeploy: false,
reduceQueueTime: true,
deployerIsProposer: false,
proposalId: "",
},
async ({ deployWithConfirmation }) => {
const safeAddress = addresses.multichainStrategist;

const moduleName = `CurvePoolBoosterBribesModule`;
await deployWithConfirmation(
moduleName,
[
safeAddress,
// Defender Relayer
addresses.mainnet.validatorRegistrator,
[
"0xFc87E0ABe3592945Ad7587F99161dBb340faa767",
"0x1A43D2F1bb24aC262D1d7ac05D16823E526FcA32",
"0x028C6f98C20094367F7b048F0aFA1E11ce0A8DBd",
"0xc835BcA1378acb32C522f3831b8dba161a763FBE",
],
ethers.utils.parseEther("0.001"), // Bridge fee
1000000, // Additional gas limit for cross-chain execution
],
"CurvePoolBoosterBribesModule"
);
const cCurvePoolBoosterBribesModule = await ethers.getContract(moduleName);

console.log(
`${moduleName} (for ${safeAddress}) deployed to`,
cCurvePoolBoosterBribesModule.address
);

return {
actions: [],
};
}
);
Loading
Loading