Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/integrations/curve/CurveFactory.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
import {IBooster} from "@interfaces/convex/IBooster.sol";
import {IStrategy} from "@interfaces/stake-dao/IStrategy.sol";
import {ILiquidityGauge} from "@interfaces/curve/ILiquidityGauge.sol";
import {IGaugeController} from "@interfaces/curve/IGaugeController.sol";
import {CurveLocker, CurveProtocol} from "@address-book/src/CurveEthereum.sol";
import {Factory} from "src/Factory.sol";
import {IRewardVault} from "src/interfaces/IRewardVault.sol";
import {ISidecarFactory} from "src/interfaces/ISidecarFactory.sol";
/// @title CurveFactory.
/// @author Stake DAO
/// @custom:github @stake-dao
/// @custom:contact contact@stakedao.org
/// @notice Factory contract for deploying Curve strategies.
contract CurveFactory is Factory {
/// @notice The bytes4 ID of the Curve protocol
/// @dev Used to identify the Curve protocol in the registry
bytes4 private constant CURVE_PROTOCOL_ID = bytes4(keccak256("CURVE"));
/// @notice Curve Gauge Controller.
address public immutable GAUGE_CONTROLLER;
/// @notice CVX token address.
address public immutable CVX;
/// @notice Address of the old strategy.
address public immutable OLD_STRATEGY;
/// @notice Convex Booster.
address public immutable BOOSTER;
/// @notice Convex Minimal Proxy Factory for Only Boost.
address public immutable CONVEX_SIDECAR_FACTORY;
/// @notice Error thrown when the set reward receiver fails.
error SetRewardReceiverFailed();
/// @notice Error thrown when the convex sidecar factory is not set.
error ConvexSidecarFactoryNotSet();
/// @notice Event emitted when a vault is deployed.
event VaultDeployed(address gauge, address vault, address rewardReceiver, address sidecar);
constructor(
address gaugeController,
address cvx,
address oldStrategy,
address booster,
address protocolController,
address vaultImplementation,
address rewardReceiverImplementation,
address rewardReceiverMigrationModule,
address rewardRouter,
address locker,
address gateway,
address convexSidecarFactory
)
Factory(protocolController, vaultImplementation, rewardReceiverImplementation, rewardReceiverMigrationModule,rewardRouter, CURVE_PROTOCOL_ID, locker, gateway)
{
GAUGE_CONTROLLER = gaugeController;
CVX = cvx;
OLD_STRATEGY = oldStrategy;
BOOSTER = booster;
CONVEX_SIDECAR_FACTORY = convexSidecarFactory;
}
/// @notice Create a new vault.
/// @param _pid Pool id.
function create(uint256 _pid) external returns (address vault, address rewardReceiver, address sidecar) {
require(CONVEX_SIDECAR_FACTORY != address(0), ConvexSidecarFactoryNotSet());
(,, address gauge,,,) = IBooster(BOOSTER).poolInfo(_pid);
/// 1. Create the vault.
(vault, rewardReceiver) = createVault(gauge);
/// 2. Attach the sidecar.
sidecar = ISidecarFactory(CONVEX_SIDECAR_FACTORY).create(gauge, abi.encode(_pid));
/// 3. Emit the event.
emit VaultDeployed(gauge, vault, rewardReceiver, sidecar);
}
function _isValidToken(address _token) internal view virtual override returns (bool) {
/// If the token is not valid, return false.
if (!super._isValidToken(_token)) return false;
/// We already add CVX to the vault by default.
if (_token == CVX) return false;
/// If the token is available as an inflation receiver, it's not valid.
try IGaugeController(GAUGE_CONTROLLER).gauge_types(_token) {
return false;
} catch {
return true;
}
}
function _isValidGauge(address _gauge) internal view virtual override returns (bool) {
bool isValid;
/// Check if the gauge is a valid candidate and available as an inflation receiver.
/// This call always reverts if the gauge is not valid.
try IGaugeController(GAUGE_CONTROLLER).gauge_types(_gauge) {
isValid = true;
} catch {
return false;
}
/// Check if the gauge is not killed.
/// Not all the pools, but most of them, have this function.
try ILiquidityGauge(_gauge).is_killed() returns (bool isKilled) {
if (isKilled) return false;
} catch {}
/// If the gauge doesn't support the is_killed function, but is unofficially killed, it can be deployed.
return isValid;
}
/// @notice Check if the gauge is shutdown in the old strategy.
/// @dev If the gauge is shutdown, we can deploy a new strategy.
function _isValidDeployment(address _gauge) internal view virtual override returns (bool) {
/// We check if the gauge is deployed in the old strategy by checking if the reward distributor is not 0.
/// We also check if the gauge is shutdown.
return
IStrategy(OLD_STRATEGY).rewardDistributors(_gauge) == address(0)
|| IStrategy(OLD_STRATEGY).isShutdown(_gauge);
}
function _getAsset(address _gauge) internal view virtual override returns (address) {
return ILiquidityGauge(_gauge).lp_token();
}
function _setupRewardTokens(address _vault, address _gauge, address _rewardReceiver) internal virtual override {
/// Add CVX to the vault if it's not already there.
if (!IRewardVault(_vault).isRewardToken(CVX)) {
IRewardVault(_vault).addRewardToken(CVX, _rewardReceiver);
}
/// Check if the gauge supports extra rewards.
/// This function is not supported on all gauges, depending on when they were deployed.
bytes memory data = abi.encodeWithSignature("reward_tokens(uint256)", 0);
(bool success,) = _gauge.call(data);
if (!success) return;
/// Loop through the extra reward tokens.
/// 8 is the maximum number of extra reward tokens supported by the gauges.
uint256 periodFinish;
address _extraRewardToken;
for (uint8 i = 0; i < 8; i++) {
/// Get the extra reward token address.
_extraRewardToken = ILiquidityGauge(_gauge).reward_tokens(i);
(,, periodFinish,,,) = ILiquidityGauge(_gauge).reward_data(_extraRewardToken);
/// If the address is 0, it means there are no more extra reward tokens.
if (_extraRewardToken == address(0)) break;
/// If the reward data is not active, skip. We allow for 30 days of inactivity.
if (periodFinish + 30 days < block.timestamp) continue;
/// If the extra reward token is already in the vault, skip.
if (IRewardVault(_vault).isRewardToken(_extraRewardToken)) continue;
/// Performs checks on the extra reward token.
/// Checks like if the token is also an lp token that can be staked in the locker, these tokens are not supported.
if (_isValidToken(_extraRewardToken)) {
/// Then we add the extra reward token to the reward distributor through the strategy.
IRewardVault(_vault).addRewardToken(_extraRewardToken, _rewardReceiver);
}
}
}
function _setRewardReceiver(address _gauge, address _rewardReceiver) internal override {
/// Set _rewardReceiver as the reward receiver on the gauge.
bytes memory data = abi.encodeWithSignature("set_rewards_receiver(address)", _rewardReceiver);
require(_executeTransaction(_gauge, data), SetRewardReceiverFailed());
}
function _initializeVault(address, address _asset, address _gauge) internal override {
/// Initialize the vault.
/// We need to approve the asset to the gauge using the Locker.
bytes memory data = abi.encodeWithSignature("approve(address,uint256)", _gauge, type(uint256).max);
/// Execute the transaction.
require(_executeTransaction(_asset, data), ApproveFailed());
}
}
"
},
"node_modules/@stake-dao/interfaces/src/interfaces/convex/IBooster.sol": {
"content": "/// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;
interface IBooster {
function poolLength() external view returns (uint256);
function poolInfo(uint256 pid)
external
view
returns (address lpToken, address token, address gauge, address crvRewards, address stash, bool shutdown);
function deposit(uint256 _pid, uint256 _amount, bool _stake) external returns (bool);
function earmarkRewards(uint256 _pid) external returns (bool);
function depositAll(uint256 _pid, bool _stake) external returns (bool);
function withdraw(uint256 _pid, uint256 _amount) external returns (bool);
function claimRewards(uint256 _pid, address gauge) external returns (bool);
}
"
},
"node_modules/@stake-dao/interfaces/src/interfaces/stake-dao/IStrategy.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;
interface IStrategy {
function locker() external view returns (address);
function deposit(address _token, uint256 amount) external;
function withdraw(address _token, uint256 amount) external;
function claimProtocolFees() external;
function claimNativeRewards() external;
function harvest(address _asset, bool _distributeSDT, bool _claimExtra) external;
function harvest(address _asset, bool _distributeSDT, bool _claimExtra, bool) external;
function rewardDistributors(address _gauge) external view returns (address);
function isShutdown(address _gauge) external view returns (bool);
function setShutdownMode(uint8 _shutdownMode) external;
function feeDistributor() external view returns (address);
/// Factory functions
function toggleVault(address vault) external;
function setGauge(address token, address gauge) external;
function setLGtype(address gauge, uint256 gaugeType) external;
function addRewardToken(address _token, address _rewardDistributor) external;
function acceptRewardDistributorOwnership(address rewardDistributor) external;
function setRewardDistributor(address gauge, address rewardDistributor) external;
function addRewardReceiver(address gauge, address rewardReceiver) external;
// Governance
function setAccumulator(address newAccumulator) external;
function setFeeRewardToken(address newFeeRewardToken) external;
function setFeeDistributor(address newFeeDistributor) external;
function setFactory(address newFactory) external;
function governance() external view returns (address);
}
"
},
"node_modules/@stake-dao/interfaces/src/interfaces/curve/ILiquidityGauge.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IL2LiquidityGauge {
function reward_data(address arg0)
external
view
returns (address distributor, uint256 period_finish, uint256 rate, uint256 last_update, uint256 integral);
function reward_tokens(uint256 arg0) external view returns (address);
function is_killed() external view returns (bool);
function lp_token() external view returns (address);
}
interface ILiquidityGauge is IERC20 {
event ApplyOwnership(address admin);
event CommitOwnership(address admin);
event Deposit(address indexed provider, uint256 value);
event UpdateLiquidityLimit(
address user, uint256 original_balance, uint256 original_supply, uint256 working_balance, uint256 working_supply
);
event Withdraw(address indexed provider, uint256 value);
function add_reward(address _reward_token, address _distributor) external;
function approve(address _spender, uint256 _value) external returns (bool);
function claim_rewards() external;
function claim_rewards(address _addr) external;
function claim_rewards(address _addr, address _receiver) external;
function claimable_tokens(address addr) external returns (uint256);
function decreaseAllowance(address _spender, uint256 _subtracted_value) external returns (bool);
function deposit(uint256 _value) external;
function deposit(uint256 _value, address _addr) external;
function deposit(uint256 _value, address _addr, bool _claim_rewards) external;
function deposit_reward_token(address _reward_token, uint256 _amount) external;
function increaseAllowance(address _spender, uint256 _added_value) external returns (bool);
function initialize(address _lp_token) external;
function kick(address addr) external;
function set_killed(bool _is_killed) external;
function set_reward_distributor(address _reward_token, address _distributor) external;
function set_rewards_receiver(address _receiver) external;
function transfer(address _to, uint256 _value) external returns (bool);
function transferFrom(address _from, address _to, uint256 _value) external returns (bool);
function user_checkpoint(address addr) external returns (bool);
function withdraw(uint256 _value) external;
function withdraw(uint256 _value, bool _claim_rewards) external;
function allowance(address arg0, address arg1) external view returns (uint256);
function balanceOf(address arg0) external view returns (uint256);
function claimable_reward(address _user, address _reward_token) external view returns (uint256);
function claimed_reward(address _addr, address _token) external view returns (uint256);
function decimals() external view returns (uint256);
function factory() external view returns (address);
function future_epoch_time() external view returns (uint256);
function inflation_rate() external view returns (uint256);
function integrate_checkpoint() external view returns (uint256);
function integrate_checkpoint_of(address arg0) external view returns (uint256);
function integrate_fraction(address arg0) external view returns (uint256);
function integrate_inv_supply(uint256 arg0) external view returns (uint256);
function integrate_inv_supply_of(address arg0) external view returns (uint256);
function is_killed() external view returns (bool);
function lp_token() external view returns (address);
function name() external view returns (string memory);
function period() external view returns (int128);
function period_timestamp(uint256 arg0) external view returns (uint256);
function reward_count() external view returns (uint256);
function reward_data(address arg0)
external
view
returns (
address token,
address distributor,
uint256 period_finish,
uint256 rate,
uint256 last_update,
uint256 integral
);
function reward_integral_for(address arg0, address arg1) external view returns (uint256);
function reward_tokens(uint256 arg0) external view returns (address);
function rewards_receiver(address arg0) external view returns (address);
function symbol() external view returns (string memory);
function totalSupply() external view returns (uint256);
function working_balances(address arg0) external view returns (uint256);
function working_supply() external view returns (uint256);
function admin() external view returns (address);
}
"
},
"node_modules/@stake-dao/interfaces/src/interfaces/curve/IGaugeController.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;
interface IGaugeController {
struct VotedSlope {
uint256 slope;
uint256 power;
uint256 end;
}
struct Points {
uint256 bias;
uint256 slope;
}
event AddType(string name, int128 type_id);
event NewGauge(address addr, int128 gauge_type, uint256 weight);
event NewGaugeWeight(address gauge_address, uint256 time, uint256 weight, uint256 total_weight);
event NewTypeWeight(int128 type_id, uint256 time, uint256 weight, uint256 total_weight);
event VoteForGauge(uint256 time, address user, address gauge_addr, uint256 weight);
function add_gauge(address addr, int128 gauge_type) external;
function add_gauge(address addr, int128 gauge_type, uint256 weight) external;
function add_type(string calldata _name) external;
function add_type(string calldata _name, uint256 weight) external;
function change_gauge_weight(address addr, uint256 weight) external;
function change_type_weight(int128 type_id, uint256 weight) external;
function checkpoint() external;
function checkpoint_gauge(address addr) external;
function gauge_relative_weight_write(address addr) external returns (uint256);
function gauge_relative_weight_write(address addr, uint256 time) external returns (uint256);
function vote_for_gauge_weights(address _gauge_addr, uint256 _user_weight) external;
function vote_for_many_gauge_weights(address[8] calldata _gauge_addrs, uint256[8] calldata _user_weight) external;
function admin() external view returns (address);
function gauge_exists(address _addr) external view returns (bool);
function gauge_relative_weight(address addr) external view returns (uint256);
function gauge_relative_weight(address addr, uint256 time) external view returns (uint256);
function gauge_type_names(int128 arg0) external view returns (string memory);
function gauge_types(address _addr) external view returns (int128);
function gauges(uint256 arg0) external view returns (address);
function get_gauge_weight(address addr) external view returns (uint256);
function get_total_weight() external view returns (uint256);
function get_type_weight(int128 type_id) external view returns (uint256);
function get_weights_sum_per_type(int128 type_id) external view returns (uint256);
function last_user_vote(address arg0, address arg1) external view returns (uint256);
function n_gauge_types() external view returns (int128);
function n_gauges() external view returns (int128);
function points_sum(int128 arg0, uint256 arg1) external view returns (Points memory);
function points_total(uint256 arg0) external view returns (uint256);
function points_type_weight(int128 arg0, uint256 arg1) external view returns (uint256);
function points_weight(address arg0, uint256 arg1) external view returns (Points memory);
function time_sum(uint256 arg0) external view returns (uint256);
function time_total() external view returns (uint256);
function time_type_weight(uint256 arg0) external view returns (uint256);
function time_weight(address arg0) external view returns (uint256);
function token() external view returns (address);
function vote_user_power(address arg0) external view returns (uint256);
function vote_user_slopes(address arg0, address arg1) external view returns (VotedSlope memory);
function voting_escrow() external view returns (address);
}
"
},
"node_modules/@stake-dao/address-book/src/CurveEthereum.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
library CurveProtocol {
address internal constant CRV = 0xD533a949740bb3306d119CC777fa900bA034cd52;
address internal constant VECRV = 0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2;
address internal constant CRV_USD = 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E;
address internal constant SD_VE_CRV = 0x478bBC744811eE8310B461514BDc29D03739084D;
address internal constant FEE_DISTRIBUTOR = 0xD16d5eC345Dd86Fb63C6a9C43c517210F1027914;
address internal constant GAUGE_CONTROLLER = 0x2F50D538606Fa9EDD2B11E2446BEb18C9D5846bB;
address internal constant SMART_WALLET_CHECKER = 0xca719728Ef172d0961768581fdF35CB116e0B7a4;
address internal constant CURVE_REGISTRY = 0xc522A6606BBA746d7960404F22a3DB936B6F4F50;
address internal constant VOTING_APP_OWNERSHIP = 0xE478de485ad2fe566d49342Cbd03E49ed7DB3356;
address internal constant VOTING_APP_PARAMETER = 0xBCfF8B0b9419b9A88c44546519b1e909cF330399;
address internal constant MINTER = 0xd061D61a4d941c39E5453435B6345Dc261C2fcE0;
address internal constant VE_BOOST = 0xD37A6aa3d8460Bd2b6536d608103D880695A23CD;
// Convex
address internal constant CONVEX_PROXY = 0x989AEb4d175e16225E39E87d0D97A3360524AD80;
address internal constant CONVEX_BOOSTER = 0xF403C135812408BFbE8713b5A23a04b3D48AAE31;
address internal constant CONVEX_TOKEN = 0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B; // CVX
address internal constant META_REGISTRY = 0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC;
}
library CurveLocker {
address internal constant TOKEN = 0xD533a949740bb3306d119CC777fa900bA034cd52;
address internal constant SDTOKEN = 0xD1b5651E55D4CeeD36251c61c50C889B36F6abB5;
address internal constant ASDTOKEN = 0x43E54C2E7b3e294De3A155785F52AB49d87B9922;
address internal constant ASDTOKEN_ADAPTER = 0x4e8DA27Fa7F109565De6FdB813D5AA1A6F73c75f;
address internal constant SYASDTOKEN = 0x18C11b1DC74cAB82AD18d5034FDe93FE90a41D99;
address internal constant LOCKER = 0x52f541764E6e90eeBc5c21Ff570De0e2D63766B6;
address internal constant DEPOSITOR = 0xa50CB9dFFcc740EE6b6f2D4B3CBc3a876b28c335;
address internal constant GAUGE = 0x7f50786A0b15723D741727882ee99a0BF34e3466;
address internal constant ACCUMULATOR = 0x11F78501e6b0cbc5DE4c7e6BBabaACdb973eb4Cd;
address internal constant VOTER = 0xb118fbE8B01dB24EdE7E87DFD19693cfca13e992;
address internal constant STRATEGY = 0x69D61428d089C2F35Bf6a472F540D0F82D1EA2cd;
address internal constant FACTORY = 0xDC9718E7704f10DB1aFaad737f8A04bcd14C20AA;
address internal constant VE_BOOST_DELEGATION = 0xe1F9C8ebBC80A013cAf0940fdD1A8554d763b9cf;
}
library CurveVotemarket {
address internal constant PLATFORM = 0x0000000895cB182E6f983eb4D8b4E0Aa0B31Ae4c;
address internal constant CURVE_CONVEX_LOCKER_VM_RECIPIENT = 0x0000000095310137125f82f37FBe5D2F99279947;
address internal constant CURVE_STAKE_DAO_LOCKER_VM_RECIPIENT = 0x0000000014814b037cF4a091FE00cbA2DeFc6115;
}
library CurveStrategy {
address internal constant ACCOUNTANT = 0x93b4B9bd266fFA8AF68e39EDFa8cFe2A62011Ce0;
address internal constant PROTOCOL_TIMELOCK = 0xb27afc7844988948FBd6210AeF4E1362bC2d8E6a;
address internal constant PROTOCOL_CONTROLLER = 0x2d8BcE1FaE00a959354aCD9eBf9174337A64d4fb;
address internal constant GATEWAY = 0xe5d6D047DF95c6627326465cB27B64A8b77A8b91;
address internal constant FEE_RECEIVER = 0x60136fefE23D269aF41aB72DE483D186dC4318D6;
address internal constant STRATEGY = 0x7D0775442d5961AE7090e4EC6C76180e8EEeEf54;
address internal constant CONVEX_SIDECAR_IMPLEMENTATION = 0x66c3ce4718A39d44CE2430eB3E8B8d43c18bA1fa;
address internal constant CONVEX_SIDECAR_FACTORY = 0x7Fa7fDb80b17f502C323D14Fa654a1e56B03C592;
address internal constant FACTORY = 0x37B015FA4Ba976c57E8e3A0084288d9DcEA06003;
address internal constant ALLOCATOR = 0x6Dbf307916Ae9c47549AbaF11Cb476252a14Ee9D;
address internal constant REWARD_VAULT_IMPLEMENTATION = 0x74D8dd40118B13B210D0a1639141cE4458CAe0c0;
address internal constant REWARD_RECEIVER_IMPLEMENTATION = 0x4E35037263f75F9fFE191B5f9B5C7cd0c3169019;
address internal constant ROUTER = 0xc3a6CfC4c8112fBfd77f0d095a0eE2f2F4505Eef;
address internal constant ROUTER_MODULE_DEPOSIT = 0xBf0a5d6a1f9A4098c69cE660F8b115dc8509b7C9;
address internal constant ROUTER_MODULE_WITHDRAW = 0xE88772DFB857317476b77F1A25b888b9424Cf63c;
address internal constant ROUTER_MODULE_CLAIM = 0xFD98cEcB88FC61101D4beBf1b6f9E65572222Ff5;
address internal constant ROUTER_MODULE_MIGRATION_CURVE = 0x0e5Ca5f4989637d480968325B716Db7A6e46466B;
address internal constant ROUTER_MODULE_MIGRATION_STAKE_DAO_V1 = 0xf0b84B9334132843fc256830Fb941d535853C120;
address internal constant ROUTER_MODULE_MIGRATION_YEARN = 0x267C77f0616d44eD6D816527974a624B2Ba65eE3;
}
"
},
"src/Factory.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {ProtocolContext} from "src/ProtocolContext.sol";
import {RewardReceiverMigrationModule} from "src/modules/RewardReceiverMigrationModule.sol";
/// @title Factory.
/// @author Stake DAO
/// @custom:github @stake-dao
/// @custom:contact contact@stakedao.org
/// @notice Factory is an abstract base contract for implementing protocol-specific vault factories.
/// It provides core functionality for creating and managing vaults across different protocols,
/// including deploying vaults and reward receivers for protocol gauges, validating gauges and tokens,
/// registering vaults with the protocol controller, and setting up reward tokens for vaults.
abstract contract Factory is ProtocolContext {
//////////////////////////////////////////////////////
// --- IMMUTABLES
//////////////////////////////////////////////////////
/// @notice Reward vault implementation address
/// @dev The implementation contract that will be cloned for each new vault
address public immutable REWARD_VAULT_IMPLEMENTATION;
/// @notice Reward receiver implementation address
/// @dev The implementation contract that will be cloned for each new reward receiver
address public immutable REWARD_RECEIVER_IMPLEMENTATION;
/// @notice Reward migration module address.
address public immutable REWARD_RECEIVER_MIGRATION_MODULE;
/// @notice Reward router address.
address public immutable REWARD_ROUTER;
//////////////////////////////////////////////////////
// --- ERRORS
//////////////////////////////////////////////////////
/// @notice Error thrown when the gauge is not a valid candidate
error InvalidGauge();
/// @notice Error thrown when the approve fails
error ApproveFailed();
/// @notice Error thrown when the token is not valid
error InvalidToken();
/// @notice Error thrown when the deployment is not valid
error InvalidDeployment();
/// @notice Error thrown when the gauge has been already used
error AlreadyDeployed();
//////////////////////////////////////////////////////
// --- EVENTS
//////////////////////////////////////////////////////
/// @notice Emitted when a new vault is deployed
/// @param vault Address of the deployed vault
/// @param asset Address of the underlying asset
/// @param gauge Address of the associated gauge
event VaultDeployed(address vault, address asset, address gauge);
//////////////////////////////////////////////////////
// --- CONSTRUCTOR
//////////////////////////////////////////////////////
/// @notice Initializes the factory with protocol controller, reward token, and vault implementation
/// @param _protocolController Address of the protocol controller
/// @param _vaultImplementation Address of the reward vault implementation
/// @param _rewardReceiverImplementation Address of the reward receiver implementation
/// @param _protocolId Protocol identifier
/// @param _locker Address of the locker
/// @param _gateway Address of the gateway
constructor(
address _protocolController,
address _vaultImplementation,
address _rewardReceiverImplementation,
address _rewardReceiverMigrationModule,
address _rewardRouter,
bytes4 _protocolId,
address _locker,
address _gateway
) ProtocolContext(_protocolId, _protocolController, _locker, _gateway) {
require(
_protocolController != address(0) && _vaultImplementation != address(0)
&& _rewardReceiverImplementation != address(0),
ZeroAddress()
);
REWARD_ROUTER = _rewardRouter;
REWARD_VAULT_IMPLEMENTATION = _vaultImplementation;
REWARD_RECEIVER_IMPLEMENTATION = _rewardReceiverImplementation;
REWARD_RECEIVER_MIGRATION_MODULE = _rewardReceiverMigrationModule;
}
//////////////////////////////////////////////////////
// --- EXTERNAL FUNCTIONS
//////////////////////////////////////////////////////
/// @notice Create a new vault for a given gauge
/// @dev Deploys a vault and reward receiver for the gauge, registers them, and sets up reward tokens
/// @param gauge Address of the gauge
/// @return vault Address of the deployed vault
/// @return rewardReceiver Address of the deployed reward receiver
/// @custom:throws InvalidGauge If the gauge is not valid
/// @custom:throws InvalidDeployment If the deployment is not valid
/// @custom:throws GaugeAlreadyUsed If the gauge has already been used
function createVault(address gauge) public virtual returns (address vault, address rewardReceiver) {
/// Perform checks on the gauge to make sure it's valid and can be used
require(_isValidGauge(gauge), InvalidGauge());
require(_isValidDeployment(gauge), InvalidDeployment());
require(PROTOCOL_CONTROLLER.vault(gauge) == address(0), AlreadyDeployed());
/// Get the asset address from the gauge
address asset = _getAsset(gauge);
/// Prepare the initialization data for the vault
/// The vault needs: gauge and asset
bytes memory data = abi.encodePacked(gauge, asset);
/// Generate a deterministic salt based on the gauge and asset
bytes32 salt = keccak256(data);
/// Clone the vault implementation with the initialization data
vault = Clones.cloneDeterministicWithImmutableArgs(REWARD_VAULT_IMPLEMENTATION, data, salt);
/// Prepare the initialization data for the reward receiver
/// The reward receiver needs: vault
data = abi.encodePacked(vault, address(0), REWARD_ROUTER);
/// Generate a deterministic salt based on the initialization data.
salt = keccak256(data);
/// Deploy Reward Receiver.
rewardReceiver = Clones.cloneDeterministicWithImmutableArgs(REWARD_RECEIVER_IMPLEMENTATION, data, salt);
/// Initialize the vault.
/// @dev Can be approval if needed etc.
_initializeVault(vault, asset, gauge);
/// Register the vault in the protocol controller
_registerVault(gauge, vault, asset, rewardReceiver);
/// Add extra reward tokens to the vault
_setupRewardTokens(vault, gauge, rewardReceiver);
/// Set the reward receiver for the gauge
_setRewardReceiver(gauge, rewardReceiver);
/// Set the valid allocation target.
PROTOCOL_CONTROLLER.setValidAllocationTarget(gauge, LOCKER);
emit VaultDeployed(vault, asset, gauge);
}
/// @notice Sync reward tokens for a gauge
/// @dev Updates the reward tokens for an existing vault
/// @param gauge Address of the gauge
/// @custom:throws InvalidGauge If the gauge is not valid or has no associated vault
function syncRewardTokens(address gauge) external {
address vault = PROTOCOL_CONTROLLER.vault(gauge);
require(vault != address(0), InvalidGauge());
/// 0. Migrate the reward receiver if the migration module is set.
if (REWARD_RECEIVER_MIGRATION_MODULE != address(0)) {
RewardReceiverMigrationModule(REWARD_RECEIVER_MIGRATION_MODULE).migrate(gauge);
}
_setupRewardTokens(vault, gauge, PROTOCOL_CONTROLLER.rewardReceiver(gauge));
}
//////////////////////////////////////////////////////
// --- INTERNAL VIRTUAL FUNCTIONS
//////////////////////////////////////////////////////
/// @notice Get the asset address from a gauge
/// @dev Must be implemented by derived factories to handle protocol-specific asset retrieval
/// @param gauge Address of the gauge
/// @return The address of the asset associated with the gauge
function _getAsset(address gauge) internal view virtual returns (address);
/// @notice Check if a deployment is valid
/// @dev Can be overridden by derived factories to add additional deployment validation
/// @return True if the deployment is valid, false otherwise
function _isValidDeployment(address) internal view virtual returns (bool) {
return true;
}
/// @notice Initialize the vault
/// @param vault Address of the vault
/// @param asset Address of the asset
/// @param gauge Address of the gauge
function _initializeVault(address vault, address asset, address gauge) internal virtual;
/// @notice Register the vault in the protocol controller
/// @param gauge Address of the gauge
/// @param vault Address of the vault
/// @param asset Address of the asset
/// @param rewardReceiver Address of the reward receiver
function _registerVault(address gauge, address vault, address asset, address rewardReceiver) internal {
PROTOCOL_CONTROLLER.registerVault(gauge, vault, asset, rewardReceiver, PROTOCOL_ID);
}
/// @notice Setup reward tokens for the vault
/// @dev Must be implemented by derived factories to handle protocol-specific reward token setup
/// @param vault Address of the vault
/// @param gauge Address of the gauge
/// @param rewardReceiver Address of the reward receiver
function _setupRewardTokens(address vault, address gauge, address rewardReceiver) internal virtual;
/// @notice Set the reward receiver for a gauge
/// @dev Must be implemented by derived factories to handle protocol-specific reward receiver setup
/// @param gauge Address of the gauge
/// @param rewardReceiver Address of the reward receiver
function _setRewardReceiver(address gauge, address rewardReceiver) internal virtual;
/// @notice Check if a gauge is valid
/// @dev Must be implemented by derived factories to handle protocol-specific gauge validation
/// @param gauge Address of the gauge
/// @return isValid True if the gauge is valid
function _isValidGauge(address gauge) internal view virtual returns (bool);
/// @notice Check if a token is valid as a reward token
/// @dev Validates that the token is not zero address and not the main reward token
/// @param token Address of the token
/// @return isValid True if the token is valid
function _isValidToken(address token) internal view virtual returns (bool) {
return token != address(0) && token != REWARD_TOKEN;
}
}
"
},
"src/interfaces/IRewardVault.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
import {IAccountant} from "src/interfaces/IAccountant.sol";
/// @title IRewardVault
/// @notice Interface for the RewardVault contract
interface IRewardVault is IERC4626 {
function addRewardToken(address rewardsToken, address distributor) external;
function depositRewards(address _rewardsToken, uint128 _amount) external;
function deposit(uint256 assets, address receiver, address referrer) external returns (uint256 shares);
function deposit(address account, address receiver, uint256 assets, address referrer)
external
returns (uint256 shares);
function claim(address[] calldata tokens, address receiver) external returns (uint256[] memory amounts);
function claim(address account, address[] calldata tokens, address receiver)
external
returns (uint256[] memory amounts);
function getRewardsDistributor(address token) external view returns (address);
function getLastUpdateTime(address token) external view returns (uint32);
function getPeriodFinish(address token) external view returns (uint32);
function getRewardRate(address token) external view returns (uint128);
function getRewardPerTokenStored(address token) external view returns (uint128);
function getRewardPerTokenPaid(address token, address account) external view returns (uint128);
function getClaimable(address token, address account) external view returns (uint128);
function getRewardTokens() external view returns (address[] memory);
function lastTimeRewardApplicable(address token) external view returns (uint256);
function rewardPerToken(address token) external view returns (uint128);
function earned(address account, address token) external view returns (uint128);
function isRewardToken(address rewardToken) external view returns (bool);
function resumeVault() external;
function gauge() external view returns (address);
function ACCOUNTANT() external view returns (IAccountant);
function checkpoint(address account) external;
function PROTOCOL_ID() external view returns (bytes4);
}
"
},
"src/interfaces/ISidecarFactory.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
interface ISidecarFactory {
function sidecar(address gauge) external view returns (address);
function create(address token, bytes memory args) external returns (address);
}
"
},
"node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
"
},
"node_modules/@openzeppelin/contracts/proxy/Clones.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.2.0) (proxy/Clones.sol)
pragma solidity ^0.8.20;
import {Create2} from "../utils/Create2.sol";
import {Errors} from "../utils/Errors.sol";
/**
* @dev https://eips.ethereum.org/EIPS/eip-1167[ERC-1167] is a standard for
* deploying minimal proxy contracts, also known as "clones".
*
* > To simply and cheaply clone contract functionality in an immutable way, this standard specifies
* > a minimal bytecode implementation that delegates all calls to a known, fixed address.
*
* The library includes functions to deploy a proxy using either `create` (traditional deployment) or `create2`
* (salted deterministic deployment). It also includes functions to predict the addresses of clones deployed using the
* deterministic method.
*/
library Clones {
error CloneArgumentsTooLong();
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create opcode, which should never revert.
*/
function clone(address implementation) internal returns (address instance) {
return clone(implementation, 0);
}
/**
* @dev Same as {xref-Clones-clone-address-}[clone], but with a `value` parameter to send native currency
* to the new contract.
*
* NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
* to always have enough balance for new deployments. Consider exposing this function under a payable method.
*/
function clone(address implementation, uint256 value) internal returns (address instance) {
if (address(this).balance < value) {
revert Errors.InsufficientBalance(address(this).balance, value);
}
assembly ("memory-safe") {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create(value, 0x09, 0x37)
}
if (instance == address(0)) {
revert Errors.FailedDeployment();
}
}
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create2 opcode and a `salt` to deterministically deploy
* the clone. Using the same `implementation` and `salt` multiple times will revert, since
* the clones cannot be deployed twice at the same address.
*/
function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
return cloneDeterministic(implementation, salt, 0);
}
/**
* @dev Same as {xref-Clones-cloneDeterministic-address-bytes32-}[cloneDeterministic], but with
* a `value` parameter to send native currency to the new contract.
*
* NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
* to always have enough balance for new deployments. Consider exposing this function under a payable method.
*/
function cloneDeterministic(
address implementation,
bytes32 salt,
uint256 value
) internal returns (address instance) {
if (address(this).balance < value) {
revert Errors.InsufficientBalance(address(this).balance, value);
}
assembly ("memory-safe") {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create2(value, 0x09, 0x37, salt)
}
if (instance == address(0)) {
revert Errors.FailedDeployment();
}
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
*/
function predictDeterministicAddress(
address implementation,
bytes32 salt,
address deployer
) internal pure returns (address predicted) {
assembly ("memory-safe") {
let ptr := mload(0x40)
mstore(add(ptr, 0x38), deployer)
mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff)
mstore(add(ptr, 0x14), implementation)
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73)
mstore(add(ptr, 0x58), salt)
mstore(add(ptr, 0x78), keccak256(add(ptr, 0x0c), 0x37))
predicted := and(keccak256(add(ptr, 0x43), 0x55), 0xffffffffffffffffffffffffffffffffffffffff)
}
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
*/
function predictDeterministicAddress(
address implementation,
bytes32 salt
) internal view returns (address predicted) {
return predictDeterministicAddress(implementation, salt, address(this));
}
/**
* @dev Deploys and returns the address of a clone that mimics the behavior of `implementation` with custom
* immutable arguments. These are provided through `args` and cannot be changed after deployment. To
* access the arguments within the implementation, use {fetchCloneArgs}.
*
* This function uses the create opcode, which should never revert.
*/
function cloneWithImmutableArgs(address implementation, bytes memory args) internal returns (address instance) {
return cloneWithImmutableArgs(implementation, args, 0);
}
/**
* @dev Same as {xref-Clones-cloneWithImmutableArgs-address-bytes-}[cloneWithImmutableArgs], but with a `value`
* parameter to send native currency to the new contract.
*
* NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
* to always have enough balance for new deployments. Consider exposing this function under a payable method.
*/
function cloneWithImmutableArgs(
address implementation,
bytes memory args,
uint256 value
) internal returns (address instance) {
if (address(this).balance < value) {
revert Errors.InsufficientBalance(address(this).balance, value);
}
bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
assembly ("memory-safe") {
instance := create(value, add(bytecode, 0x20), mload(bytecode))
}
if (instance == address(0)) {
revert Errors.FailedDeployment();
}
}
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation` with custom
* immutable arguments. These are provided through `args` and cannot be changed after deployment. To
* access the arguments within the implementation, use {fetchCloneArgs}.
*
* This function uses the create2 opcode and a `salt` to deterministically deploy the clone. Using the same
* `implementation`, `args` and `salt` multiple times will revert, since the clones cannot be deployed twice
* at the same address.
*/
function cloneDeterministicWithImmutableArgs(
address implementation,
bytes memory args,
bytes32 salt
) internal returns (address instance) {
return cloneDeterministicWithImmutableArgs(implementation, args, salt, 0);
}
/**
* @dev Same as {xref-Clones-cloneDeterministicWithImmutableArgs-address-bytes-bytes32-}[cloneDeterministicWithImmutableArgs],
* but with a `value` parameter to send native currency to the new contract.
*
* NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
* to always have enough balance for new deployments. Consider exposing this function under a payable method.
*/
function cloneDeterministicWithImmutableArgs(
address implementation,
bytes memory args,
bytes32 salt,
uint256 value
) internal returns (address instance) {
bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
return Create2.deploy(value, salt, bytecode);
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministicWithImmutableArgs}.
*/
function predictDeterministicAddressWithImmutableArgs(
address implementation,
bytes memory args,
bytes32 salt,
address deployer
) internal pure returns (address predicted) {
bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
return Create2.computeAddress(salt, keccak256(bytecode), deployer);
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministicWithImmutableArgs}.
*/
function predictDeterministicAddressWithImmutableArgs(
address implementation,
bytes memory args,
bytes32 salt
) internal view returns (address predicted) {
return predictDeterministicAddressWithImmutableArgs(implementation, args, salt, address(this));
}
/**
* @dev Get the immutable args attached to a clone.
*
* - If `instance` is a clone that was deployed using `clone` or `cloneDeterministic`, this
* function will return an empty array.
* - If `instance` is a clone that was deployed using `cloneWithImmutableArgs` or
* `cloneDeterministicWithImmutableArgs`, this function will return the args array used at
* creation.
* - If `instance` is NOT a clone deployed using this library, the behavior is undefined. This
* function should only be used to check addresses that are known to be clones.
*/
function fetchCloneArgs(address instance) internal view returns (bytes memory) {
bytes memory result = new bytes(instance.code.length - 45); // revert if length is too short
assembly ("memory-safe") {
extcodecopy(instance, add(result, 32), 45, mload(result))
}
return result;
}
/**
* @dev Helper that prepares the initcode of the proxy with immutable args.
*
* An assembly variant of this function requires copying the `args` array, which can be efficiently done using
* `mcopy`. Unfortunately, that opcode is not available before cancun. A pure solidity implementation using
* abi.encodePacked is more expensive but also more portable and easier to review.
*
* NOTE: https://eips.ethereum.org/EIPS/eip-170[EIP-170] limits the length of the contract code to 24576 bytes.
* With the proxy code taking 45 bytes, that limits the length of the immutable args to 24531 bytes.
*/
function _cloneCodeWithImmutableArgs(
address implementation,
bytes memory args
) private pure returns (bytes memory) {
if (args.length > 24531) revert CloneArgumentsTooLong();
return
abi.encodePacked(
hex"61",
uint16(args.length + 45),
hex"3d81600a3d39f3363d3d373d3d3d363d73",
implementation,
hex"5af43d82803e903d91602b57fd5bf3",
args
);
}
}
"
},
"src/ProtocolContext.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
import {IModuleManager} from "@interfaces/safe/IModuleManager.sol";
import {IAccountant} from "src/interfaces/IAccountant.sol";
import {IProtocolController} from "src/interfaces/IProtocolController.sol";
/// @title ProtocolContext.
/// @author Stake DAO
/// @custom:github @stake-dao
/// @custom:contact contact@stakedao.org
/// @notice Base contract providing shared protocol configuration and transaction execution.
contract ProtocolContext {
//////////////////////////////////////////////////////
// --- IMMUTABLES
//////////////////////////////////////////////////////
/// @notice Unique identifier for the protocol (e.g., keccak256("CURVE") for Curve)
/// @dev Used to look up protocol-specific components in ProtocolController
bytes4 public immutable PROTOCOL_ID;
/// @notice The locker contract that holds and manages protocol tokens (e.g., veCRV)
/// @dev On L2s, this may be the same as GATEWAY when no separate locker exists
address public immutable LOCKER;
/// @notice Safe multisig that owns the locker and executes privileged operations
/// @dev All protocol interactions go through this gateway for security
address public immutable GATEWAY;
/// @notice The accountant responsible for tracking rewards and user balances
/// @dev Retrieved from ProtocolController during construction
address public immutable ACCOUNTANT;
/// @notice The main reward token for this protocol (e.g., CRV for Curve)
/// @dev Retrieved from the accountant's configuration
address public immutable REWARD_TOKEN;
/// @notice Reference to the central registry for protocol components
IProtocolController public immutable PROTOCOL_CONTROLLER;
//////////////////////////////////////////////////////
// --- ERRORS
//////////////////////////////////////////////////////
/// @notice Error thrown when a required address is zero
error ZeroAddress();
/// @notice Error thrown when a protocol ID is zero
error InvalidProtocolId();
//////////////////////////////////////////////////////
// --- CONSTRUCTOR
//////////////////////////////////////////////////////
/// @notice Initializes protocol configuration that all inheriting contracts will use
/// @dev Retrieves accountant and reward token from ProtocolController for consistency
/// @param _protocolId The protocol identifier (must match registered protocol in controller)
/// @param _protocolController The protocol controller contract address
/// @param _locker The locker contract address (pass address(0) for L2s where gateway acts as locker)
/// @param _gateway The gateway contract address (Safe multisig)
/// @custom:throws ZeroAddress If protocol controller or gateway is zero
/// @custom:throws InvalidProtocolId If protocol ID is empty
constructor(bytes4 _protocolId, address _protocolController, address _locker, address _gateway) {
require(_protocolController != address(0) && _gateway != address(0), ZeroAddress());
require(_protocolId != bytes4(0), InvalidProtocolId());
GATEWAY = _gateway;
PROTOCOL_ID = _protocolId;
ACCOUNTANT = IProtocolController(_protocolController).accountant(_protocolId);
REWARD_TOKEN = IAccountant(ACCOUNTANT).REWARD_TOKEN();
PROTOCOL_CONTROLLER = IProtocolController(_protocolController);
// L2 optimization: Gateway can act as both transaction executor and token holder
if (_locker == address(0)) {
LOCKER = GATEWAY;
} else {
LOCKER = _locker;
}
}
//////////////////////////////////////////////////////
// --- INTERNAL FUNCTIONS
//////////////////////////////////////////////////////
/// @notice Executes privileged transactions through the Safe module system
/// @dev Handles two execution patterns:
/// - Mainnet: Gateway -> Locker -> Target (locker holds funds and executes)
/// - L2: Gateway acts as locker and executes directly on target
/// @param target The address of the contract to interact with
/// @param data The calldata to send to the target
/// @return success Whether the transaction executed successfully
function _executeTransaction(address target, bytes memory data) internal returns (bool success) {
if (LOCKER == GATEWAY) {
// L2 pattern: Gateway holds funds and executes directly
success = IModuleManager(GATEWAY).execTransactionFromModule(target, 0, data, IModuleManager.Operation.Call);
} else {
// Mainnet pattern: Gateway instructs locker (which holds funds) to execute
// The locker contract has the necessary approvals and balances
success = IModuleManager(GATEWAY)
.execTransactionFromModule(
LOCKER,
0,
abi.encodeWithSignature("execute(address,uint256,bytes)", target, 0, data),
IModuleManager.Operation.Call
);
}
}
/// @notice Executes privileged transactions through the Safe module system
/// @dev Handles two execution patterns:
/// - Mainnet: Gateway -> Locker -> Target (locker holds funds and executes)
/// - L2: Gateway acts as locker and executes directly on target
/// @param target The address of the contract to interact with
/// @param data The calldata to send to the target
/// @return success Whether the transaction executed successfully
function _executeTransactionReturnData(address target, bytes memory data)
internal
returns (bool success, bytes memory returnData)
{
if (LOCKER == GATEWAY) {
// L2 pattern: Gateway holds funds and executes directly
(success, returnData) = IModuleManager(GATEWAY)
.execTransactionFromModuleReturnData(target, 0, data, IModuleManager.Operation.Call);
} else {
// Mainnet pattern: Gateway instructs locker (which holds funds) to execute
// The locker contract has the necessary approvals and balances
(success, returnData) = IModuleManager(GATEWAY)
.execTransactionFromModuleReturnData(
LOCKER,
0,
abi.encodeWithSignature("execute(address,uint256,bytes)", target, 0, data),
IModuleManager.Operation.Call
);
}
}
}
"
},
"src/modules/RewardReceiverMigrationModule.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {ILiquidityGauge} from "@interfaces/curve/ILiquidityGauge.sol";
import {ProtocolContext} from "src/ProtocolContext.sol";
import {IStrategy} from "src/interfaces/IStrategy.sol";
import {IRewardReceiver} from "src/interfaces/IRewardReceiver.sol";
import {ICurveFactory} from "src/interfaces/IFactoryWithSidecar.sol";
import {ConvexSidecar} from "src/integrations/curve/ConvexSidecar.sol";
import {OnlyBoostAllocator} from "src/integrations/curve/OnlyBoostAllocator.sol";
import {ConvexSidecarFactory} from "src/integrations/curve/ConvexSidecarFactory.sol";
/// @title RewardReceiverMigrationModule.
/// @author Stake DAO
/// @custom:github @stake-dao
/// @custom:contact contact@stakedao.org
/// @notice RewardReceiverMigrationModule is a module that migrates the reward receiver from a legacy reward receiver to a new reward receiver.
contract RewardReceiverMigrationModule is ProtocolContext {
/// @notice Cache struct to avoid stack too deep errors.
struct Cache {
address vault;
address strategy;
address allocator;
address rewardReceiver;
address sidecarFactory;
address sidecar;
address asset;
address factory;
address newRewardReceiverImplementation;
}
/// @notice The reward router address.
address public immutable REWARD_ROUTER;
/// @notice The old Convex sidecar factory address.
address public immutable OLD_SIDECAR_FACTORY;
/// @notice The new Convex sidecar factory address.
address public immutable NEW_SIDECAR_FACTORY;
/// @notice Error thrown when the set reward receiver fails.
error SetRewardReceiverFailed();
/// @notice Error thrown when the set locker only fails.
error SetLockerOnlyFailed();
/// @notice Error thrown when the curve factory does not point to the expected sidecar factory.
error UnexpectedSidecarFactory(address expected, address actual);
/// @notice Emitted when the migration is completed.
event MigrationCompleted(address indexed vault, address indexed gauge, address newSidecar, address newRewardReceiver);
constructor(bytes4 _protocolId, address _protocolController, address _locker, address _gateway, address _rewardRouter, address _oldSidecarFactory, address _newSidecarFactory)
ProtocolContext(_protocolId, _protocolController, _locker, _gateway)
{
REWARD_ROUTER = _rewardRouter;
OLD_SIDECAR_FACTORY = _oldSidecarFactory;
NEW_SIDECAR_FACTORY = _newSidecarFactory;
}
/// @notice Migrates the reward receiver for a given gauge.
/// @dev Because the old ConvexSidecar implementation use reward receiver address as immutable argument, we need to migrate sidecar as well.
function migrate(address gauge) external {
/// 0a. Cache the required addresses to avoid stack too deep errors.
address factory = PROTOCOL_CONTROLLER.factory(PROTOCOL_ID);
Cache memory cache = Cache({
vault: PROTOCOL_CONTROLLER.vault(gauge),
strategy: PROTOCOL_CONTROLLER.strategy(PROTOCOL_ID),
allocator: PROTOCOL_CONTROLLER.allocator(PROTOCOL_ID),
rewardReceiver: PROTOCOL_CONTROLLER.rewardReceiver(gauge),
sidecarFactory: ICurveFactory(factory).CONVEX_SIDECAR_FACTORY(),
sidecar: ConvexSidecarFactory(OLD_SIDECAR_FACTORY).sidecar(gauge),
asset: PROTOCOL_CONTROLLER.asset(gauge),
factory: factory,
newRewardReceiverImplementation: ICurveFactory(factory).REWARD_RECEIVER_IMPLEMENTATION()
});
require (cache.sidecarFactory == NEW_SIDECAR_FACTORY, UnexpectedSidecarFactory(NEW_SIDECAR_FACTORY, cache.sidecarFactory));
address legacyRewardReceiver = _getLegacyRewardReceiver(cache.rewardReceiver);
bytes memory data = abi.encodePacked(cache.vault, legacyRewardReceiver, REWARD_ROUTER);
/// 0b. Check if the migration is possible.
if(_isAlreadyMigrated(gauge, cache.factory, cache.newRewardReceiverImplementation, data)) return;
/// 1a. Flush residual rewards if the old sidecar is not zero address.
uint pid;
address newSidecar;
if(cache.sidecar != address(0)) {
/// 1b. Flush residual rewards using `ConvexSidecar.claimExtraRewards()`
ConvexSidecar(cache.sidecar).claimExtraRewards();
/// 1c. Cache the pid.
pid = ConvexSidecar(cache.sidecar).pid();
/// 1d. Freeze Convex allocations using `OnlyBoostAllocator.setLockerOnly(gauge, true)`
require(_executeTransaction(address(cache.allocator), abi.encodeWithSelector(OnlyBoostAllocator.setLockerOnly.selector, gauge, true)), SetLockerOnlyFailed());
/// 1e. Rebalance the strategy to drain the old sidecar.
IStrategy(cache.strategy).rebalance(gauge);
/// 1f. Disable old sidecar in the ProtocolController using `removeValidAllocationTarget(gauge, oldSidecar)`
PROTOCOL_CONTROLLER.removeValidAllocationTarget(gauge, cache.sidecar);
/// 1g. Deploy new sidecar using `ConvexSidecarFactory.create(gauge, abi.e
Submitted on: 2025-10-24 18:45:50
Comments
Log in to comment.
No comments yet.