From aca54e1d37b675cc82e4845bec8ed617d27cf1cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= <55331875+clement-ux@users.noreply.github.com> Date: Fri, 31 Jan 2025 00:28:59 +0100 Subject: [PATCH] Implement Curve Pool Booster. (#2327) * feat: Implement Curve Pool Booster. * feat: Add proxy for Curve Pool Booster. * feaat: prepare deployment file for Curve Pool Booster. * feat: add deployment file for Curve Pool Booster. * feat: Add more test. * Revert "Merge remote-tracking branch 'origin/master' into clement/pool-booster-curve" This reverts commit b3de3b014c3fab007038b379076f99de7fbecf54, reversing changes made to e65f962552ba7063fa4c2bf4caf877c7541d9f9c. * fix: bring back latest changes on master. * Fix tests * feat: send ETH back. * feat: Add events. * feat: rescue tokens. * feat: add possiblity to blacklist users. * feat: add convex vecrv voter. * prettier * feat: Add fee. * fix: adjust deploy number. * fix: adjust deployment file with new Curve Pool. * feat: add new curve pool/gauge. * fix: use `Strategizable`instead of operator. * prettier. * fix: adjust event names. * docs: add natspec to functions. * fix: prevent `feeCollector` to be address(0). * fix: use `call` instead of `transfer` for ETH. * fix: use internal logic for setters. * fix: adjust event name. * fix: cache balance for gas. * fix: ensure receiver is not address(0). * fix: type adjustment. * fix: emit event when fees are collected. * fix: add onlyGovernor for initialization. * fix: use safeApprove. * fix: add natspec. * fix: group logic. * fix: use multichain strategist. * prettier. * fix: adjust deployment number. * fix: adjust comments. * fix: change from BASE_FEE to FEE_BASE. * fix: add reentrancy blocker. * fix: handle rebasing token for fees. * linter + prettier. * Add `closeCampaign()` to CurvePoolBooster. (#2360) * feat: add closeCampaign function to CurvePoolBooster. * fix: remove immutable from campaignRemoteManager. * fix: adjust naming and prettier. * feat: configure deploymment with CreateX. * fix: use encodeFunctionData to encodeWithSignature. * feat: add arbitrum deployment file for CurvePoolBooster. * feat: create a funciton for encodedSalt. * feat: create multichainStrategist variable. * fix: change proxy owner. * try something. * fix: adjust with new CampaignRemoteManager. * fix merge conflit. * fix: add more tests. * feat: add extra test for Curve Pool Booster. * prettier. * fix: add missing`nonReentrant`. * fix: approve 0 before approving. * fix: gas optimization. * fix: adjust salt. * Ran prettier for js files * Fixed deployName in Arb deploy of Pool Booster * Deploy 119 and 120 - Curve Pool Booster Mainnet and Arbitrum (#2367) * fix: adjust deployName. * fix: deploy 119 and 120. * feat: add safeTransfer for USDT. * fix: adjust deployed address for CurvePoolBooster. * fix: simplidy tests. * Fixed Arb deploy script of Curve Pool Booster * FIxed forked coverage tests * Disabled OUSD AMO fork tests * Fixed CurvePoolBooster fork test * Fixed SimpleHarvester fork tests --------- Co-authored-by: Shahul Hameed <10547529+shahthepro@users.noreply.github.com> Co-authored-by: Domen Grabec Co-authored-by: Nicholas Addison --- contracts/abi/createx.json | 77 ++++ .../interfaces/ICampaignRemoteManager.sol | 50 +++ contracts/contracts/proxies/Proxies.sol | 7 + .../contracts/strategies/CurvePoolBooster.sol | 415 ++++++++++++++++++ .../arbitrumOne/003_pool_booster_curve.js | 126 ++++++ .../deploy/mainnet/121_pool_booster_curve.js | 123 ++++++ .../deployments/arbitrumOne/.migrations.json | 3 +- .../deployments/mainnet/.migrations.json | 3 +- contracts/fork-test.sh | 4 +- contracts/test/_fixture.js | 8 + .../simple-harvester.mainnet.fork-test.js | 14 +- .../curvePoolBooster.mainnet.fork-test.js | 392 +++++++++++++++++ ...pool-3crv-tilted-pool.mainnet.fork-test.js | 3 +- ...etapool-balanced-pool.mainnet.fork-test.js | 3 +- ...pool-ousd-tilted-pool.mainnet.fork-test.js | 3 +- contracts/utils/addresses.js | 13 + contracts/utils/deploy.js | 29 ++ 17 files changed, 1256 insertions(+), 17 deletions(-) create mode 100644 contracts/abi/createx.json create mode 100644 contracts/contracts/interfaces/ICampaignRemoteManager.sol create mode 100644 contracts/contracts/strategies/CurvePoolBooster.sol create mode 100644 contracts/deploy/arbitrumOne/003_pool_booster_curve.js create mode 100644 contracts/deploy/mainnet/121_pool_booster_curve.js create mode 100644 contracts/test/strategies/curvePoolBooster.mainnet.fork-test.js diff --git a/contracts/abi/createx.json b/contracts/abi/createx.json new file mode 100644 index 0000000000..9e30b0e694 --- /dev/null +++ b/contracts/abi/createx.json @@ -0,0 +1,77 @@ +[ + { + "inputs": [ + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "initCode", + "type": "bytes" + } + ], + "name": "deployCreate2", + "outputs": [ + { + "internalType": "address", + "name": "newContract", + "type": "address" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "initCode", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "constructorAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "initCallAmount", + "type": "uint256" + } + ], + "internalType": "struct CreateX.Values", + "name": "values", + "type": "tuple" + }, + { + "internalType": "address", + "name": "refundAddress", + "type": "address" + } + ], + "name": "deployCreate2AndInit", + "outputs": [ + { + "internalType": "address", + "name": "newContract", + "type": "address" + } + ], + "stateMutability": "payable", + "type": "function" + } +] \ No newline at end of file diff --git a/contracts/contracts/interfaces/ICampaignRemoteManager.sol b/contracts/contracts/interfaces/ICampaignRemoteManager.sol new file mode 100644 index 0000000000..d4823f3692 --- /dev/null +++ b/contracts/contracts/interfaces/ICampaignRemoteManager.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface ICampaignRemoteManager { + function createCampaign( + CampaignCreationParams memory params, + uint256 destinationChainId, + uint256 additionalGasLimit, + address votemarket + ) external payable; + + function manageCampaign( + CampaignManagementParams memory params, + uint256 destinationChainId, + uint256 additionalGasLimit, + address votemarket + ) external payable; + + function closeCampaign( + CampaignClosingParams memory params, + uint256 destinationChainId, + uint256 additionalGasLimit, + address votemarket + ) external payable; + + struct CampaignCreationParams { + uint256 chainId; + address gauge; + address manager; + address rewardToken; + uint8 numberOfPeriods; + uint256 maxRewardPerVote; + uint256 totalRewardAmount; + address[] addresses; + address hook; + bool isWhitelist; + } + + struct CampaignManagementParams { + uint256 campaignId; + address rewardToken; + uint8 numberOfPeriods; + uint256 totalRewardAmount; + uint256 maxRewardPerVote; + } + + struct CampaignClosingParams { + uint256 campaignId; + } +} diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index 583df1fd3c..663265b7fa 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -359,6 +359,13 @@ contract MorphoGauntletPrimeUSDTStrategyProxy is } +/** + * @notice CurvePoolBoosterProxy delegates calls to a CurvePoolBooster implementation + */ +contract CurvePoolBoosterProxy is InitializeGovernedUpgradeabilityProxy { + +} + /** * @notice OETHFixedRateDripperProxy delegates calls to a OETHFixedRateDripper implementation */ diff --git a/contracts/contracts/strategies/CurvePoolBooster.sol b/contracts/contracts/strategies/CurvePoolBooster.sol new file mode 100644 index 0000000000..f1a0742920 --- /dev/null +++ b/contracts/contracts/strategies/CurvePoolBooster.sol @@ -0,0 +1,415 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Initializable } from "../utils/Initializable.sol"; +import { Strategizable } from "../governance/Strategizable.sol"; +import { ICampaignRemoteManager } from "../interfaces/ICampaignRemoteManager.sol"; + +/// @title CurvePoolBooster +/// @author Origin Protocol +/// @notice Contract to manage interactions with VotemarketV2 for a dedicated Curve pool/gauge. +contract CurvePoolBooster is Initializable, Strategizable { + using SafeERC20 for IERC20; + + //////////////////////////////////////////////////// + /// --- CONSTANTS && IMMUTABLES + //////////////////////////////////////////////////// + /// @notice Base fee for the contract, 100% + uint16 public constant FEE_BASE = 10_000; + + /// @notice Address of the gauge to manage + address public immutable gauge; + + /// @notice Address of the reward token + address public immutable rewardToken; + + /// @notice Chain id of the target chain + uint256 public immutable targetChainId; + + //////////////////////////////////////////////////// + /// --- STORAGE + //////////////////////////////////////////////////// + /// @notice Fee in FEE_BASE unit payed when managing campaign. + uint16 public fee; + + /// @notice Address of the fee collector + address public feeCollector; + + /// @notice Address of the campaignRemoteManager linked to VotemarketV2 + address public campaignRemoteManager; + + /// @notice Address of votemarket in L2 + address public votemarket; + + /// @notice Id of the campaign created + uint256 public campaignId; + + //////////////////////////////////////////////////// + /// --- EVENTS + //////////////////////////////////////////////////// + event FeeUpdated(uint16 newFee); + event FeeCollected(address feeCollector, uint256 feeAmount); + event FeeCollectorUpdated(address newFeeCollector); + event VotemarketUpdated(address newVotemarket); + event CampaignRemoteManagerUpdated(address newCampaignRemoteManager); + event CampaignCreated( + address gauge, + address rewardToken, + uint256 maxRewardPerVote, + uint256 totalRewardAmount + ); + event CampaignIdUpdated(uint256 newId); + event CampaignClosed(uint256 campaignId); + event TotalRewardAmountUpdated(uint256 extraTotalRewardAmount); + event NumberOfPeriodsUpdated(uint8 extraNumberOfPeriods); + event RewardPerVoteUpdated(uint256 newMaxRewardPerVote); + event TokensRescued(address token, uint256 amount, address receiver); + + //////////////////////////////////////////////////// + /// --- CONSTRUCTOR && INITIALIZATION + //////////////////////////////////////////////////// + constructor( + uint256 _targetChainId, + address _rewardToken, + address _gauge + ) { + targetChainId = _targetChainId; + rewardToken = _rewardToken; + gauge = _gauge; + + // Prevent implementation contract to be governed + _setGovernor(address(0)); + } + + /// @notice initialize function, to set up initial internal state + /// @param _strategist Address of the strategist + /// @param _fee Fee in FEE_BASE unit payed when managing campaign + /// @param _feeCollector Address of the fee collector + function initialize( + address _strategist, + uint16 _fee, + address _feeCollector, + address _campaignRemoteManager, + address _votemarket + ) external onlyGovernor initializer { + _setStrategistAddr(_strategist); + _setFee(_fee); + _setFeeCollector(_feeCollector); + _setCampaignRemoteManager(_campaignRemoteManager); + _setVotemarket(_votemarket); + } + + //////////////////////////////////////////////////// + /// --- MUTATIVE FUNCTIONS + //////////////////////////////////////////////////// + /// @notice Create a new campaign on VotemarketV2 + /// @dev This will use all token available in this contract + /// @param numberOfPeriods Duration of the campaign in weeks + /// @param maxRewardPerVote Maximum reward per vote to distribute, to avoid overspending + /// @param blacklist List of addresses to exclude from the campaign + /// @param bridgeFee Fee to pay for the bridge + /// @param additionalGasLimit Additional gas limit for the bridge + function createCampaign( + uint8 numberOfPeriods, + uint256 maxRewardPerVote, + address[] calldata blacklist, + uint256 bridgeFee, + uint256 additionalGasLimit + ) external nonReentrant onlyGovernorOrStrategist { + require(campaignId == 0, "Campaign already created"); + require(numberOfPeriods > 1, "Invalid number of periods"); + require(maxRewardPerVote > 0, "Invalid reward per vote"); + + // Handle fee (if any) + uint256 balanceSubFee = _handleFee(); + + // Approve the balanceSubFee to the campaign manager + IERC20(rewardToken).safeApprove(campaignRemoteManager, 0); + IERC20(rewardToken).safeApprove(campaignRemoteManager, balanceSubFee); + + // Create a new campaign + ICampaignRemoteManager(campaignRemoteManager).createCampaign{ + value: bridgeFee + }( + ICampaignRemoteManager.CampaignCreationParams({ + chainId: targetChainId, + gauge: gauge, + manager: address(this), + rewardToken: rewardToken, + numberOfPeriods: numberOfPeriods, + maxRewardPerVote: maxRewardPerVote, + totalRewardAmount: balanceSubFee, + addresses: blacklist, + hook: address(0), + isWhitelist: false + }), + targetChainId, + additionalGasLimit, + votemarket + ); + + emit CampaignCreated( + gauge, + rewardToken, + maxRewardPerVote, + balanceSubFee + ); + } + + /// @notice Manage the total reward amount of the campaign + /// @dev This function should be called after the campaign is created + /// @dev This will use all the token available in this contract + /// @param bridgeFee Fee to pay for the bridge + /// @param additionalGasLimit Additional gas limit for the bridge + function manageTotalRewardAmount( + uint256 bridgeFee, + uint256 additionalGasLimit + ) external nonReentrant onlyGovernorOrStrategist { + require(campaignId != 0, "Campaign not created"); + + // Handle fee (if any) + uint256 balanceSubFee = _handleFee(); + + // Approve the total reward amount to the campaign manager + IERC20(rewardToken).safeApprove(campaignRemoteManager, 0); + IERC20(rewardToken).safeApprove(campaignRemoteManager, balanceSubFee); + + // Manage the campaign + // https://github.com/stake-dao/votemarket-v2/blob/main/packages/votemarket/src/Votemarket.sol#L668 + ICampaignRemoteManager(campaignRemoteManager).manageCampaign{ + value: bridgeFee + }( + ICampaignRemoteManager.CampaignManagementParams({ + campaignId: campaignId, + rewardToken: rewardToken, + numberOfPeriods: 0, + totalRewardAmount: balanceSubFee, + maxRewardPerVote: 0 + }), + targetChainId, + additionalGasLimit, + votemarket + ); + + emit TotalRewardAmountUpdated(balanceSubFee); + } + + /// @notice Manage the number of periods of the campaign + /// @dev This function should be called after the campaign is created + /// @param extraNumberOfPeriods Number of additional periods (cannot be 0) + /// that will be added to already existing amount of periods. + /// @param bridgeFee Fee to pay for the bridge + /// @param additionalGasLimit Additional gas limit for the bridge + function manageNumberOfPeriods( + uint8 extraNumberOfPeriods, + uint256 bridgeFee, + uint256 additionalGasLimit + ) external nonReentrant onlyGovernorOrStrategist { + require(campaignId != 0, "Campaign not created"); + require(extraNumberOfPeriods > 0, "Invalid number of periods"); + + // Manage the campaign + ICampaignRemoteManager(campaignRemoteManager).manageCampaign{ + value: bridgeFee + }( + ICampaignRemoteManager.CampaignManagementParams({ + campaignId: campaignId, + rewardToken: rewardToken, + numberOfPeriods: extraNumberOfPeriods, + totalRewardAmount: 0, + maxRewardPerVote: 0 + }), + targetChainId, + additionalGasLimit, + votemarket + ); + + emit NumberOfPeriodsUpdated(extraNumberOfPeriods); + } + + /// @notice Manage the reward per vote of the campaign + /// @dev This function should be called after the campaign is created + /// @param newMaxRewardPerVote New maximum reward per vote + /// @param bridgeFee Fee to pay for the bridge + /// @param additionalGasLimit Additional gas limit for the bridge + function manageRewardPerVote( + uint256 newMaxRewardPerVote, + uint256 bridgeFee, + uint256 additionalGasLimit + ) external nonReentrant onlyGovernorOrStrategist { + require(campaignId != 0, "Campaign not created"); + require(newMaxRewardPerVote > 0, "Invalid reward per vote"); + + // Manage the campaign + ICampaignRemoteManager(campaignRemoteManager).manageCampaign{ + value: bridgeFee + }( + ICampaignRemoteManager.CampaignManagementParams({ + campaignId: campaignId, + rewardToken: rewardToken, + numberOfPeriods: 0, + totalRewardAmount: 0, + maxRewardPerVote: newMaxRewardPerVote + }), + targetChainId, + additionalGasLimit, + votemarket + ); + + emit RewardPerVoteUpdated(newMaxRewardPerVote); + } + + /// @notice Close the campaign. + /// @dev This function only work on the L2 chain. Not on mainnet. + /// @dev The _campaignId parameter is not related to the campaignId of this contract, allowing greater flexibility. + /// @param _campaignId Id of the campaign to close + function closeCampaign( + uint256 _campaignId, + uint256 bridgeFee, + uint256 additionalGasLimit + ) external nonReentrant onlyGovernorOrStrategist { + ICampaignRemoteManager(campaignRemoteManager).closeCampaign{ + value: bridgeFee + }( + ICampaignRemoteManager.CampaignClosingParams({ + campaignId: campaignId + }), + targetChainId, + additionalGasLimit, + votemarket + ); + emit CampaignClosed(_campaignId); + } + + /// @notice calculate the fee amount and transfer it to the feeCollector + /// @return Balance after fee + function _handleFee() internal returns (uint256) { + // Cache current rewardToken balance + uint256 balance = IERC20(rewardToken).balanceOf(address(this)); + require(balance > 0, "No reward to manage"); + + uint256 feeAmount = (balance * fee) / FEE_BASE; + + // If there is a fee, transfer it to the feeCollector + if (feeAmount > 0) { + // Transfer the fee to the feeCollector + IERC20(rewardToken).safeTransfer(feeCollector, feeAmount); + emit FeeCollected(feeCollector, feeAmount); + + return IERC20(rewardToken).balanceOf(address(this)); + } + + // Return remaining balance + return balance; + } + + //////////////////////////////////////////////////// + /// --- GOVERNANCE && OPERATION + //////////////////////////////////////////////////// + /// @notice Set the campaign id + /// @dev Only callable by the governor or strategist + /// @param _campaignId New campaign id + function setCampaignId(uint256 _campaignId) + external + onlyGovernorOrStrategist + { + campaignId = _campaignId; + emit CampaignIdUpdated(_campaignId); + } + + /// @notice Rescue ETH from the contract + /// @dev Only callable by the governor or strategist + /// @param receiver Address to receive the ETH + function rescueETH(address receiver) + external + nonReentrant + onlyGovernorOrStrategist + { + require(receiver != address(0), "Invalid receiver"); + uint256 balance = address(this).balance; + (bool success, ) = receiver.call{ value: balance }(""); + require(success, "Transfer failed"); + emit TokensRescued(address(0), balance, receiver); + } + + /// @notice Rescue ERC20 tokens from the contract + /// @dev Only callable by the governor or strategist + /// @param token Address of the token to rescue + function rescueToken(address token, address receiver) + external + nonReentrant + onlyGovernor + { + require(receiver != address(0), "Invalid receiver"); + uint256 balance = IERC20(token).balanceOf(address(this)); + IERC20(token).safeTransfer(receiver, balance); + emit TokensRescued(token, balance, receiver); + } + + /// @notice Set the fee + /// @dev Only callable by the governor + /// @param _fee New fee + function setFee(uint16 _fee) external onlyGovernor { + _setFee(_fee); + } + + /// @notice Internal logic to set the fee + function _setFee(uint16 _fee) internal { + require(_fee <= FEE_BASE / 2, "Fee too high"); + fee = _fee; + emit FeeUpdated(_fee); + } + + /// @notice Set the fee collector + /// @dev Only callable by the governor + /// @param _feeCollector New fee collector + function setFeeCollector(address _feeCollector) external onlyGovernor { + _setFeeCollector(_feeCollector); + } + + /// @notice Internal logic to set the fee collector + function _setFeeCollector(address _feeCollector) internal { + require(_feeCollector != address(0), "Invalid fee collector"); + feeCollector = _feeCollector; + emit FeeCollectorUpdated(_feeCollector); + } + + /// @notice Set the campaignRemoteManager + /// @param _campaignRemoteManager New campaignRemoteManager address + function setCampaignRemoteManager(address _campaignRemoteManager) + external + onlyGovernor + { + _setCampaignRemoteManager(_campaignRemoteManager); + } + + /// @notice Internal logic to set the campaignRemoteManager + /// @param _campaignRemoteManager New campaignRemoteManager address + function _setCampaignRemoteManager(address _campaignRemoteManager) + internal + { + require( + _campaignRemoteManager != address(0), + "Invalid campaignRemoteManager" + ); + campaignRemoteManager = _campaignRemoteManager; + emit CampaignRemoteManagerUpdated(_campaignRemoteManager); + } + + /// @notice Set the votemarket address + /// @param _votemarket New votemarket address + function setVotemarket(address _votemarket) external onlyGovernor { + _setVotemarket(_votemarket); + } + + /// @notice Internal logic to set the votemarket address + function _setVotemarket(address _votemarket) internal onlyGovernor { + require(_votemarket != address(0), "Invalid votemarket"); + votemarket = _votemarket; + emit VotemarketUpdated(_votemarket); + } + + receive() external payable {} +} diff --git a/contracts/deploy/arbitrumOne/003_pool_booster_curve.js b/contracts/deploy/arbitrumOne/003_pool_booster_curve.js new file mode 100644 index 0000000000..326690348f --- /dev/null +++ b/contracts/deploy/arbitrumOne/003_pool_booster_curve.js @@ -0,0 +1,126 @@ +const addresses = require("../../utils/addresses"); +const { deployOnArb } = require("../../utils/deploy-l2"); +const { encodeSaltForCreateX } = require("../../utils/deploy"); + +const createxAbi = require("../../abi/createx.json"); +const PoolBoosterBytecode = require("../../artifacts/contracts/strategies/CurvePoolBooster.sol/CurvePoolBooster.json"); +const ProxyBytecode = require("../../artifacts/contracts/proxies/Proxies.sol/CurvePoolBoosterProxy.json"); + +// --------------------------------!!! / WARNING \ !!!----------------------------------------- +// +// `encodedSalt`, ProxyBytecode and PoolBoosterBytecode should be EXACTLY the same as the 119 !!! +// It is using createX to deploy contract at the SAME address as the one deployed in 119. +// +// -------------------------------------------------------------------------------------------- + +module.exports = deployOnArb( + { + deployName: "003_pool_booster_curve", + forceDeploy: false, + //forceSkip: true, + reduceQueueTime: true, + deployerIsProposer: false, + proposalId: "", + }, + async ({ withConfirmation }) => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + console.log(`Deployer address: ${deployerAddr}`); + + const cCurvePoolBooster = await ethers.getContractAt( + "CurvePoolBooster", + addresses.zero + ); + const cCurvePoolBoosterProxy = await ethers.getContractAt( + "CurvePoolBoosterProxy", + addresses.zero + ); + + console.log("\nStarting deployment using CreateX"); + const rewardToken = addresses.mainnet.OUSDProxy; + const gauge = addresses.mainnet.CurveOUSDUSDTGauge; + const targetedChainId = 42161; // arbitrum + const fee = 0; + + // Get CreateX contract + const cCreateX = await ethers.getContractAt(createxAbi, addresses.createX); + + // Generate salt + const salt = ethers.utils.keccak256( + ethers.utils.concat([ + ethers.utils.arrayify(rewardToken), + ethers.utils.arrayify(gauge), + ethers.utils.arrayify(1), + ]) + ); + + const encodedSalt = encodeSaltForCreateX(deployerAddr, false, salt); + console.log(`Encoded salt: ${encodedSalt}`); + + // --- Deploy implementation --- // + const cachedInitCodeImpl = ethers.utils.concat([ + PoolBoosterBytecode.bytecode, + ethers.utils.defaultAbiCoder.encode( + ["uint256", "address", "address"], + [targetedChainId, rewardToken, gauge] + ), + ]); + const txResponse = await withConfirmation( + cCreateX.connect(sDeployer).deployCreate2(encodedSalt, cachedInitCodeImpl) + ); + const txReceipt = await txResponse.wait(); + // event 0 is GovernorshipTransferred + // event 1 is ContractCreation, topics[1] is the address of the deployed contract, topics[2] is the salt + const implementationAddress = ethers.utils.getAddress( + `0x${txReceipt.events[1].topics[1].slice(26)}` + ); + console.log( + `Curve Booster Implementation deployed at: ${implementationAddress}` + ); + + // --- Deploy and init proxy --- // + const cachedInitCodeProxy = ProxyBytecode.bytecode; // No constructor arguments + const initializeImplem = cCurvePoolBooster.interface.encodeFunctionData( + "initialize(address,uint16,address,address,address)", + [ + addresses.multichainStrategist, // strategist + fee, // fee + addresses.multichainStrategist, // feeCollector + addresses.mainnet.CampaignRemoteManager, // campaignRemoteManager + addresses.votemarket, // votemarket + ] + ); + const initializeProxy = cCurvePoolBoosterProxy.interface.encodeFunctionData( + "initialize(address,address,bytes)", + [ + implementationAddress, // implementation + addresses.arbitrumOne.admin, // governor + initializeImplem, // init data + ] + ); + const txResponseProxy = await withConfirmation( + cCreateX + .connect(sDeployer) + .deployCreate2AndInit( + encodedSalt, + cachedInitCodeProxy, + initializeProxy, + ["0x00", "0x00"], + deployerAddr + ) + ); + const txReceiptProxy = await txResponseProxy.wait(); + // event 0 is GovernorshipTransferred + // event 1 is ContractCreation, topics[1] is the address of the deployed contract, topics[2] is the salt + // event 2 is StrategistUpdated + // event 3 is FeeUpdated + // event 4 is FeeCollectorUpdated + // event 5 is CampaignRemoteManagerUpdated + // event 6 is GovernorshipTransferred + const proxyAddress = ethers.utils.getAddress( + `0x${txReceiptProxy.events[1].topics[1].slice(26)}` + ); + console.log(`Curve Booster Proxy deployed at: ${proxyAddress}`); + return {}; + } +); diff --git a/contracts/deploy/mainnet/121_pool_booster_curve.js b/contracts/deploy/mainnet/121_pool_booster_curve.js new file mode 100644 index 0000000000..540dd62df1 --- /dev/null +++ b/contracts/deploy/mainnet/121_pool_booster_curve.js @@ -0,0 +1,123 @@ +const addresses = require("../../utils/addresses"); +const { + deploymentWithGovernanceProposal, + encodeSaltForCreateX, +} = require("../../utils/deploy"); +const createxAbi = require("../../abi/createx.json"); +const PoolBoosterBytecode = require("../../artifacts/contracts/strategies/CurvePoolBooster.sol/CurvePoolBooster.json"); +const ProxyBytecode = require("../../artifacts/contracts/proxies/Proxies.sol/CurvePoolBoosterProxy.json"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "121_pool_booster_curve", + forceDeploy: false, + //forceSkip: true, + reduceQueueTime: true, + deployerIsProposer: false, + proposalId: "", + }, + async ({ withConfirmation }) => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + console.log(`Deployer address: ${deployerAddr}`); + + const cCurvePoolBooster = await ethers.getContractAt( + "CurvePoolBooster", + addresses.zero + ); + const cCurvePoolBoosterProxy = await ethers.getContractAt( + "CurvePoolBoosterProxy", + addresses.zero + ); + + console.log("\nStarting deployment using CreateX"); + const rewardToken = addresses.mainnet.OUSDProxy; + const gauge = addresses.mainnet.CurveOUSDUSDTGauge; + const targetedChainId = 42161; // arbitrum + const fee = 0; + + // Get CreateX contract + const cCreateX = await ethers.getContractAt(createxAbi, addresses.createX); + + // Generate salt + const salt = ethers.utils.keccak256( + ethers.utils.concat([ + ethers.utils.arrayify(rewardToken), + ethers.utils.arrayify(gauge), + ethers.utils.arrayify(1), + ]) + ); + + const encodedSalt = encodeSaltForCreateX(deployerAddr, false, salt); + console.log(`Encoded salt: ${encodedSalt}`); + + // --- Deploy implementation --- // + const cachedInitCodeImpl = ethers.utils.concat([ + PoolBoosterBytecode.bytecode, + ethers.utils.defaultAbiCoder.encode( + ["uint256", "address", "address"], + [targetedChainId, rewardToken, gauge] + ), + ]); + const txResponse = await withConfirmation( + cCreateX.connect(sDeployer).deployCreate2(encodedSalt, cachedInitCodeImpl) + ); + const txReceipt = await txResponse.wait(); + // event 0 is GovernorshipTransferred + // event 1 is ContractCreation, topics[1] is the address of the deployed contract, topics[2] is the salt + const implementationAddress = ethers.utils.getAddress( + `0x${txReceipt.events[1].topics[1].slice(26)}` + ); + console.log( + `Curve Booster Implementation deployed at: ${implementationAddress}` + ); + + // --- Deploy and init proxy --- // + const cachedInitCodeProxy = ProxyBytecode.bytecode; // No constructor arguments + const initializeImplem = cCurvePoolBooster.interface.encodeFunctionData( + "initialize(address,uint16,address,address,address)", + [ + addresses.multichainStrategist, // strategist + fee, // fee + addresses.multichainStrategist, // feeCollector + addresses.mainnet.CampaignRemoteManager, // campaignRemoteManager + addresses.votemarket, // votemarket + ] + ); + const initializeProxy = cCurvePoolBoosterProxy.interface.encodeFunctionData( + "initialize(address,address,bytes)", + [ + implementationAddress, // implementation + addresses.mainnet.Timelock, // governor + initializeImplem, // init data + ] + ); + const txResponseProxy = await withConfirmation( + cCreateX + .connect(sDeployer) + .deployCreate2AndInit( + encodedSalt, + cachedInitCodeProxy, + initializeProxy, + ["0x00", "0x00"], + deployerAddr + ) + ); + const txReceiptProxy = await txResponseProxy.wait(); + // event 0 is GovernorshipTransferred + // event 1 is ContractCreation, topics[1] is the address of the deployed contract, topics[2] is the salt + // event 2 is StrategistUpdated + // event 3 is FeeUpdated + // event 4 is FeeCollectorUpdated + // event 5 is CampaignRemoteManagerUpdated + // event 6 is GovernorshipTransferred + const proxyAddress = ethers.utils.getAddress( + `0x${txReceiptProxy.events[1].topics[1].slice(26)}` + ); + console.log(`Curve Booster Proxy deployed at: ${proxyAddress}`); + + await ethers.getContractAt("CurvePoolBooster", proxyAddress); + await ethers.getContractAt("CurvePoolBoosterProxy", proxyAddress); + return {}; + } +); diff --git a/contracts/deployments/arbitrumOne/.migrations.json b/contracts/deployments/arbitrumOne/.migrations.json index 2988389fe2..5e4a841c66 100644 --- a/contracts/deployments/arbitrumOne/.migrations.json +++ b/contracts/deployments/arbitrumOne/.migrations.json @@ -1,4 +1,5 @@ { "001_deploy_woeth_on_arb": 1707820141, - "002_upgrade_woeth_on_arb": 1710959437 + "002_upgrade_woeth_on_arb": 1710959437, + "003_pool_booster_curve": 1737965774 } \ No newline at end of file diff --git a/contracts/deployments/mainnet/.migrations.json b/contracts/deployments/mainnet/.migrations.json index ab6fe75e6e..3ae6e736d2 100644 --- a/contracts/deployments/mainnet/.migrations.json +++ b/contracts/deployments/mainnet/.migrations.json @@ -105,5 +105,6 @@ "117_oeth_fixed_rate_dripper": 1736875175, "118_multichain_strategist": 1736875175, "119_multisig_as_canceller": 1737991984, - "120_remove_ousd_amo": 1737992146 + "120_remove_ousd_amo": 1737992146, + "121_pool_booster_curve": 1737965774 } diff --git a/contracts/fork-test.sh b/contracts/fork-test.sh index 8f631100ff..d0e82f1594 100755 --- a/contracts/fork-test.sh +++ b/contracts/fork-test.sh @@ -77,7 +77,7 @@ main() # Run specific files when a param is given if [[ ! -z "$1" ]]; then - params+="$@" + params+="--testfiles $@" fi # Add trace flag if enabled @@ -89,7 +89,7 @@ main() if [[ $is_coverage == "true" ]]; then echo "Running tests and generating coverage reports..." - FORK=true IS_TEST=true npx --no-install hardhat coverage --testfiles "${params[@]}" + FORK=true IS_TEST=true npx --no-install hardhat coverage "${params[@]}" else echo "Running fork tests..." FORK=true IS_TEST=true npx --no-install hardhat test ${params[@]} diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index b1f8322630..b50093de9e 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -646,6 +646,13 @@ const defaultFixture = deployments.createFixture(async () => { morphoGauntletPrimeUSDTStrategyProxy.address ); + const curvePoolBooster = isFork + ? await ethers.getContractAt( + "CurvePoolBooster", + "0xF4c001dfe53C584425d7943395C7E57b10BD1DC8" // hardcoded as generated with CreateX. + ) + : undefined; + const simpleOETHHarvester = isFork ? await ethers.getContract("OETHHarvesterSimple") : undefined; @@ -1120,6 +1127,7 @@ const defaultFixture = deployments.createFixture(async () => { morphoGauntletPrimeUSDCVault, morphoGauntletPrimeUSDTStrategy, morphoGauntletPrimeUSDTVault, + curvePoolBooster, simpleOETHHarvester, // Flux strategy diff --git a/contracts/test/harvest/simple-harvester.mainnet.fork-test.js b/contracts/test/harvest/simple-harvester.mainnet.fork-test.js index f1d04134bf..a5d26af98b 100644 --- a/contracts/test/harvest/simple-harvester.mainnet.fork-test.js +++ b/contracts/test/harvest/simple-harvester.mainnet.fork-test.js @@ -32,11 +32,6 @@ describe("ForkTest: SimpleHarvester", function () { it("Should support Strategy as governor", async () => { const { simpleOETHHarvester, timelock } = fixture; - expect( - await simpleOETHHarvester.supportedStrategies( - addresses.mainnet.ConvexOETHAMOStrategy - ) - ).to.be.equal(false); await simpleOETHHarvester .connect(timelock) .setSupportedStrategy(addresses.mainnet.ConvexOETHAMOStrategy, true); @@ -50,11 +45,6 @@ describe("ForkTest: SimpleHarvester", function () { it("Should support Strategy as strategist", async () => { const { simpleOETHHarvester, strategist } = fixture; - expect( - await simpleOETHHarvester - .connect(strategist) - .supportedStrategies(addresses.mainnet.ConvexOETHAMOStrategy) - ).to.be.equal(false); await simpleOETHHarvester .connect(strategist) .setSupportedStrategy(addresses.mainnet.ConvexOETHAMOStrategy, true); @@ -138,6 +128,10 @@ describe("ForkTest: SimpleHarvester", function () { it("Should revert if strategy is not authorized", async () => { const { simpleOETHHarvester, convexEthMetaStrategy, timelock } = fixture; + await simpleOETHHarvester + .connect(timelock) + .setSupportedStrategy(addresses.mainnet.ConvexOETHAMOStrategy, false); + await expect( // prettier-ignore simpleOETHHarvester diff --git a/contracts/test/strategies/curvePoolBooster.mainnet.fork-test.js b/contracts/test/strategies/curvePoolBooster.mainnet.fork-test.js new file mode 100644 index 0000000000..701e58eabc --- /dev/null +++ b/contracts/test/strategies/curvePoolBooster.mainnet.fork-test.js @@ -0,0 +1,392 @@ +const { expect } = require("chai"); +const { parseUnits } = require("ethers/lib/utils"); +const { impersonateAndFund } = require("../../utils/signers.js"); + +const addresses = require("../../utils/addresses"); +const { isCI } = require("../helpers"); + +const { loadDefaultFixture } = require("../_fixture"); + +describe("ForkTest: CurvePoolBooster", function () { + this.timeout(0); + + // Retry up to 3 times on CI + this.retries(isCI ? 3 : 0); + + let fixture, + curvePoolBooster, + sStrategist, + ousd, + wousd, + woethSigner, + josh, + sGov; + beforeEach(async () => { + fixture = await loadDefaultFixture(); + curvePoolBooster = fixture.curvePoolBooster; + ousd = fixture.ousd; + wousd = fixture.wousd; + josh = fixture.josh; + + // Set Campaign Id to 0 + const { multichainStrategistAddr } = await getNamedAccounts(); + sStrategist = await ethers.provider.getSigner(multichainStrategistAddr); + const gov = await curvePoolBooster.governor(); + sGov = await ethers.provider.getSigner(gov); + woethSigner = await impersonateAndFund(wousd.address); + + await curvePoolBooster.connect(sStrategist).setCampaignId(0); + }); + + async function dealOETHAndCreateCampaign() { + // Deal OETH to pool booster + await ousd + .connect(woethSigner) + .transfer(curvePoolBooster.address, parseUnits("10")); + expect(await ousd.balanceOf(curvePoolBooster.address)).to.equal( + parseUnits("10") + ); + + // Deal ETH to pool booster + await sStrategist.sendTransaction({ + to: curvePoolBooster.address, + value: parseUnits("1"), + }); + + // Create campaign + await curvePoolBooster + .connect(sStrategist) + .createCampaign( + 4, + 10, + [addresses.mainnet.ConvexVoter], + parseUnits("0.1"), + 0 + ); + } + + // --- Initialization --- + it("Should have correct params", async () => { + expect(await curvePoolBooster.gauge()).to.equal( + addresses.mainnet.CurveOUSDUSDTGauge + ); + expect(await curvePoolBooster.campaignRemoteManager()).to.equal( + addresses.mainnet.CampaignRemoteManager + ); + expect(await curvePoolBooster.rewardToken()).to.equal( + addresses.mainnet.OUSDProxy + ); + expect(await curvePoolBooster.targetChainId()).to.equal(42161); + expect(await curvePoolBooster.strategistAddr()).to.equal( + sStrategist._address + ); + expect(await curvePoolBooster.governor()).to.equal( + addresses.mainnet.Timelock + ); + expect(await curvePoolBooster.campaignRemoteManager()).to.equal( + addresses.mainnet.CampaignRemoteManager + ); + }); + + // --- Campaign Management --- + it("Should Create a campaign", async () => { + await dealOETHAndCreateCampaign(); + + expect(await ousd.balanceOf(curvePoolBooster.address)).to.equal( + parseUnits("0") + ); + }); + + it("Should Create a campaign with fee", async () => { + // Set fee and fee collector + await curvePoolBooster.connect(sGov).setFee(1000); // 10% + await curvePoolBooster.connect(sGov).setFeeCollector(josh.address); + expect(await ousd.balanceOf(josh.address)).to.equal(0); + + // Deal OETH and create campaign + await dealOETHAndCreateCampaign(); + + // Ensure fee is collected + expect(await ousd.balanceOf(josh.address)).to.equal(parseUnits("1")); + }); + + it("Should manage total rewards", async () => { + await dealOETHAndCreateCampaign(); + + // Deal new OETH to pool booster + await ousd + .connect(woethSigner) + .transfer(curvePoolBooster.address, parseUnits("13")); + expect(await ousd.balanceOf(curvePoolBooster.address)).to.equal( + parseUnits("13") + ); + + await curvePoolBooster.connect(sStrategist).setCampaignId(12); + + await curvePoolBooster + .connect(sStrategist) + .manageTotalRewardAmount(parseUnits("0.1"), 0); + expect(await ousd.balanceOf(curvePoolBooster.address)).to.equal( + parseUnits("0") + ); + }); + + it("Should manage number of periods", async () => { + await dealOETHAndCreateCampaign(); + + await curvePoolBooster.connect(sStrategist).setCampaignId(12); + + await curvePoolBooster + .connect(sStrategist) + .manageNumberOfPeriods(2, parseUnits("0.1"), 0); + }); + + it("Should manage reward per voter", async () => { + await dealOETHAndCreateCampaign(); + + // Deal new OETH to pool booster + await ousd + .connect(woethSigner) + .transfer(curvePoolBooster.address, parseUnits("13")); + expect(await ousd.balanceOf(curvePoolBooster.address)).to.equal( + parseUnits("13") + ); + + await curvePoolBooster.connect(sStrategist).setCampaignId(12); + + await curvePoolBooster + .connect(sStrategist) + .manageRewardPerVote(100, parseUnits("0.1"), 0); + }); + + it("Should close a campaing", async () => { + await dealOETHAndCreateCampaign(); + + await curvePoolBooster + .connect(sStrategist) + .closeCampaign(12, parseUnits("0.1"), 0); + }); + + it("Should revert if not called by operator", async () => { + await expect( + curvePoolBooster.createCampaign( + 4, + 10, + [addresses.mainnet.ConvexVoter], + parseUnits("0.1"), + 0 + ) + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + await expect( + curvePoolBooster.manageTotalRewardAmount(parseUnits("0.1"), 0) + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + await expect( + curvePoolBooster.manageNumberOfPeriods(2, parseUnits("0.1"), 0) + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + await expect( + curvePoolBooster.manageRewardPerVote(100, parseUnits("0.1"), 0) + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + await expect(curvePoolBooster.setCampaignId(12)).to.be.revertedWith( + "Caller is not the Strategist or Governor" + ); + }); + + it("Should revert if campaign is already created", async () => { + await curvePoolBooster.connect(sStrategist).setCampaignId(12); + + await expect( + curvePoolBooster + .connect(sStrategist) + .createCampaign( + 4, + 10, + [addresses.mainnet.ConvexVoter], + parseUnits("0.1"), + 0 + ) + ).to.be.revertedWith("Campaign already created"); + }); + + it("Should revert if campaign is not created", async () => { + await expect( + curvePoolBooster + .connect(sStrategist) + .manageTotalRewardAmount(parseUnits("0.1"), 0) + ).to.be.revertedWith("Campaign not created"); + await expect( + curvePoolBooster + .connect(sStrategist) + .manageNumberOfPeriods(2, parseUnits("0.1"), 0) + ).to.be.revertedWith("Campaign not created"); + await expect( + curvePoolBooster + .connect(sStrategist) + .manageRewardPerVote(100, parseUnits("0.1"), 0) + ).to.be.revertedWith("Campaign not created"); + }); + + it("Should revert if Invalid number of periods", async () => { + await expect( + curvePoolBooster + .connect(sStrategist) + .createCampaign( + 0, + 10, + [addresses.mainnet.ConvexVoter], + parseUnits("0.1"), + 0 + ) + ).to.be.revertedWith("Invalid number of periods"); + + await curvePoolBooster.connect(sStrategist).setCampaignId(12); + + await expect( + curvePoolBooster + .connect(sStrategist) + .manageNumberOfPeriods(0, parseUnits("0.1"), 0) + ).to.be.revertedWith("Invalid number of periods"); + }); + + it("Should revert if Invalid reward per vote", async () => { + await expect( + curvePoolBooster + .connect(sStrategist) + .createCampaign( + 4, + 0, + [addresses.mainnet.ConvexVoter], + parseUnits("0.1"), + 0 + ) + ).to.be.revertedWith("Invalid reward per vote"); + + await curvePoolBooster.connect(sStrategist).setCampaignId(12); + + await expect( + curvePoolBooster + .connect(sStrategist) + .manageRewardPerVote(0, parseUnits("0.1"), 0) + ).to.be.revertedWith("Invalid reward per vote"); + }); + + it("Should revert if No reward to manage", async () => { + await expect( + curvePoolBooster + .connect(sStrategist) + .createCampaign( + 4, + 10, + [addresses.mainnet.ConvexVoter], + parseUnits("0.1"), + 0 + ) + ).to.be.revertedWith("No reward to manage"); + + await curvePoolBooster.connect(sStrategist).setCampaignId(12); + + await expect( + curvePoolBooster.connect(sStrategist).manageTotalRewardAmount(0, 0) + ).to.be.revertedWith("No reward to manage"); + }); + + // --- Rescue ETH and ERC20 --- + it("Should rescue ETH", async () => { + // Deal ETH to pool booster + await sStrategist.sendTransaction({ + to: curvePoolBooster.address, + value: parseUnits("1"), + }); + + const balanceBefore = await ethers.provider.getBalance( + curvePoolBooster.address + ); + await curvePoolBooster.connect(sStrategist).rescueETH(sStrategist._address); + const balanceAfter = await ethers.provider.getBalance( + curvePoolBooster.address + ); + expect(balanceBefore).to.be.gte(parseUnits("1")); + expect(balanceAfter).to.be.eq(parseUnits("0")); + }); + + it("Should rescue ERC20", async () => { + // Deal OETH to pool booster + await ousd + .connect(woethSigner) + .transfer(curvePoolBooster.address, parseUnits("10")); + expect(await ousd.balanceOf(curvePoolBooster.address)).to.equal( + parseUnits("10") + ); + + await curvePoolBooster + .connect(sGov) + .rescueToken(ousd.address, sStrategist._address); + expect(await ousd.balanceOf(curvePoolBooster.address)).to.equal(0); + }); + + it("Should revert if receiver is invalid", async () => { + await expect( + curvePoolBooster.connect(sGov).rescueToken(ousd.address, addresses.zero) + ).to.be.revertedWith("Invalid receiver"); + + await expect( + curvePoolBooster.connect(sGov).rescueETH(addresses.zero) + ).to.be.revertedWith("Invalid receiver"); + }); + + // --- Setters --- + it("Should set campaign id", async () => { + expect(await curvePoolBooster.campaignId()).to.equal(0); + + await curvePoolBooster.connect(sStrategist).setCampaignId(12); + expect(await curvePoolBooster.campaignId()).to.equal(12); + }); + + it("Should set fee and fee collector", async () => { + expect(await curvePoolBooster.fee()).to.equal(0); + + await curvePoolBooster.connect(sGov).setFee(100); + expect(await curvePoolBooster.fee()).to.equal(100); + + expect(await curvePoolBooster.feeCollector()).not.to.equal(josh.address); + await curvePoolBooster.connect(sGov).setFeeCollector(josh.address); + expect(await curvePoolBooster.feeCollector()).to.equal(josh.address); + }); + + it("Should revert if fee too high", async () => { + await expect( + curvePoolBooster.connect(sGov).setFee(10000) + ).to.be.revertedWith("Fee too high"); + }); + + it("Should set Campaign Remote Manager", async () => { + expect(await curvePoolBooster.campaignRemoteManager()).to.equal( + addresses.mainnet.CampaignRemoteManager + ); + + await curvePoolBooster.connect(sGov).setCampaignRemoteManager(josh.address); + + expect(await curvePoolBooster.campaignRemoteManager()).to.equal( + josh.address + ); + }); + + it("Should revert if campaign remote manager is invalid", async () => { + await expect( + curvePoolBooster.connect(sGov).setCampaignRemoteManager(addresses.zero) + ).to.be.revertedWith("Invalid campaignRemoteManager"); + }); + + it("Should set Votemarket address", async () => { + expect(await curvePoolBooster.votemarket()).to.equal(addresses.votemarket); + + await curvePoolBooster.connect(sGov).setVotemarket(josh.address); + + expect(await curvePoolBooster.votemarket()).to.equal(josh.address); + }); + + it("Should revert if votemarket is invalid", async () => { + await expect( + curvePoolBooster.connect(sGov).setVotemarket(addresses.zero) + ).to.be.revertedWith("Invalid votemarket"); + }); +}); diff --git a/contracts/test/strategies/ousd-metapool-3crv-tilted-pool.mainnet.fork-test.js b/contracts/test/strategies/ousd-metapool-3crv-tilted-pool.mainnet.fork-test.js index 373e57c78f..c343536c15 100644 --- a/contracts/test/strategies/ousd-metapool-3crv-tilted-pool.mainnet.fork-test.js +++ b/contracts/test/strategies/ousd-metapool-3crv-tilted-pool.mainnet.fork-test.js @@ -4,7 +4,8 @@ const { units, ousdUnits, isCI } = require("../helpers"); const { createFixtureLoader } = require("../_fixture"); const { withCRV3TitledOUSDMetapool } = require("../_metastrategies-fixtures"); -describe("ForkTest: Convex 3pool/OUSD Meta Strategy - Titled to 3CRV", function () { +// The OUSD AMO has been removed +describe.skip("ForkTest: Convex 3pool/OUSD Meta Strategy - Titled to 3CRV", function () { this.timeout(0); // Retry up to 3 times on CI diff --git a/contracts/test/strategies/ousd-metapool-balanced-pool.mainnet.fork-test.js b/contracts/test/strategies/ousd-metapool-balanced-pool.mainnet.fork-test.js index 8b0ff510e3..6798ed65d5 100644 --- a/contracts/test/strategies/ousd-metapool-balanced-pool.mainnet.fork-test.js +++ b/contracts/test/strategies/ousd-metapool-balanced-pool.mainnet.fork-test.js @@ -7,7 +7,8 @@ const { withBalancedOUSDMetaPool } = require("../_metastrategies-fixtures"); const log = require("../../utils/logger")("test:fork:ousd:metapool"); -describe("ForkTest: Convex 3pool/OUSD Meta Strategy - Balanced Metapool", function () { +// The OUSD AMO has been removed +describe.skip("ForkTest: Convex 3pool/OUSD Meta Strategy - Balanced Metapool", function () { this.timeout(0); // Retry up to 3 times on CI diff --git a/contracts/test/strategies/ousd-metapool-ousd-tilted-pool.mainnet.fork-test.js b/contracts/test/strategies/ousd-metapool-ousd-tilted-pool.mainnet.fork-test.js index ad51913ae8..21f425995f 100644 --- a/contracts/test/strategies/ousd-metapool-ousd-tilted-pool.mainnet.fork-test.js +++ b/contracts/test/strategies/ousd-metapool-ousd-tilted-pool.mainnet.fork-test.js @@ -4,7 +4,8 @@ const { units, ousdUnits, isCI } = require("../helpers"); const { createFixtureLoader } = require("../_fixture"); const { withOUSDTitledMetapool } = require("../_metastrategies-fixtures"); -describe("ForkTest: Convex 3pool/OUSD Meta Strategy - Titled to OUSD", function () { +// The OUSD AMO has been removed +describe.skip("ForkTest: Convex 3pool/OUSD Meta Strategy - Titled to OUSD", function () { this.timeout(0); // Retry up to 3 times on CI diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index c6ea7aa852..9a2aedae5e 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -4,6 +4,9 @@ const addresses = {}; addresses.zero = "0x0000000000000000000000000000000000000000"; addresses.dead = "0x0000000000000000000000000000000000000001"; addresses.ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; +addresses.createX = "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed"; +addresses.multichainStrategist = "0x4FF1b9D9ba8558F5EAfCec096318eA0d8b541971"; +addresses.votemarket = "0x5e5C922a5Eeab508486eB906ebE7bDFFB05D81e5"; addresses.mainnet = {}; addresses.holesky = {}; @@ -155,6 +158,11 @@ addresses.mainnet.CurveLUSDMetaPool = addresses.mainnet.ConvexOUSDAMOStrategy = "0x89Eb88fEdc50FC77ae8a18aAD1cA0ac27f777a90"; addresses.mainnet.CurveOUSDGauge = "0x25f0cE4E2F8dbA112D9b115710AC297F816087CD"; +addresses.mainnet.ConvexVoter = "0x989AEb4d175e16225E39E87d0D97A3360524AD80"; +addresses.mainnet.CurveOUSDUSDTPool = + "0x37715d41ee0af05e77ad3a434a11bbff473efe41"; +addresses.mainnet.CurveOUSDUSDTGauge = + "0x74231E4d96498A30FCEaf9aACCAbBD79339Ecd7f"; // Curve OETH/ETH pool addresses.mainnet.ConvexOETHAMOStrategy = @@ -165,6 +173,10 @@ addresses.mainnet.CurveOETHGauge = "0xd03BE91b1932715709e18021734fcB91BB431715"; addresses.mainnet.CVXETHRewardsPool = "0x24b65DC1cf053A8D96872c323d29e86ec43eB33A"; +// Votemarket - StakeDAO +addresses.mainnet.CampaignRemoteManager = + "0x000000009dF57105d76B059178989E01356e4b45"; + // Morpho addresses.mainnet.MorphoStrategyProxy = "0x5A4eEe58744D1430876d5cA93cAB5CcB763C037D"; @@ -287,6 +299,7 @@ addresses.mainnet.LidoWithdrawalQueue = // Arbitrum One addresses.arbitrumOne = {}; addresses.arbitrumOne.WOETHProxy = "0xD8724322f44E5c58D7A815F542036fb17DbbF839"; +addresses.arbitrumOne.admin = "0xfD1383fb4eE74ED9D83F2cbC67507bA6Eac2896a"; // Base addresses.base = {}; diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index 46a2d65c87..ed8155a93d 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -1608,6 +1608,34 @@ function deploymentWithGuardianGovernor(opts, fn) { return main; } +function encodeSaltForCreateX(deployer, crossChainProtectionFlag, salt) { + // Generate encoded salt (deployer address || crossChainProtectionFlag || bytes11(keccak256(rewardToken, gauge))) + + // convert deployer address to bytes20 + const addressDeployerBytes20 = ethers.utils.hexlify( + ethers.utils.zeroPad(deployer, 20) + ); + + // convert crossChainProtectionFlag to bytes1 + const crossChainProtectionFlagBytes1 = crossChainProtectionFlag + ? "0x01" + : "0x00"; + + // convert salt to bytes11 + const saltBytes11 = "0x" + salt.slice(2, 24); + + // concat all bytes into a bytes32 + const encodedSalt = ethers.utils.hexlify( + ethers.utils.concat([ + addressDeployerBytes20, + crossChainProtectionFlagBytes1, + saltBytes11, + ]) + ); + + return encodedSalt; +} + module.exports = { log, sleep, @@ -1622,4 +1650,5 @@ module.exports = { deploymentWithGuardianGovernor, handleTransitionGovernance, + encodeSaltForCreateX, };