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/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.encode(pid))` using the new ConvexSidecarFactory contract
newSidecar = ConvexSidecarFactory(cache.sidecarFactory).create(gauge, abi.encode(pid));
/// 1i. Reenable Convex allocations using `OnlyBoostAllocator.setLockerOnly(gauge, false)`
require(_executeTransaction(address(cache.allocator), abi.encodeWithSelector(OnlyBoostAllocator.setLockerOnly.selector, gauge, false)), SetLockerOnlyFailed());
/// 1j. Repopulate the new sidecar using `Strategy.rebalance(gauge)`
IStrategy(cache.strategy).rebalance(gauge);
}
/// 2 Generate a deterministic salt based on the initialization data.
bytes32 salt = keccak256(data);
/// 2b. Clone the reward receiver implementation with the initialization data.
address newRewardReceiver = Clones.cloneDeterministicWithImmutableArgs(cache.newRewardReceiverImplementation, data, salt);
/// 3. Update the reward receiver in the ProtocolController.
PROTOCOL_CONTROLLER.registerVault(gauge, cache.vault, cache.asset, newRewardReceiver, PROTOCOL_ID);
/// 4. Set the reward receiver for the gauge.
_setRewardReceiver(gauge, newRewardReceiver);
/// 5. Emit the migration completed event.
emit MigrationCompleted(cache.vault, gauge, newSidecar, newRewardReceiver);
}
/// @notice Checks if the migration is possible.
/// @dev The migration is onlu possible if the gauge was not already migrated.
/// It should not revert. This function is used in the Factory to migrate before syncing extra rewards.
function _isAlreadyMigrated(address gauge, address factory, address newRewardReceiverImplementation, bytes memory data) internal view returns (bool) {
return PROTOCOL_CONTROLLER.rewardReceiver(gauge) == getRewardReceiverAddress(newRewardReceiverImplementation, factory, data)
|| PROTOCOL_CONTROLLER.rewardReceiver(gauge) == getRewardReceiverAddress(newRewardReceiverImplementation, address(this), data);
}
/// @notice Computes the reward receiver address for a given deployer and data.
/// @param deployer The deployer of the reward receiver (factory or module).
/// @return rewardReceiver The address of the reward receiver.
function getRewardReceiverAddress(address newRewardReceiverImplementation, address deployer, bytes memory data) internal pure returns (address rewardReceiver) {
bytes32 salt = keccak256(data);
return Clones.predictDeterministicAddressWithImmutableArgs(newRewardReceiverImplementation, data, salt, deployer);
}
/// @notice Computes the legacy reward receiver address for a given current reward receiver.
/// @param current The current reward receiver address.
/// @return legacy The legacy reward receiver address.
function _getLegacyRewardReceiver(address current) internal view returns (address legacy) {
if (current == address(0)) return current;
try IRewardReceiver(current).legacyRewardReceiver() returns (address _legacy) {
legacy = _legacy;
} catch {
return current;
}
}
/// @notice Sets the reward receiver for a gauge.
/// @param _gauge The gauge to set the reward receiver for.
/// @param _rewardReceiver The reward receiver to set.
function _setRewardReceiver(address _gauge, address _rewardReceiver) internal {
/// Set _rewardReceiver as the reward receiver on the gauge.
bytes memory data = abi.encodeWithSignature("set_rewards_receiver(address)", _rewardReceiver);
require(_executeTransaction(_gauge, data), SetRewardReceiverFailed());
}
}
"
},
"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
);
}
}
"
},
"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);
}
"
},
"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/interfaces/IStrategy.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
import "src/interfaces/IAllocator.sol";
interface IStrategy {
/// @notice The policy for harvesting rewards.
enum HarvestPolicy {
CHECKPOINT,
HARVEST
}
struct PendingRewards {
uint128 feeSubjectAmount;
uint128 totalAmount;
}
function deposit(IAllocator.Allocation calldata allocation, HarvestPolicy policy)
external
returns (PendingRewards memory pendingRewards);
function withdraw(IAllocator.Allocation calldata allocation, HarvestPolicy policy, address receiver)
external
returns (PendingRewards memory pendingRewards);
function balanceOf(address gauge) external view returns (uint256 balance);
function harvest(address gauge, bytes calldata extraData) external returns (PendingRewards memory pendingRewards);
function flush() external;
function shutdown(address gauge) external;
function rebalance(address gauge) external;
}
"
},
"src/interfaces/IRewardReceiver.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
interface IRewardReceiver {
function distributeRewards() external;
function distributeRewardToken(IERC20 token) external;
function pullRewards(address token) external returns (uint128 amount);
function legacyRewardReceiver() external view returns (address);
}
"
},
"src/interfaces/IFactoryWithSidecar.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
import {IFactory} from 'src/interfaces/IFactory.sol';
/// @title IFactoryWithSidecar.
/// @author Stake DAO
/// @custom:github @stake-dao
/// @custom:contact contact@stakedao.org
/// @notice Interface for factories that support Convex sidecar functionality.
interface ICurveFactory is IFactory {
/// @notice Convex sidecar factory address
function CONVEX_SIDECAR_FACTORY() external view returns (address);
}
"
},
"src/integrations/curve/ConvexSidecar.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
/// Interfaces
import {IBooster} from "@interfaces/convex/IBooster.sol";
import {IBaseRewardPool} from "@interfaces/convex/IBaseRewardPool.sol";
import {IStashTokenWrapper} from "@interfaces/convex/IStashTokenWrapper.sol";
/// OpenZeppelin
import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
/// Project Contracts
import {Sidecar} from "src/Sidecar.sol";
import {ImmutableArgsParser} from "src/libraries/ImmutableArgsParser.sol";
/// @title ConvexSidecar.
/// @author Stake DAO
/// @custom:github @stake-dao
/// @custom:contact contact@stakedao.org
/// @notice Sidecar for managing Convex rewards and deposits.
contract ConvexSidecar is Sidecar {
using SafeERC20 for IERC20;
using ImmutableArgsParser for address;
/// @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"));
//////////////////////////////////////////////////////
// --- IMPLEMENTATION CONSTANTS
//////////////////////////////////////////////////////
/// @notice Convex Reward Token address.
IERC20 public immutable CVX;
/// @notice Convex Booster address.
address public immutable BOOSTER;
/// @notice Thrown when the reward receiver is not set in the protocol controller.
error RewardReceiverNotSet();
//////////////////////////////////////////////////////
// --- ISIDECAR CLONE IMMUTABLES
//////////////////////////////////////////////////////
/// @notice Staking token address.
function asset() public view override returns (IERC20 _asset) {
return IERC20(address(this).readAddress(0));
}
/// @notice Curve gauge associated with the sidecar.
function gauge() public view returns (address _gauge) {
return address(this).readAddress(20);
}
function rewardReceiver() public view override returns (address _rewardReceiver) {
_rewardReceiver = PROTOCOL_CONTROLLER.rewardReceiver(gauge());
if (_rewardReceiver == address(0)) revert RewardReceiverNotSet();
}
//////////////////////////////////////////////////////
// --- CONVEX CLONE IMMUTABLES
//////////////////////////////////////////////////////
/// @notice Staking Convex LP contract address.
function baseRewardPool() public view returns (IBaseRewardPool _baseRewardPool) {
return IBaseRewardPool(address(this).readAddress(40));
}
/// @notice Identifier of the pool on Convex.
function pid() public view returns (uint256 _pid) {
return address(this).readUint256(60);
}
//////////////////////////////////////////////////////
// --- CONSTRUCTOR
//////////////////////////////////////////////////////
constructor(address _accountant, address _protocolController, address _cvx, address _booster)
Sidecar(CURVE_PROTOCOL_ID, _accountant, _protocolController)
{
CVX = IERC20(_cvx);
BOOSTER = _booster;
}
//////////////////////////////////////////////////////
// --- INITIALIZATION
//////////////////////////////////////////////////////
/// @notice Initialize the contract by approving the ConvexCurve booster to spend the LP token.
function _initialize() internal override {
require(asset().allowance(address(this), address(BOOSTER)) == 0, AlreadyInitialized());
asset().forceApprove(address(BOOSTER), type(uint256).max);
}
//////////////////////////////////////////////////////
// --- ISIDECAR OPERATIONS OVERRIDE
//////////////////////////////////////////////////////
/// @notice Deposit LP token into Convex.
/// @param amount Amount of LP token to deposit.
/// @dev The reason there's an empty address parameter is to keep flexibility for future implementations.
/// Not all fallbacks will be minimal proxies, so we need to keep the same function signature.
/// Only callable by the strategy.
function _deposit(uint256 amount) internal override {
/// Deposit the LP token into Convex and stake it (true) to receive rewards.
IBooster(BOOSTER).deposit(pid(), amount, true);
}
/// @notice Withdraw LP token from Convex.
/// @param amount Amount of LP token to withdraw.
/// @param receiver Address to receive the LP token.
function _withdraw(uint256 amount, address receiver) internal override {
/// Withdraw from Convex gauge without claiming rewards (false).
baseRewardPool().withdrawAndUnwrap(amount, false);
/// Send the LP token to the receiver.
asset().safeTransfer(receiver, amount);
}
/// @notice Claim rewards from Convex.
/// @return rewardTokenAmount Amount of reward token claimed.
function _claim() internal override returns (uint256 rewardTokenAmount) {
/// Claim rewardToken.
baseRewardPool().getReward(address(this), false);
rewardTokenAmount = REWARD_TOKEN.balanceOf(address(this));
if (rewardTokenAmount > 0) {
/// Send the reward token to the accountant.
REWARD_TOKEN.safeTransfer(ACCOUNTANT, rewardTokenAmount);
}
}
/// @notice Get the balance of the LP token on Convex held by this contract.
function balanceOf() public view override returns (uint256) {
return baseRewardPool().balanceOf(address(this));
}
/// @notice Get the reward tokens from the base reward pool.
/// @return Array of all extra reward tokens.
function getRewardTokens() public view override returns (address[] memory) {
// Check if there is extra rewards
uint256 extraRewardsLength = baseRewardPool().extraRewardsLength();
address[] memory tokens = new address[](extraRewardsLength);
address _token;
for (uint256 i; i < extraRewardsLength;) {
/// Get the address of the virtual balance pool.
_token = baseRewardPool().extraRewards(i);
tokens[i] = IBaseRewardPool(_token).rewardToken();
/// For PIDs greater than 150, the virtual balance pool also has a wrapper.
/// So we need to get the token from the wrapper.
/// Try catch because pid 151 case is only on Mainnet, not on L2s.
/// More: https://docs.convexfinance.com/convexfinanceintegration/baserewardpool
try IStashTokenWrapper(tokens[i]).token() returns (address _t) {
tokens[i] = _t;
} catch {}
unchecked {
++i;
}
}
return tokens;
}
/// @notice Get the amount of reward token earned by the strategy.
/// @return The amount of reward token earned by the strategy.
function getPendingRewards() public view override returns (uint256) {
return baseRewardPool().earned(address(this)) + REWARD_TOKEN.balanceOf(address(this));
}
//////////////////////////////////////////////////////
// --- EXTRA CONVEX OPERATIONS
//////////////////////////////////////////////////////
function claimExtraRewards() external {
address[] memory extraRewardTokens = getRewardTokens();
/// It'll claim rewardToken but we'll leave it here for clarity until the claim() function is called by the strategy.
baseRewardPool().getReward(address(this), true);
/// Send the reward token to the reward receiver.
address receiver = rewardReceiver();
uint256 balance = CVX.balanceOf(address(this));
if (balance > 0) {
CVX.safeTransfer(receiver, balance);
}
/// Handle the extra reward tokens.
for (uint256 i = 0; i < extraRewardTokens.length;) {
uint256 _balance = IERC20(extraRewardTokens[i]).balanceOf(address(this));
if (_balance > 0) {
/// Send the whole balance to the reward receiver.
IERC20(extraRewardTokens[i]).safeTransfer(receiver, _balance);
}
unchecked {
++i;
}
}
}
}
"
},
"src/integrations/curve/OnlyBoostAllocator.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {Allocator} from "src/Allocator.sol";
import {IBalanceProvider} from "src/interfaces/IBalanceProvider.sol";
import {ISidecar} from "src/interfaces/ISidecar.sol";
import {IConvexSidecarFactory} from "src/interfaces/IConvexSidecarFactory.sol";
/// @title OnlyBoostAllocator.
/// @author Stake DAO
/// @custom:github @stake-dao
/// @custom:contact contact@stakedao.org
/// @notice Calculates the optimal LP token allocation for Stake DAO Locker and Convex.
contract OnlyBoostAllocator is Allocator {
using Math for uint256;
/// @notice Address of the Curve Boost Delegation V3 contract
address public immutable BOOST_PROVIDER;
/// @notice Address of the Convex Boost Holder contract
address public immutable CONVEX_BOOST_HOLDER;
/// @notice Address of the Convex Sidecar Factory contract
IConvexSidecarFactory public immutable CONVEX_SIDECAR_FACTORY;
/// @notice Gauges forced to allocate 100% to the locker (used during migrations).
mapping(address => bool) public lockerOnly;
/// @notice Emitted when the locker-only override is toggled for a gauge.
event LockerOnlyUpdated(address indexed gauge, bool lockerOnly);
/// @notice Initializes the OnlyBoostAllocator contract
/// @param _locker Address of the Stake DAO Liquidity Locker
/// @param _gateway Address of the gateway contract
/// @param _convexSidecarFactory Address of the Convex Sidecar Factory contract
constructor(
address _locker,
address _gateway,
address _convexSidecarFactory,
address _boostProvider,
address _convexBoostHolder
) Allocator(_locker, _gateway) {
BOOST_PROVIDER = _boostProvider;
CONVEX_BOOST_HOLDER = _convexBoostHolder;
CONVEX_SIDECAR_FACTORY = IConvexSidecarFactory(_convexSidecarFactory);
}
/// @notice Enables or disables sidecar allocations for a gauge.
/// @dev Need to rebalance the allocator to update the allocation targets, else the deposit/withdrawal will fail.
function setLockerOnly(address gauge, bool value) external onlyLocker {
lockerOnly[gauge] = value;
emit LockerOnlyUpdated(gauge, value);
}
//////////////////////////////////////////////////////
// --- DEPOSIT ALLOCATION
//////////////////////////////////////////////////////
/// @inheritdoc Allocator
function getDepositAllocation(address asset, address gauge, uint256 amount)
public
view
override
returns (Allocation memory alloc)
{
// 1. Resolve the sidecar for the gauge.
address sidecar = CONVEX_SIDECAR_FACTORY.getSidecar(gauge);
if (sidecar == address(0)) {
return super.getDepositAllocation(asset, gauge, amount);
}
// 3. Prepare targets and amounts containers.
alloc.asset = asset;
alloc.gauge = gauge;
alloc.targets = _targets(sidecar);
alloc.amounts = _pair(0, 0);
if (lockerOnly[gauge]) {
alloc.amounts[1] = amount;
return alloc;
}
// 4. Fetch current balances.
uint256 balanceOfLocker = IBalanceProvider(gauge).balanceOf(LOCKER);
// 5. Get the optimal balance based on Convex balance and veBoost ratio.
uint256 optimalBalanceOfLocker = getOptimalLockerBalance(gauge);
// 6. Calculate the amount of lps to deposit into the locker.
alloc.amounts[1] =
optimalBalanceOfLocker > balanceOfLocker ? Math.min(optimalBalanceOfLocker - balanceOfLocker, amount) : 0;
// 7. Calculate the amount of lps to deposit into the sidecar.
alloc.amounts[0] = amount - alloc.amounts[1];
}
//////////////////////////////////////////////////////
// --- WITHDRAWAL ALLOCATION
//////////////////////////////////////////////////////
/// @inheritdoc Allocator
function getWithdrawalAllocation(address asset, address gauge, uint256 amount)
public
view
override
returns (Allocation memory alloc)
{
// 1. Resolve the sidecar.
address sidecar = CONVEX_SIDECAR_FACTORY.getSidecar(gauge);
// 2. Fallback to base allocator if none.
if (sidecar == address(0)) {
return super.getWithdrawalAllocation(asset, gauge, amount);
}
// 3. Prepare return struct.
alloc.asset = asset;
alloc.gauge = gauge;
alloc.targets = _targets(sidecar);
alloc.amounts = _pair(0, 0);
// 4. Current balances.
uint256 balanceOfSidecar = ISidecar(sidecar).balanceOf();
uint256 balanceOfLocker = IBalanceProvider(gauge).balanceOf(LOCKER);
// 5. Calculate the optimal amount of lps that must be held by the locker.
uint256 optimalBalanceOfLocker = getOptimalLockerBalance(gauge);
// 6. Calculate the total balance.
uint256 totalBalance = balanceOfSidecar + balanceOfLocker;
if (lockerOnly[gauge]) {
if (totalBalance <= amount) {
alloc.amounts[0] = balanceOfSidecar;
alloc.amounts[1] = balanceOfLocker;
} else {
alloc.amounts[0] = Math.min(amount, balanceOfSidecar);
uint256 remaining = amount - alloc.amounts[0];
alloc.amounts[1] = Math.min(remaining, balanceOfLocker);
}
return alloc;
}
// 7. Adjust the withdrawal based on the optimal amount for Stake DAO
if (totalBalance <= amount) {
// 7a. If the total balance is less than or equal to the withdrawal amount, withdraw everything
alloc.amounts[0] = balanceOfSidecar;
alloc.amounts[1] = balanceOfLocker;
} else if (optimalBalanceOfLocker >= balanceOfLocker) {
// 7b. If Stake DAO balance is below optimal, prioritize withdrawing from Convex
alloc.amounts[0] = Math.min(amount, balanceOfSidecar);
alloc.amounts[1] = amount > alloc.amounts[0] ? amount - alloc.amounts[0] : 0;
} else {
// 7c. If Stake DAO is above optimal, prioritize withdrawing from Stake DAO,
// but only withdraw as much as needed to bring the balance down to the optimal amount.
alloc.amounts[1] = Math.min(amount, balanceOfLocker - optimalBalanceOfLocker);
alloc.amounts[0] = amount > alloc.amounts[1] ? Math.min(amount - alloc.amounts[1], balanceOfSidecar) : 0;
// 7d. If there is still more to withdraw, withdraw the rest from Stake DAO.
if (amount > alloc.amounts[0] + alloc.amounts[1]) {
alloc.amounts[1] += amount - alloc.amounts[0] - alloc.amounts[1];
}
}
}
//////////////////////////////////////////////////////
// --- REBALANCE ALLOCATION
//////////////////////////////////////////////////////
/// @inheritdoc Allocator
function getRebalancedAllocation(address asset, address gauge, uint256 totalBalance)
public
view
override
returns (Allocation memory alloc)
{
// 1. Resolve sidecar.
address sidecar = CONVEX_SIDECAR_FACTORY.getSidecar(gauge);
if (sidecar == address(0)) {
return super.getRebalancedAllocation(asset, gauge, totalBalance);
}
// 2. Prepare struct.
alloc.asset = asset;
alloc.gauge = gauge;
alloc.targets = _targets(sidecar);
alloc.amounts = _pair(0, 0);
if (lockerOnly[gauge]) {
alloc.amounts[0] = 0;
alloc.amounts[1] = totalBalance;
}
else {
// 3. For rebalancing, we still want to match the optimal balance based on Convex holdings
// This ensures we maintain the boost-maximizing ratio
uint256 optimalLockerBalance = getOptimalLockerBalance(gauge);
// Cap the locker amount to the total balance available
alloc.amounts[1] = Math.min(optimalLockerBalance, totalBalance);
alloc.amounts[0] = totalBalance - alloc.amounts[1];
}
}
//////////////////////////////////////////////////////
// --- VIEW HELPER FUNCTIONS
//////////////////////////////////////////////////////
/// @inheritdoc Allocator
function getAllocationTargets(address gauge) public view override returns (address[] memory) {
address sidecar = CONVEX_SIDECAR_FACTORY.getSidecar(gauge);
if (sidecar == address(0)) {
return super.getAllocationTargets(gauge);
}
return _targets(sidecar);
}
/// @notice Returns the optimal amount of LP token that must be held by Stake DAO Locker
/// @dev Calculates the optimal balance to maximize boost efficiency
/// @param gauge Address of the Curve gauge
/// @return balanceOfLocker Optimal amount of LP token that should be held by Stake DAO Locker
function getOptimalLockerBalance(address gauge) public view returns (uint256 balanceOfLocker) {
// 1. Get the balance of veBoost on Stake DAO and Convex
uint256 veBoostOfLocker = IBalanceProvider(BOOST_PROVIDER).balanceOf(LOCKER);
uint256 veBoostOfConvex = IBalanceProvider(BOOST_PROVIDER).balanceOf(CONVEX_BOOST_HOLDER);
// 2. Get the balance of the liquidity gauge on Convex
uint256 balanceOfConvex = IBalanceProvider(gauge).balanceOf(CONVEX_BOOST_HOLDER);
// 3. If there is no balance of Convex or no veBoost on Convex, return 0
if (balanceOfConvex == 0 || veBoostOfConvex == 0) return 0;
// 4. Compute the optimal balance for Stake DAO based on veBoost ratio
// This ensures Stake DAO gets LP tokens proportional to its veBoost advantage
balanceOfLocker = balanceOfConvex.mulDiv(veBoostOfLocker, veBoostOfConvex);
}
//////////////////////////////////////////////////////
// --- HELPER FUNCTIONS
//////////////////////////////////////////////////////
/// @dev Returns the pair `[sidecar, LOCKER]` used by allocation targets.
function _targets(address sidecar) private view returns (address[] memory arr) {
arr = new address[](2);
arr[0] = sidecar;
arr[1] = LOCKER;
}
/// @dev Utility to allocate a two‑element uint256 array.
function _pair(uint256 a0, uint256 a1) private pure returns (uint256[] memory arr) {
arr = new uint256[](2);
arr[0] = a0;
arr[1] = a1;
}
}
"
},
"src/integrations/curve/ConvexSidecarFactory.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.28;
import {SidecarFactory} from "src/SidecarFactory.sol";
import {IBooster} from "@interfaces/convex/IBooster.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {ConvexSidecar} from "src/integrations/curve/ConvexSidecar.sol";
/// @title ConvexSidecarFactory.
/// @author Stake DAO
/// @custom:github @stake-dao
/// @custom:contact contact@stakedao.org
/// @notice Factory contract for deploying ConvexSidecar instances.
contract ConvexSidecarFactory is SidecarFactory {
/// @notice The bytes4 ID of the Convex protocol
/// @dev Used to identify the Convex protocol in the registry
bytes4 private constant CURVE_PROTOCOL_ID = bytes4(keccak256("CURVE"));
/// @notice Convex Booster contract address
address public immutable BOOSTER;
address public constant OLD_SIDECAR_FACTORY = 0x7Fa7fDb80b17f502C323D14Fa654a1e56B03C592;
/// @notice Error emitted when the pool is shutdown
error PoolShutdown();
/// @notice Error emitted when the arguments are invalid
error InvalidArguments();
/// @notice Error emitted when the reward receiver is not set
error VaultNotDeployed();
/// @notice Constructor
/// @param _implementation Address of the sidecar implementation
/// @param _protocolController Address of the protocol controller
/// @param _booster Address of the Convex Booster contract
constructor(address _implementation, address _protocolController, address _booster)
SidecarFactory(CURVE_PROTOCOL_ID, _implementation, _protocolController)
{
BOOSTER = _booster;
}
/// @notice Convenience function to create a sidecar with a uint256 pid parameter
/// @param pid Pool ID in Convex
/// @return sidecar Address of the created sidecar
function create(address gauge, uint256 pid) external returns (address sidecar) {
bytes memory args = abi.encode(pid);
return create(gauge, args);
}
/// @notice Validates the gauge and arguments for Convex
/// @param gauge The gauge to validate
/// @param args The arguments containing the pool ID
function _isValidGauge(address gauge, bytes memory args) internal view override {
require(args.length == 32, InvalidArguments());
uint256 pid = abi.decode(args, (uint256));
// Get the pool info from Convex
(,, address curveGauge,,, bool isShutdown) = IBooster(BOOSTER).poolInfo(pid);
// Ensure the pool is not shutdown
if (isShutdown) revert PoolShutdown();
// Ensure the gauge matches
if (curveGauge != gauge) revert InvalidGauge();
}
/// @notice Creates a ConvexSidecar for a gauge
/// @param gauge The gauge to create a sidecar for
/// @param args The arguments containing the pool ID
/// @return sidecarAddress Address of the created sidecar
function _create(address gauge, bytes memory args) internal override returns (address sidecarAddress) {
uint256 pid = abi.decode(args, (uint256));
// Get the LP token and base reward pool from Convex
(address lpToken,,, address baseRewardPool,,) = IBooster(BOOSTER).poolInfo(pid);
address rewardReceiver = PROTOCOL_CONTROLLER.rewardReceiver(gauge);
require(rewardReceiver != address(0), VaultNotDeployed());
// Encode the immutable arguments for the clone
bytes memory data = abi.encodePacked(lpToken, gauge, baseRewardPool, pid);
// Create a deterministic salt based on the token and gauge
bytes32 salt = keccak256(data);
// Clone the implementation contract
sidecarAddress = Clones.cloneDeterministicWithImmutableArgs(IMPLEMENTATION, data, salt);
// Initialize the sidecar
ConvexSidecar(sidecarAddress).initialize();
// Set the valid allocation target
PROTOCOL_CONTROLLER.setValidAllocationTarget(gauge, sidecarAddress);
return sidecarAddress;
}
function getSidecar(address gauge) public view returns (address _sidecar) {
_sidecar = sidecar[gauge];
if (_sidecar == address(0)) {
_sidecar = ConvexSidecarFactory(OLD_SIDECAR_FACTORY).sidecar(gauge);
}
}
}
"
},
"node_modules/@openzeppelin/contracts/utils/Create2.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/Create2.sol)
pragma solidity ^0.8.20;
import {Errors} from "./Errors.sol";
/**
* @dev Helper to make usage of the `CREATE2` EVM opcode easier and safer.
* `CREATE2` can be used to compute in advance the address where a smart
* contract will be deployed, which allows for interesting new mechanisms known
* as 'counterfactual interactions'.
*
* See the https://eips.ethereum.org/EIPS/eip-1014#motivation[EIP] for more
* information.
*/
library Create2 {
/**
* @dev There's no code to deploy.
*/
error Create2EmptyBytecode();
/**
* @dev Deploys a contract using `CREATE2`. The address where the contract
* will be deployed can be known in advance via {computeAddress}.
*
* The bytecode for a contract can be obtained from Solidity with
* `type(contractName).creationCode`.
*
* Requirements:
*
* - `bytecode` must not be empty.
* - `salt` must have not been used for `bytecode` already.
* - the factory must have a balance of at least `amount`.
* - if `amount` is non-zero, `bytecode` must have a `payable` constructor.
*/
function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) internal returns (address addr) {
if (address(this).balance < amount) {
revert Errors.InsufficientBalance(address(this).balance, amount);
}
if (bytecode.length == 0) {
revert Create2EmptyBytecode();
}
assembly ("memory-safe") {
addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
// if no address was created, and returndata is not empty, bubble revert
if and(iszero(addr), not(iszero(returndatasize()))) {
let p := mload(0x40)
returndatacopy(p, 0, returndatasize())
revert(p, returndatasize())
}
}
if (addr == address(0)) {
revert Errors.FailedDeployment();
}
}
/**
* @dev Returns the address where a contract will be stored if deployed via {deploy}. Any change in the
* `bytecodeHash` or `salt` will result in a new destination address.
*/
function computeAddress(bytes32 salt, bytes32 bytecodeHash) internal view returns (address) {
return computeAddress(salt, bytecodeHash, address(this));
}
/**
* @dev Returns the address where a contract will be stored if deployed via {deploy} from a contract located at
* `deployer`. If `deployer` is this contract's address, returns the same value as {computeAddress}.
*/
function computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer) internal pure returns (address addr) {
assembly ("memory-safe") {
let ptr := mload(0x40) // Get free memory pointer
// | | ↓ ptr ... ↓ ptr + 0x0B (start) ... ↓ ptr + 0x20 ... ↓ ptr + 0x40 ... |
// |-------------------|---------------------------------------------------------------------------|
// | bytecodeHash | CCCCCCCCCCCCC...CC |
// | salt | BBBBBBBBBBBBB...BB |
// | deployer | 000000...0000AAAAAAAAAAAAAAAAAAA...AA |
// | 0xFF | FF |
// |-------------------|---------------------------------------------------------------------------|
// | memory | 000000...00FFAAAAAAAAAAAAAAAAAAA...AABBBBBBBBBBBBB...BBCCCCCCCCCCCCC...CC |
// | keccak(start, 85) | ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ |
mstore(add(ptr, 0x40), bytecodeHash)
mstore(add(ptr, 0x20), salt)
mstore(ptr, deployer) // Right-aligned with 12 preceding garbage bytes
let start := add(ptr, 0x0b) // The hashed data starts at the final garbage byte which we will set to 0xff
mstore8(start, 0xff)
addr := and(keccak256(start, 85), 0xffffffffffffffffffffffffffffffffffffffff)
}
}
}
"
},
"node_modules/@openzeppelin/contracts/utils/Errors.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/Errors.sol)
pragma solidity ^0.8.20;
/**
* @dev Collection of common custom errors used in multiple contracts
*
* IMPORTANT: Backwards compatibility is not guaranteed in future versions of the library.
* It is recommended to avoid relying on the error API for critical functionality.
*
* _Available since v5.1._
*/
library Errors {
/**
* @dev The ETH balance of the account is not enough to perform the operation.
*/
error InsufficientBalance(uint256 balance, uint256 needed);
/**
* @dev A call to an address target failed. The target may have reverted.
*/
error FailedCall();
/**
* @dev The deployment failed.
*/
error FailedDeployment();
/**
* @dev A necessary precompile is missing.
*/
error MissingPrecompile(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(add
Submitted on: 2025-10-24 18:45:48
Comments
Log in to comment.
No comments yet.