Skip to content

Add Safe module to help automate claim bribes #2510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
334 changes: 334 additions & 0 deletions contracts/contracts/automation/ClaimBribesSafeModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

import { AccessControlEnumerable } from "@openzeppelin/contracts/access/AccessControlEnumerable.sol";
import { ICLGauge } from "../interfaces/aerodrome/ICLGauge.sol";
import { ICLPool } from "../interfaces/aerodrome/ICLPool.sol";

struct BribePoolInfo {
address poolAddress;
address rewardContractAddress;
address[] rewardTokens;
}

interface IAerodromeVoter {
function claimBribes(
address[] memory _bribes,
address[][] memory _tokens,
uint256 _tokenId
) external;
}

interface IVeNFT {
function ownerOf(uint256 tokenId) external view returns (address);
}

interface ISafe {
function execTransactionFromModule(
address,
uint256,
bytes memory,
uint8
) external returns (bool);
}

interface ICLRewardContract {
function rewards(uint256 index) external view returns (address);

function rewardsListLength() external view returns (uint256);
}

contract ClaimBribesSafeModule is AccessControlEnumerable {
ISafe public immutable safeAddress;
IAerodromeVoter public immutable voter;
address public immutable veNFT;

uint256[] nftIds;
mapping(uint256 => uint256) nftIdIndex;

BribePoolInfo[] bribePools;
mapping(address => uint256) bribePoolIndex;

event NFTIdAdded(uint256 nftId);
event NFTIdRemoved(uint256 nftId);

event BribePoolAdded(address bribePool);
event BribePoolRemoved(address bribePool);

bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");

modifier onlySafe() {

Check warning on line 60 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L60

Added line #L60 was not covered by tests
require(
msg.sender == address(safeAddress),
"Caller is not the Gnosis Safe"
);
_;

Check warning on line 65 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L65

Added line #L65 was not covered by tests
}

modifier onlyExecutor() {

Check warning on line 68 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L68

Added line #L68 was not covered by tests
require(
hasRole(EXECUTOR_ROLE, msg.sender),
"Caller is not the Executor"
);
_;

Check warning on line 73 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L73

Added line #L73 was not covered by tests
}

constructor(

Check warning on line 76 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L76

Added line #L76 was not covered by tests
address _safeAddress,
address _voter,
address _veNFT
) {
safeAddress = ISafe(_safeAddress);
voter = IAerodromeVoter(_voter);
veNFT = _veNFT;

Check warning on line 83 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L81-L83

Added lines #L81 - L83 were not covered by tests

// Safe is the admin
_setupRole(DEFAULT_ADMIN_ROLE, _safeAddress);
_setupRole(EXECUTOR_ROLE, _safeAddress);

Check warning on line 87 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L86-L87

Added lines #L86 - L87 were not covered by tests
}

/**
* @dev Claim bribes for a range of NFTs
* @param nftIndexStart The start index of the NFTs
* @param nftIndexEnd The end index of the NFTs
* @param silent Doesn't revert if the claim fails when true
*/
function claimBribes(

Check warning on line 96 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L96

Added line #L96 was not covered by tests
uint256 nftIndexStart,
uint256 nftIndexEnd,
bool silent
) external onlyExecutor {
if (nftIndexEnd < nftIndexStart) {
(nftIndexStart, nftIndexEnd) = (nftIndexEnd, nftIndexStart);

Check warning on line 102 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L102

Added line #L102 was not covered by tests
}
uint256 nftCount = nftIds.length;

Check warning on line 104 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L104

Added line #L104 was not covered by tests
nftIndexEnd = nftCount < nftIndexEnd ? nftCount : nftIndexEnd;

(

Check warning on line 107 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L107

Added line #L107 was not covered by tests
address[] memory rewardContractAddresses,
address[][] memory rewardTokens
) = _getRewardsInfoArray();

for (uint256 i = nftIndexStart; i < nftIndexEnd; i++) {
uint256 nftId = nftIds[i];
bool success = ISafe(safeAddress).execTransactionFromModule(

Check warning on line 114 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L112-L114

Added lines #L112 - L114 were not covered by tests
address(voter),
0, // Value
abi.encodeWithSelector(
IAerodromeVoter.claimBribes.selector,
rewardContractAddresses,
rewardTokens,
nftId
),
0 // Call
);

require(success || silent, "ClaimBribes failed");
}
}

/**
* @dev Get the reward contract address and reward tokens for all pools
* @return rewardContractAddresses The reward contract addresses
* @return rewardTokens The reward tokens
*/
function _getRewardsInfoArray()

Check warning on line 135 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L135

Added line #L135 was not covered by tests
internal
view
returns (
address[] memory rewardContractAddresses,
address[][] memory rewardTokens
)
{
BribePoolInfo[] memory _bribePools = bribePools;
uint256 bribePoolCount = _bribePools.length;
rewardContractAddresses = new address[](bribePoolCount);
rewardTokens = new address[][](bribePoolCount);

Check warning on line 146 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L143-L146

Added lines #L143 - L146 were not covered by tests

for (uint256 i = 0; i < bribePoolCount; i++) {
rewardContractAddresses[i] = _bribePools[i].rewardContractAddress;
rewardTokens[i] = _bribePools[i].rewardTokens;

Check warning on line 150 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L148-L150

Added lines #L148 - L150 were not covered by tests
}
}

/***************************************
NFT Management
****************************************/
/**
* @dev Add NFT IDs to the list
* @param _nftIds The NFT IDs to add
*/
function addNFTIds(uint256[] memory _nftIds) external onlySafe {
for (uint256 i = 0; i < _nftIds.length; i++) {
uint256 nftId = _nftIds[i];

Check warning on line 163 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L162-L163

Added lines #L162 - L163 were not covered by tests
if (nftIdExists(nftId)) {
// If it already exists, skip
continue;

Check warning on line 166 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L166

Added line #L166 was not covered by tests
}

// Make sure the NFT is owned by the Safe
require(
IVeNFT(veNFT).ownerOf(nftId) == address(safeAddress),
"NFT not owned by safe"
);

nftIdIndex[nftId] = nftIds.length;
nftIds.push(nftId);

Check warning on line 176 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L175-L176

Added lines #L175 - L176 were not covered by tests

emit NFTIdAdded(nftId);

Check warning on line 178 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L178

Added line #L178 was not covered by tests
}
}

/**
* @dev Remove NFT IDs from the list
* @param _nftIds The NFT IDs to remove
*/
function removeNFTIds(uint256[] memory _nftIds) external onlySafe {
for (uint256 i = 0; i < _nftIds.length; i++) {
uint256 nftId = _nftIds[i];

Check warning on line 188 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L187-L188

Added lines #L187 - L188 were not covered by tests
if (!nftIdExists(nftId)) {
// If it doesn't exist, skip
continue;

Check warning on line 191 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L191

Added line #L191 was not covered by tests
}

uint256 index = nftIdIndex[nftId];
uint256 lastNftId = nftIds[nftIds.length - 1];
nftIds[index] = lastNftId;
nftIdIndex[lastNftId] = index;
nftIds.pop();

Check warning on line 198 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L194-L198

Added lines #L194 - L198 were not covered by tests

emit NFTIdRemoved(nftId);

Check warning on line 200 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L200

Added line #L200 was not covered by tests
}
}

/**
* @dev Check if a NFT exists on the list
* @param nftId The NFT ID to check
* @return true if the NFT ID exists, false otherwise
*/
function nftIdExists(uint256 nftId) public view returns (bool) {
uint256 index = nftIdIndex[nftId];
uint256[] memory _nftIds = nftIds;
return (index < _nftIds.length) && _nftIds[index] == nftId;

Check warning on line 212 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L209-L212

Added lines #L209 - L212 were not covered by tests
}

/**
* @dev Get the length of the nftIds list
* @return The length of the nftIds list
*/
function getNFTIdsLength() external view returns (uint256) {
return nftIds.length;

Check warning on line 220 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L219-L220

Added lines #L219 - L220 were not covered by tests
}

/**
* @dev Get all NFT IDs
* @return The NFT IDs
*/
function getAllNFTIds() external view returns (uint256[] memory) {
return nftIds;

Check warning on line 228 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L227-L228

Added lines #L227 - L228 were not covered by tests
}

/***************************************
Bribe Pool Management
****************************************/
// @dev Whitelist a pool to claim bribes from
// @param _poolAddress The address of the pool to whitelist
function addBribePool(address _poolAddress) external onlySafe {
// Find the gauge address
address _gaugeAddress = ICLPool(_poolAddress).gauge();

Check warning on line 238 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L238

Added line #L238 was not covered by tests
// And the reward contract address
address _rewardContractAddress = ICLGauge(_gaugeAddress)

Check warning on line 240 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L240

Added line #L240 was not covered by tests
.feesVotingReward();

BribePoolInfo memory bribePool = BribePoolInfo({

Check warning on line 243 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L243

Added line #L243 was not covered by tests
poolAddress: _poolAddress,
rewardContractAddress: _rewardContractAddress,
rewardTokens: _getRewardTokenAddresses(_rewardContractAddress)
});

if (bribePoolExists(_poolAddress)) {
// Update if it already exists
bribePools[bribePoolIndex[_poolAddress]] = bribePool;

Check warning on line 251 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L251

Added line #L251 was not covered by tests
} else {
// If not, Append to the list
bribePoolIndex[_poolAddress] = bribePools.length;
bribePools.push(bribePool);

Check warning on line 255 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L254-L255

Added lines #L254 - L255 were not covered by tests
}

emit BribePoolAdded(_poolAddress);

Check warning on line 258 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L258

Added line #L258 was not covered by tests
}

/**
* @dev Update the reward token addresses for all pools
*/
function updateRewardTokenAddresses() external onlyExecutor {
BribePoolInfo[] storage _bribePools = bribePools;
for (uint256 i = 0; i < _bribePools.length; i++) {
BribePoolInfo storage bribePool = _bribePools[i];
bribePool.rewardTokens = _getRewardTokenAddresses(

Check warning on line 268 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L265-L268

Added lines #L265 - L268 were not covered by tests
bribePool.rewardContractAddress
);
}
}

/**
* @dev Get the reward token addresses for a given reward contract address
* @param _rewardContractAddress The address of the reward contract
* @return _rewardTokens The reward token addresses
*/
function _getRewardTokenAddresses(address _rewardContractAddress)

Check warning on line 279 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L279

Added line #L279 was not covered by tests
internal
view
returns (address[] memory)
{
address[] memory _rewardTokens = new address[](

Check warning on line 284 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L284

Added line #L284 was not covered by tests
ICLRewardContract(_rewardContractAddress).rewardsListLength()
);
for (uint256 i = 0; i < _rewardTokens.length; i++) {
_rewardTokens[i] = ICLRewardContract(_rewardContractAddress)

Check warning on line 288 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L287-L288

Added lines #L287 - L288 were not covered by tests
.rewards(i);
}

return _rewardTokens;

Check warning on line 292 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L292

Added line #L292 was not covered by tests
}

/**
* @dev Remove a bribe pool from the list
* @param _poolAddress The address of the pool to remove
*/
function removeBribePool(address _poolAddress) external onlySafe {
if (!bribePoolExists(_poolAddress)) {
// If it doesn't exist, skip
return;

Check warning on line 302 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L302

Added line #L302 was not covered by tests
}

uint256 index = bribePoolIndex[_poolAddress];
BribePoolInfo memory lastBribePool = bribePools[bribePools.length - 1];
bribePools[index] = lastBribePool;
bribePoolIndex[lastBribePool.poolAddress] = index;
bribePools.pop();

Check warning on line 309 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L305-L309

Added lines #L305 - L309 were not covered by tests

emit BribePoolRemoved(_poolAddress);

Check warning on line 311 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L311

Added line #L311 was not covered by tests
}

/**
* @dev Check if a bribe pool exists
* @param bribePool The address of the pool to check
* @return true if the pool exists, false otherwise
*/
function bribePoolExists(address bribePool) public view returns (bool) {
BribePoolInfo[] memory _bribePools = bribePools;
uint256 poolIndex = bribePoolIndex[bribePool];
return

Check warning on line 322 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L319-L322

Added lines #L319 - L322 were not covered by tests
poolIndex < _bribePools.length &&
_bribePools[poolIndex].poolAddress == bribePool;
}

/**
* @dev Get the length of the bribe pools list
* @return The length of the bribe pools list
*/
function getBribePoolsLength() external view returns (uint256) {
return bribePools.length;

Check warning on line 332 in contracts/contracts/automation/ClaimBribesSafeModule.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/automation/ClaimBribesSafeModule.sol#L331-L332

Added lines #L331 - L332 were not covered by tests
}
}
2 changes: 2 additions & 0 deletions contracts/contracts/interfaces/aerodrome/ICLGauge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,6 @@ interface ICLGauge {
// /// @param depositor The address of the user
// /// @return The amount of positions staked in the gauge
// function stakedLength(address depositor) external view returns (uint256);

function feesVotingReward() external view returns (address);
}
Loading
Loading