From f48634989eb875f72899abd1ce7385510ed52682 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 5 May 2026 08:42:28 +0200 Subject: [PATCH 1/5] initial commit of the hydrex AMO --- contracts/contracts/mocks/MockHydrexGauge.sol | 97 ++++++ contracts/contracts/proxies/Proxies.sol | 7 + .../hydrex/OETHbHydrexAMOStrategy.sol | 20 ++ contracts/contracts/strategies/hydrex/TODO.md | 49 ++++ contracts/deploy/base/048_oethb_hydrex_amo.js | 67 +++++ contracts/deploy/deployActions.js | 43 +++ contracts/test/_fixture-base.js | 276 +++++++++++++++++- .../base/oethb-hydrex-amo.base.fork-test.js | 133 +++++++++ contracts/utils/addresses.js | 11 + 9 files changed, 702 insertions(+), 1 deletion(-) create mode 100644 contracts/contracts/mocks/MockHydrexGauge.sol create mode 100644 contracts/contracts/strategies/hydrex/OETHbHydrexAMOStrategy.sol create mode 100644 contracts/contracts/strategies/hydrex/TODO.md create mode 100644 contracts/deploy/base/048_oethb_hydrex_amo.js create mode 100644 contracts/test/strategies/base/oethb-hydrex-amo.base.fork-test.js diff --git a/contracts/contracts/mocks/MockHydrexGauge.sol b/contracts/contracts/mocks/MockHydrexGauge.sol new file mode 100644 index 0000000000..d2f11072cc --- /dev/null +++ b/contracts/contracts/mocks/MockHydrexGauge.sol @@ -0,0 +1,97 @@ +// 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"; + +/** + * @title MockHydrexGauge + * @notice Test-only mock implementing the subset of the IAlgebraGauge interface + * that StableSwapAMMStrategy and the AMO behavior suite invoke + * (TOKEN, balanceOf, totalSupply, deposit, withdraw, getReward, + * emergency, emergencyWithdraw, owner, DISTRIBUTION, + * notifyRewardAmount). Used by the OETHb Hydrex AMO fork-test fixture + * until Hydrex deploys the real GaugeV2 for the superOETHb/WETH pool. + */ +contract MockHydrexGauge { + using SafeERC20 for IERC20; + + address public immutable TOKEN; + address public immutable rewardToken; + address public immutable owner; + address public immutable DISTRIBUTION; + + mapping(address => uint256) public balanceOf; + uint256 public totalSupply; + bool public emergency; + + /** + * @param _token Pool LP token that gets staked in the gauge. + * @param _rewardToken Reward token (HYDX in production). + * @param _owner Address allowed to toggle emergency mode. + * @param _distribution Address allowed to call notifyRewardAmount. + */ + constructor( + address _token, + address _rewardToken, + address _owner, + address _distribution + ) { + TOKEN = _token; + rewardToken = _rewardToken; + owner = _owner; + DISTRIBUTION = _distribution; + } + + function deposit(uint256 _amount) external { + require(!emergency, "Gauge: emergency"); + IERC20(TOKEN).safeTransferFrom(msg.sender, address(this), _amount); + balanceOf[msg.sender] += _amount; + totalSupply += _amount; + } + + function withdraw(uint256 _amount) external { + require(!emergency, "Gauge: emergency"); + balanceOf[msg.sender] -= _amount; + totalSupply -= _amount; + IERC20(TOKEN).safeTransfer(msg.sender, _amount); + } + + function emergencyWithdraw() external { + uint256 bal = balanceOf[msg.sender]; + balanceOf[msg.sender] = 0; + totalSupply -= bal; + if (bal > 0) { + IERC20(TOKEN).safeTransfer(msg.sender, bal); + } + } + + function getReward() external { + uint256 bal = IERC20(rewardToken).balanceOf(address(this)); + if (bal > 0) { + IERC20(rewardToken).safeTransfer(msg.sender, bal); + } + } + + function activateEmergencyMode() external { + require(msg.sender == owner, "Gauge: not owner"); + emergency = true; + } + + function stopEmergencyMode() external { + require(msg.sender == owner, "Gauge: not owner"); + emergency = false; + } + + /// @notice Matches the real IGauge.notifyRewardAmount(address,uint256) + /// signature so the AMO behavior suite can fund rewards. + function notifyRewardAmount(address _token, uint256 _amount) external { + require(msg.sender == DISTRIBUTION, "Gauge: not distribution"); + require(_token == rewardToken, "Gauge: wrong token"); + IERC20(rewardToken).safeTransferFrom( + msg.sender, + address(this), + _amount + ); + } +} diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index 39f70cec94..8d586b3b71 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -249,3 +249,10 @@ contract OUSDMorphoV2StrategyProxy is InitializeGovernedUpgradeabilityProxy { contract OETHSupernovaAMOProxy is InitializeGovernedUpgradeabilityProxy { } + +/** + * @notice OETHbHydrexAMOProxy delegates calls to an OETHbHydrexAMOStrategy implementation + */ +contract OETHbHydrexAMOProxy is InitializeGovernedUpgradeabilityProxy { + +} diff --git a/contracts/contracts/strategies/hydrex/OETHbHydrexAMOStrategy.sol b/contracts/contracts/strategies/hydrex/OETHbHydrexAMOStrategy.sol new file mode 100644 index 0000000000..fbc5c4c28b --- /dev/null +++ b/contracts/contracts/strategies/hydrex/OETHbHydrexAMOStrategy.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title OETHb Hydrex Algorithmic Market Maker (AMO) Strategy + * @notice AMO strategy for the Hydrex superOETHb/WETH stable pool on Base + * @author Origin Protocol Inc + */ +import { StableSwapAMMStrategy } from "../algebra/StableSwapAMMStrategy.sol"; + +contract OETHbHydrexAMOStrategy is StableSwapAMMStrategy { + /** + * @param _baseConfig The `platformAddress` is the address of the Hydrex superOETHb/WETH pool. + * The `vaultAddress` is the address of the OETHBase Vault. + * @param _gauge Address of the Hydrex gauge for the pool. + */ + constructor(BaseStrategyConfig memory _baseConfig, address _gauge) + StableSwapAMMStrategy(_baseConfig, _gauge) + {} +} diff --git a/contracts/contracts/strategies/hydrex/TODO.md b/contracts/contracts/strategies/hydrex/TODO.md new file mode 100644 index 0000000000..76bd4bfc4f --- /dev/null +++ b/contracts/contracts/strategies/hydrex/TODO.md @@ -0,0 +1,49 @@ +# OETHb Hydrex AMO — open issues + +This file tracks the loose ends from the initial Hydrex AMO PR. **Delete this +file once every item below is resolved.** + +## Blockers (must clear before mainnet deploy) + +- [ ] **Live Hydrex gauge address.** `addresses.base.HydrexOETHb_WETH.gauge` + in `contracts/utils/addresses.js` is currently `0x0000…0000`. Replace with + the real Hydrex gauge once it is deployed for the superOETHb/WETH pool. + - Verify on-chain that it is a `GaugeV2`-shaped staking gauge (exposes + `TOKEN()`, `deposit(uint256)`, `withdraw(uint256)`, `getReward()`, + `emergency()`, `emergencyWithdraw()`), **not** a non-staking + `GaugeIncentiveCampaign` (the `0xac39…9993`-style contract). + - The deploy script `deploy/base/048_oethb_hydrex_amo.js` no-ops while the + placeholder is in place; it will start running its full body the moment a + non-zero gauge address is set. + +- [ ] **HYDX reward token verification.** `addresses.base.HYDX` is + `0x00000e7efa313F4E11Bfff432471eD9423AC6B30` (per docs). Once the gauge is + live, assert `gauge.rewardToken() == addresses.base.HYDX` — ideally with a + `require(...)` in `048_oethb_hydrex_amo.js` so a wrong address fails the + deploy loudly. + +## Cleanup once gauge is live + +- [ ] **Delete the mock gauge.** Once the live gauge address is in + `addresses.base.HydrexOETHb_WETH.gauge`, the fork-test fixture + short-circuits to the live-gauge branch and the mock is no longer touched. + Remove: + - `contracts/contracts/mocks/MockHydrexGauge.sol` + - the `_mockHydrexGaugeIfNeeded` helper in `contracts/test/_fixture-base.js` + - the `if (hydrexGaugeIsMock)` HYDX-allowance block (also in the fixture) + - the `hydrexGaugeIsMock` field from the fixture's return value + - the `USING MOCK HYDREX GAUGE — replace …` warning log + + Fork tests should keep passing with no other changes. + +- [ ] **Tune `scenarioConfig` magnitudes** in + `test/strategies/base/oethb-hydrex-amo.base.fork-test.js` once the pool has + real bootstrapped liquidity. The current numbers were sized for the + fixture-seeded ~150 / 150 pool; live-pool numbers can probably go up. + +## Nice-to-have + +- [ ] Confirm the L2 initialize-time governor pattern in + `deployOETHbHydrexAMOStrategyImplementation` (currently passes + `addresses.base.timelock`) matches whichever convention the rest of + `deploy/base/*` uses for proxy `initialize(impl, governor, data)` calls. diff --git a/contracts/deploy/base/048_oethb_hydrex_amo.js b/contracts/deploy/base/048_oethb_hydrex_amo.js new file mode 100644 index 0000000000..b2ab062e9b --- /dev/null +++ b/contracts/deploy/base/048_oethb_hydrex_amo.js @@ -0,0 +1,67 @@ +const { deployOnBase } = require("../../utils/deploy-l2"); +const addresses = require("../../utils/addresses"); +const { + deployOETHbHydrexAMOStrategyImplementation, +} = require("../deployActions"); + +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + +// BLOCKED: this script is a no-op until `addresses.base.HydrexOETHb_WETH.gauge` +// is set to the live Hydrex GaugeV2 for the superOETHb/WETH pool. Before +// unblocking, also re-verify that `gauge.rewardToken() == addresses.base.HYDX`. +module.exports = deployOnBase( + { + deployName: "048_oethb_hydrex_amo", + }, + async ({ deployWithConfirmation, ethers }) => { + if (addresses.base.HydrexOETHb_WETH.gauge === ZERO_ADDRESS) { + console.log( + "Skipping 048_oethb_hydrex_amo: Hydrex gauge for superOETHb/WETH " + + "is not yet deployed. Set addresses.base.HydrexOETHb_WETH.gauge " + + "to the live gauge address before re-running this deploy." + ); + return { actions: [] }; + } + + // 1. Deploy the OETHb Hydrex AMO proxy + await deployWithConfirmation("OETHbHydrexAMOProxy"); + const cOETHbHydrexAMOProxy = await ethers.getContract( + "OETHbHydrexAMOProxy" + ); + + // 2. Deploy & initialize the OETHb Hydrex AMO strategy implementation + const cOETHbHydrexAMOStrategy = + await deployOETHbHydrexAMOStrategyImplementation(); + + // 3. Connect to the OETHBase Vault as IVault + const cOETHBaseVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); + const cVault = await ethers.getContractAt( + "IVault", + cOETHBaseVaultProxy.address + ); + + return { + name: "Deploy OETHb Hydrex AMO Strategy on Base", + actions: [ + // Approve the strategy on the OETHBase Vault + { + contract: cVault, + signature: "approveStrategy(address)", + args: [cOETHbHydrexAMOProxy.address], + }, + // Allow the strategy to mint OETHb via the Vault + { + contract: cVault, + signature: "addStrategyToMintWhitelist(address)", + args: [cOETHbHydrexAMOProxy.address], + }, + // Set the harvester address on the strategy + { + contract: cOETHbHydrexAMOStrategy, + signature: "setHarvesterAddress(address)", + args: [addresses.base.multichainStrategist], + }, + ], + }; + } +); diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 3bb91cbcbb..d221b4bbe1 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -899,6 +899,48 @@ const deployOETHSupernovaAMOStrategyImplementation = async () => { return cOETHSupernovaAMOStrategy; }; +const deployOETHbHydrexAMOStrategyImplementation = async () => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + const cOETHbHydrexAMOStrategyProxy = await ethers.getContract( + "OETHbHydrexAMOProxy" + ); + const cOETHBaseVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); + + // Deploy OETHb Hydrex AMO Strategy implementation + const dHydrexAMOStrategy = await deployWithConfirmation( + "OETHbHydrexAMOStrategy", + [ + [addresses.base.HydrexOETHb_WETH.pool, cOETHBaseVaultProxy.address], + addresses.base.HydrexOETHb_WETH.gauge, + ] + ); + + const cOETHbHydrexAMOStrategy = await ethers.getContractAt( + "OETHbHydrexAMOStrategy", + cOETHbHydrexAMOStrategyProxy.address + ); + + // Initialize OETHb Hydrex AMO Strategy via the proxy + const depositPriceRange = parseUnits("0.01", 18); // 1% or 100 basis points + const initData = cOETHbHydrexAMOStrategy.interface.encodeFunctionData( + "initialize(address[],uint256)", + [[addresses.base.HYDX], depositPriceRange] + ); + await withConfirmation( + // prettier-ignore + cOETHbHydrexAMOStrategyProxy + .connect(sDeployer)["initialize(address,address,bytes)"]( + dHydrexAMOStrategy.address, + addresses.base.timelock, + initData + ) + ); + + return cOETHbHydrexAMOStrategy; +}; + const getCreate2ProxiesFilePath = async () => { const networkName = isFork || isForkTest || isCI ? "localhost" : await getNetworkName(); @@ -1270,6 +1312,7 @@ module.exports = { deploySonicSwapXAMOStrategyImplementation, deploySonicSwapXAMOStrategyImplementationAndInitialize, deployOETHSupernovaAMOStrategyImplementation, + deployOETHbHydrexAMOStrategyImplementation, deployProxyWithCreateX, deployCrossChainMasterStrategyImpl, deployCrossChainRemoteStrategyImpl, diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index c820f6596f..e5f6e6b4cd 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -1,16 +1,19 @@ const hre = require("hardhat"); const { ethers } = hre; const mocha = require("mocha"); +const { parseUnits, formatUnits } = require("ethers/lib/utils"); const { isFork, isBaseFork, oethUnits, usdcUnits } = require("./helpers"); const { impersonateAndFund, impersonateAccount } = require("../utils/signers"); const { nodeRevert, nodeSnapshot } = require("./_fixture"); -const { deployWithConfirmation } = require("../utils/deploy"); +const { deployWithConfirmation, withConfirmation } = require("../utils/deploy"); const addresses = require("../utils/addresses"); const erc20Abi = require("./abi/erc20.json"); const hhHelpers = require("@nomicfoundation/hardhat-network-helpers"); const log = require("../utils/logger")("test:fixtures-base"); +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + const aeroSwapRouterAbi = require("./abi/aerodromeSwapRouter.json"); const aeroNonfungiblePositionManagerAbi = require("./abi/aerodromeNonfungiblePositionManager.json"); const aerodromeSugarAbi = require("./abi/aerodromeSugarHelper.json"); @@ -363,10 +366,281 @@ mocha.after(async () => { } }); +/** + * Resolve the Hydrex gauge contract for the OETHb/WETH pool. Returns the live + * gauge if `addresses.base.HydrexOETHb_WETH.gauge` is set and has code on the + * fork; otherwise deploys a `MockHydrexGauge`. Until Hydrex deploys the real + * gauge for this pool the fork test always lands on the mock branch. + */ +async function _mockHydrexGaugeIfNeeded(poolAddress, rewardTokenAddress) { + const configured = addresses.base.HydrexOETHb_WETH.gauge; + + if (configured !== ZERO_ADDRESS) { + const code = await ethers.provider.getCode(configured); + if (code && code !== "0x") { + return { + gauge: await ethers.getContractAt("IGauge", configured), + isMock: false, + }; + } + } + + console.warn( + "USING MOCK HYDREX GAUGE — replace addresses.base.HydrexOETHb_WETH.gauge " + + "with the live Hydrex GaugeV2 once it has been deployed for the " + + "superOETHb/WETH pool." + ); + + // Use the timelock as both owner and distribution — both are easy to + // impersonate in tests, and the behavior suite reads them via the gauge. + const { timelockAddr } = await getNamedAccounts(); + await deployWithConfirmation("MockHydrexGauge", [ + poolAddress, + rewardTokenAddress, + timelockAddr, + timelockAddr, + ]); + const cMockGauge = await ethers.getContract("MockHydrexGauge"); + return { + gauge: await ethers.getContractAt("IGauge", cMockGauge.address), + isMock: true, + }; +} + +async function oethbHydrexAMOFixture( + config = { + assetMintAmount: 0, + depositToStrategy: false, + balancePool: false, + poolAddWethAmount: 0, + poolAddOethAmount: 0, + } +) { + if (!isFork || !isBaseFork) { + throw new Error( + "oethbHydrexAMOFixture is only supported on Base fork tests" + ); + } + + const fixture = await defaultFixture(); + const { oethb, oethbVault, weth, governor, strategist, nick } = fixture; + + const cfg = { + assetMintAmount: config?.assetMintAmount || 0, + depositToStrategy: config?.depositToStrategy || false, + balancePool: config?.balancePool || false, + poolAddWethAmount: config?.poolAddWethAmount || 0, + poolAddOethAmount: config?.poolAddOethAmount || 0, + }; + + // Pool already exists on Base. Connect to it. + const hydrexPool = await ethers.getContractAt( + "IPair", + addresses.base.HydrexOETHb_WETH.pool + ); + + // Resolve / mock the gauge. + const { gauge: hydrexGauge, isMock: hydrexGaugeIsMock } = + await _mockHydrexGaugeIfNeeded(hydrexPool.address, addresses.base.HYDX); + + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + await impersonateAndFund(deployerAddr); + + // Deploy proxy + implementation against the (possibly mocked) gauge address. + await deployWithConfirmation("OETHbHydrexAMOProxy"); + const cOETHbHydrexAMOProxy = await ethers.getContract("OETHbHydrexAMOProxy"); + + const dHydrexAMOStrategy = await deployWithConfirmation( + "OETHbHydrexAMOStrategy", + [[hydrexPool.address, oethbVault.address], hydrexGauge.address] + ); + + const cOETHbHydrexAMOStrategy = await ethers.getContractAt( + "OETHbHydrexAMOStrategy", + cOETHbHydrexAMOProxy.address + ); + + // Initialize the proxy (only if not already initialized in a previous run). + const currentImpl = await cOETHbHydrexAMOProxy.implementation(); + if (currentImpl === ZERO_ADDRESS) { + const depositPriceRange = parseUnits("0.01", 18); // 1% / 100 bp + const initData = cOETHbHydrexAMOStrategy.interface.encodeFunctionData( + "initialize(address[],uint256)", + [[addresses.base.HYDX], depositPriceRange] + ); + await withConfirmation( + // prettier-ignore + cOETHbHydrexAMOProxy + .connect(sDeployer)["initialize(address,address,bytes)"]( + dHydrexAMOStrategy.address, + addresses.base.timelock, + initData + ) + ); + } + + // Wire the strategy into the OETHBase Vault. + await withConfirmation( + oethbVault.connect(governor).approveStrategy(cOETHbHydrexAMOProxy.address) + ); + await withConfirmation( + oethbVault + .connect(governor) + .addStrategyToMintWhitelist(cOETHbHydrexAMOProxy.address) + ); + + // Match the deploy script (048_oethb_hydrex_amo): the harvester address on + // the strategy is the multichain strategist multisig, not the SuperOETH + // Harvester proxy. The behavior suite's harvest config also expects the + // strategist to be both the caller and the recipient of reward tokens. + const sStrategyGovernor = await impersonateAndFund(addresses.base.timelock); + await withConfirmation( + cOETHbHydrexAMOStrategy + .connect(sStrategyGovernor) + .setHarvesterAddress(addresses.base.multichainStrategist) + ); + + // Reward token handle (HYDX). + const hydrexRewardToken = await ethers.getContractAt( + erc20Abi, + addresses.base.HYDX + ); + + // The behavior suite calls `gauge.notifyRewardAmount(token, amount)` from + // the impersonated DISTRIBUTION address, which uses `transferFrom`. On a + // real Hydrex Voter→Gauge wiring this allowance is pre-set during gauge + // creation. With our mock we have to set it explicitly. + if (hydrexGaugeIsMock) { + const distributionAddr = await hydrexGauge.DISTRIBUTION(); + const distributionSigner = await impersonateAndFund(distributionAddr); + await hydrexRewardToken + .connect(distributionSigner) + .approve(hydrexGauge.address, ethers.constants.MaxUint256); + } + + // Impersonate the OETHBase Vault so tests can call deposit/withdraw on the + // strategy directly. + const oethbVaultSigner = await impersonateAndFund(oethbVault.address); + + // Ensure `nick` has plenty of WETH to mint OETHb and seed/manipulate pool. + await hhHelpers.setBalance(nick.address, oethUnits("1000000")); + await weth.connect(nick).deposit({ value: oethUnits("500000") }); + + // Seed the pool once if it's effectively empty (Hydrex pool today has dust + // reserves only — ~100 gwei per side). + const seedAmount = parseUnits("150"); + if ((await hydrexPool.totalSupply()).lt(seedAmount.mul(2))) { + await weth.connect(nick).approve(oethbVault.address, seedAmount.mul(2)); + await oethbVault.connect(nick).mint(seedAmount.mul(2)); + await weth.connect(nick).transfer(hydrexPool.address, seedAmount); + await oethb.connect(nick).transfer(hydrexPool.address, seedAmount); + await hydrexPool.connect(nick).mint(nick.address); + } + + // Mint some OETHb using WETH if configured. + if (cfg.assetMintAmount > 0) { + const wethAmount = parseUnits(cfg.assetMintAmount.toString()); + + // Flush any accrued yield into OETHb supply so the protocol sits at + // exactly 1:1 backing before the test mint. The "with an insolvent vault" + // suite assumes a fresh-peg starting state; without rebase first, the + // pre-existing yield buffer absorbs the 21bp loss the suite simulates. + await oethbVault.connect(nick).rebase(); + + let wethBalance = await weth.balanceOf(oethbVault.address); + const queue = await oethbVault.withdrawalQueueMetadata(); + const available = wethBalance.add(queue.claimed).sub(queue.queued); + // Mint 10x the requested amount to dilute the existing OETHb yield buffer. + // Without this, the "with an insolvent vault" suite cannot push the + // protocol below the 0.998 solvency threshold with its 21bp loss because + // pre-existing yield absorbs it. (Same approach as the Sonic fixture.) + const mintAmount = wethAmount.sub(available).mul(10); + + if (mintAmount.gt(0)) { + await weth.connect(nick).approve(oethbVault.address, mintAmount); + await oethbVault.connect(nick).mint(mintAmount); + } + + if (cfg.depositToStrategy) { + wethBalance = await weth.balanceOf(oethbVault.address); + log( + `Depositing ${formatUnits( + wethAmount + )} WETH to OETHb Hydrex AMO strategy. Vault has ${formatUnits( + wethBalance + )} WETH` + ); + await oethbVault + .connect(strategist) + .depositToStrategy( + cOETHbHydrexAMOStrategy.address, + [weth.address], + [wethAmount] + ); + } + } + + if (cfg.balancePool) { + const { _reserve0, _reserve1 } = await hydrexPool.getReserves(); + const oTokenPoolIndex = + (await hydrexPool.token0()) === oethb.address ? 0 : 1; + const assetReserves = oTokenPoolIndex === 0 ? _reserve1 : _reserve0; + const oTokenReserves = oTokenPoolIndex === 0 ? _reserve0 : _reserve1; + + const diff = parseInt( + assetReserves.sub(oTokenReserves).div(oethUnits("1")).toString() + ); + + if (diff > 0) { + cfg.poolAddOethAmount += diff; + } else if (diff < 0) { + cfg.poolAddWethAmount += -diff; + } + } + + // Add WETH to the pool directly. + if (cfg.poolAddWethAmount > 0) { + log(`Adding ${cfg.poolAddWethAmount} WETH to the pool`); + const wethAmount = parseUnits(cfg.poolAddWethAmount.toString(), 18); + await weth.connect(nick).transfer(hydrexPool.address, wethAmount); + } + + // Add OETHb to the pool directly. + if (cfg.poolAddOethAmount > 0) { + log(`Adding ${cfg.poolAddOethAmount} OETHb to the pool`); + const oethAmount = parseUnits(cfg.poolAddOethAmount.toString(), 18); + await weth.connect(nick).approve(oethbVault.address, oethAmount); + await oethbVault.connect(nick).mint(oethAmount); + await oethb.connect(nick).transfer(hydrexPool.address, oethAmount); + } + + await hydrexPool.sync(); + + // The behavior suite uses `.connect(timelock)` and expects a signer. + // The default Base fixture exposes `timelock` as a contract object; replace + // it with the impersonated timelock signer for AMO behavior tests. + const timelockSigner = await impersonateAndFund(addresses.base.timelock); + // JsonRpcSigner does not expose `.address`; behavior tests read it directly. + timelockSigner.address = addresses.base.timelock; + + return { + ...fixture, + timelock: timelockSigner, + oethbVaultSigner, + hydrexRewardToken, + hydrexPool, + hydrexGauge, + hydrexAMOStrategy: cOETHbHydrexAMOStrategy, + hydrexGaugeIsMock, + }; +} + module.exports = { defaultBaseFixture, MINTER_ROLE, BURNER_ROLE, bridgeHelperModuleFixture, crossChainFixture, + oethbHydrexAMOFixture, }; diff --git a/contracts/test/strategies/base/oethb-hydrex-amo.base.fork-test.js b/contracts/test/strategies/base/oethb-hydrex-amo.base.fork-test.js new file mode 100644 index 0000000000..09168d4b3f --- /dev/null +++ b/contracts/test/strategies/base/oethb-hydrex-amo.base.fork-test.js @@ -0,0 +1,133 @@ +const { createFixtureLoader } = require("../../_fixture"); +const { oethbHydrexAMOFixture } = require("../../_fixture-base"); +const { + shouldBehaveLikeAlgebraAmoStrategy, +} = require("../../behaviour/algebraAmoStrategy"); + +describe("Base Fork Test: OETHb Hydrex AMO Strategy", function () { + shouldBehaveLikeAlgebraAmoStrategy(async () => { + // Magnitudes are tuned ~5–10× smaller than the mainnet Supernova test + // because the superOETHb/WETH pool starts with a much smaller bootstrap. + // Ratios are kept the same so every behavioral branch still fires. + const scenarioConfig = { + attackerFrontRun: { + moderateAssetIn: "5", + largeAssetIn: "1000", + largeOTokenIn: "1000", + }, + bootstrapPool: { + smallAssetBootstrapIn: "10", + mediumAssetBootstrapIn: "50", + largeAssetBootstrapIn: "50000", + }, + mintValues: { + extraSmall: "0.1", + extraSmallPlus: "0.2", + small: "1", + medium: "2", + }, + poolImbalance: { + lotMoreOToken: { addOToken: 100 }, + littleMoreOToken: { addOToken: 1 }, + lotMoreAsset: { addAsset: 100 }, + littleMoreAsset: { addAsset: 1 }, + }, + smallPoolShare: { + bootstrapAssetSwapIn: "20", + bigLiquidityAsset: "10", + oTokenBuffer: "20", + stressSwapOToken: "8", + stressSwapAsset: "12", + stressSwapAssetAlt: "8", + }, + rebalanceProbe: { + frontRun: { + depositAmount: "50", + failedDepositAmount: "50", + failedDepositAllAmount: "50", + tiltSeedWithdrawAmount: "15", + assetTiltWithdrawAmount: "10", + oTokenTiltWithdrawAmount: "0.001", + }, + lotMoreOToken: { + failedDepositAmount: "50", + partialWithdrawAmount: "4", + smallSwapAssetsToPool: "0.3", + largeSwapAssetsToPool: "4", + nearMaxSwapAssetsToPool: "7", + excessiveSwapAssetsToPool: "500", + disallowedSwapOTokensToPool: "0.0001", + }, + littleMoreOToken: { + depositAmount: "3", + partialWithdrawAmount: "2", + smallSwapAssetsToPool: "0.3", + excessiveSwapAssetsToPool: "12", + disallowedSwapOTokensToPool: "0.0001", + }, + lotMoreAsset: { + failedDepositAmount: "15", + partialWithdrawAmount: "2", + smallSwapOTokensToPool: "0.03", + largeSwapOTokensToPool: "12", + overshootSwapOTokensToPool: "90", + disallowedSwapAssetsToPool: "0.00001", + }, + littleMoreAsset: { + depositAmount: "4", + partialWithdrawAmount: "2", + smallSwapOTokensToPool: "0.2", + overshootSwapOTokensToPool: "30", + disallowedSwapAssetsToPool: "0.00001", + }, + }, + insolvent: { + swapOTokensToPool: "0.1", + }, + harvest: { + collectedBy: "strategist", + }, + }; + + return { + scenarioConfig, + loadFixture: async ({ + assetMintAmount = 0, + depositToStrategy = false, + balancePool = false, + poolAddAssetAmount = 0, + poolAddOTokenAmount = 0, + } = {}) => { + const fixtureLoader = await createFixtureLoader(oethbHydrexAMOFixture, { + assetMintAmount, + depositToStrategy, + balancePool, + poolAddWethAmount: poolAddAssetAmount, + poolAddOethAmount: poolAddOTokenAmount, + }); + + const fixture = await fixtureLoader(); + const oTokenPoolIndex = + (await fixture.hydrexPool.token0()) === fixture.oethb.address ? 0 : 1; + + return { + assetToken: fixture.weth, + oToken: fixture.oethb, + rewardToken: fixture.hydrexRewardToken, + amoStrategy: fixture.hydrexAMOStrategy, + pool: fixture.hydrexPool, + gauge: fixture.hydrexGauge, + governor: fixture.timelock, + timelock: fixture.timelock, + strategist: fixture.strategist, + nick: fixture.nick, + oTokenPoolIndex, + vaultSigner: fixture.oethbVaultSigner, + vault: fixture.oethbVault, + harvester: fixture.harvester, + scenarioConfig, + }; + }, + }; + }); +}); diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 78eec1b5b0..b53c00c781 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -467,6 +467,17 @@ addresses.base.OETHb_WETH.gauge = "0x9da8420dbEEBDFc4902B356017610259ef7eeDD8"; addresses.base.childLiquidityGaugeFactory = "0xe35A879E5EfB4F1Bb7F70dCF3250f2e19f096bd8"; +// Base Hydrex +addresses.base.HYDX = "0x00000e7efa313F4E11Bfff432471eD9423AC6B30"; +addresses.base.hydrexVoter = "0xc69E3eF39E3fFBcE2A1c570f8d3ADF76909ef17b"; +addresses.base.HydrexOETHb_WETH = {}; +addresses.base.HydrexOETHb_WETH.pool = + "0xEB9ebc2dEF5aa715C0CED10749cbdC15Ac27f632"; +// TODO: replace with the live Hydrex gauge once it has been deployed for the +// superOETHb/WETH pool. Re-verify that gauge.rewardToken() == HYDX before deploy. +addresses.base.HydrexOETHb_WETH.gauge = + "0x0000000000000000000000000000000000000000"; + addresses.base.CCIPRouter = "0x881e3A65B4d4a04dD529061dd0071cf975F58bCD"; addresses.base.MerklDistributor = "0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd"; From ff7d545c9f6cddcc34fac77097ef1830a7727ec7 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 5 May 2026 11:37:40 +0200 Subject: [PATCH 2/5] move gauge mock to deploy file --- contracts/deploy/base/048_oethb_hydrex_amo.js | 46 ++++-- contracts/deploy/deployActions.js | 9 +- contracts/test/_fixture-base.js | 156 ++++++------------ 3 files changed, 92 insertions(+), 119 deletions(-) diff --git a/contracts/deploy/base/048_oethb_hydrex_amo.js b/contracts/deploy/base/048_oethb_hydrex_amo.js index b2ab062e9b..99a5baa9c0 100644 --- a/contracts/deploy/base/048_oethb_hydrex_amo.js +++ b/contracts/deploy/base/048_oethb_hydrex_amo.js @@ -1,26 +1,49 @@ const { deployOnBase } = require("../../utils/deploy-l2"); const addresses = require("../../utils/addresses"); +const { isBaseForkTest } = require("../../utils/hardhat-helpers"); const { deployOETHbHydrexAMOStrategyImplementation, } = require("../deployActions"); const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; -// BLOCKED: this script is a no-op until `addresses.base.HydrexOETHb_WETH.gauge` -// is set to the live Hydrex GaugeV2 for the superOETHb/WETH pool. Before -// unblocking, also re-verify that `gauge.rewardToken() == addresses.base.HYDX`. module.exports = deployOnBase( { deployName: "048_oethb_hydrex_amo", }, async ({ deployWithConfirmation, ethers }) => { - if (addresses.base.HydrexOETHb_WETH.gauge === ZERO_ADDRESS) { - console.log( - "Skipping 048_oethb_hydrex_amo: Hydrex gauge for superOETHb/WETH " + - "is not yet deployed. Set addresses.base.HydrexOETHb_WETH.gauge " + - "to the live gauge address before re-running this deploy." + let gaugeAddress = addresses.base.HydrexOETHb_WETH.gauge; + + // Mock gauge fallback path. ONLY allowed in Base fork tests. Any other + // Base context (live mainnet deploy, local fork node, anything where + // IS_TEST !== "true") must hard-fail so a strategy backed by a mock or + // zero gauge can never escape into production. + if (gaugeAddress === ZERO_ADDRESS) { + if (!isBaseForkTest) { + throw new Error( + "Hydrex gauge for superOETHb/WETH is not deployed yet. Refusing " + + "to deploy the strategy with a placeholder/mock gauge outside of " + + "Base fork tests. Set addresses.base.HydrexOETHb_WETH.gauge to " + + "the live Hydrex GaugeV2 (and confirm gauge.rewardToken() == " + + "addresses.base.HYDX) before running this deploy." + ); + } + + console.warn( + "USING MOCK HYDREX GAUGE — replace addresses.base.HydrexOETHb_WETH.gauge " + + "with the live Hydrex GaugeV2 once it has been deployed for the " + + "superOETHb/WETH pool." ); - return { actions: [] }; + + const { timelockAddr } = await getNamedAccounts(); + await deployWithConfirmation("MockHydrexGauge", [ + addresses.base.HydrexOETHb_WETH.pool, + addresses.base.HYDX, + timelockAddr, // owner + timelockAddr, // distribution + ]); + const cMockGauge = await ethers.getContract("MockHydrexGauge"); + gaugeAddress = cMockGauge.address; } // 1. Deploy the OETHb Hydrex AMO proxy @@ -29,9 +52,10 @@ module.exports = deployOnBase( "OETHbHydrexAMOProxy" ); - // 2. Deploy & initialize the OETHb Hydrex AMO strategy implementation + // 2. Deploy & initialize the strategy implementation against the resolved + // gauge address (live or mock). const cOETHbHydrexAMOStrategy = - await deployOETHbHydrexAMOStrategyImplementation(); + await deployOETHbHydrexAMOStrategyImplementation(gaugeAddress); // 3. Connect to the OETHBase Vault as IVault const cOETHBaseVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index d221b4bbe1..124e4e3385 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -899,10 +899,15 @@ const deployOETHSupernovaAMOStrategyImplementation = async () => { return cOETHSupernovaAMOStrategy; }; -const deployOETHbHydrexAMOStrategyImplementation = async () => { +const deployOETHbHydrexAMOStrategyImplementation = async (gaugeAddress) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); + // Default to the addresses entry so any other caller still works; the + // 048_oethb_hydrex_amo deploy script always passes the resolved address + // (live gauge or, in fork tests only, a freshly-deployed MockHydrexGauge). + const _gauge = gaugeAddress || addresses.base.HydrexOETHb_WETH.gauge; + const cOETHbHydrexAMOStrategyProxy = await ethers.getContract( "OETHbHydrexAMOProxy" ); @@ -913,7 +918,7 @@ const deployOETHbHydrexAMOStrategyImplementation = async () => { "OETHbHydrexAMOStrategy", [ [addresses.base.HydrexOETHb_WETH.pool, cOETHBaseVaultProxy.address], - addresses.base.HydrexOETHb_WETH.gauge, + _gauge, ] ); diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index e5f6e6b4cd..ce81e4fe21 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -5,7 +5,8 @@ const { parseUnits, formatUnits } = require("ethers/lib/utils"); const { isFork, isBaseFork, oethUnits, usdcUnits } = require("./helpers"); const { impersonateAndFund, impersonateAccount } = require("../utils/signers"); const { nodeRevert, nodeSnapshot } = require("./_fixture"); -const { deployWithConfirmation, withConfirmation } = require("../utils/deploy"); +const { deployWithConfirmation } = require("../utils/deploy"); +const { expect } = require("chai"); const addresses = require("../utils/addresses"); const erc20Abi = require("./abi/erc20.json"); const hhHelpers = require("@nomicfoundation/hardhat-network-helpers"); @@ -366,47 +367,6 @@ mocha.after(async () => { } }); -/** - * Resolve the Hydrex gauge contract for the OETHb/WETH pool. Returns the live - * gauge if `addresses.base.HydrexOETHb_WETH.gauge` is set and has code on the - * fork; otherwise deploys a `MockHydrexGauge`. Until Hydrex deploys the real - * gauge for this pool the fork test always lands on the mock branch. - */ -async function _mockHydrexGaugeIfNeeded(poolAddress, rewardTokenAddress) { - const configured = addresses.base.HydrexOETHb_WETH.gauge; - - if (configured !== ZERO_ADDRESS) { - const code = await ethers.provider.getCode(configured); - if (code && code !== "0x") { - return { - gauge: await ethers.getContractAt("IGauge", configured), - isMock: false, - }; - } - } - - console.warn( - "USING MOCK HYDREX GAUGE — replace addresses.base.HydrexOETHb_WETH.gauge " + - "with the live Hydrex GaugeV2 once it has been deployed for the " + - "superOETHb/WETH pool." - ); - - // Use the timelock as both owner and distribution — both are easy to - // impersonate in tests, and the behavior suite reads them via the gauge. - const { timelockAddr } = await getNamedAccounts(); - await deployWithConfirmation("MockHydrexGauge", [ - poolAddress, - rewardTokenAddress, - timelockAddr, - timelockAddr, - ]); - const cMockGauge = await ethers.getContract("MockHydrexGauge"); - return { - gauge: await ethers.getContractAt("IGauge", cMockGauge.address), - isMock: true, - }; -} - async function oethbHydrexAMOFixture( config = { assetMintAmount: 0, @@ -422,8 +382,14 @@ async function oethbHydrexAMOFixture( ); } + // `defaultFixture()` runs all `deploy/base/*` scripts, including + // 048_oethb_hydrex_amo, which deploys the proxy + implementation, runs + // proxy.initialize, and (via its returned `actions[]`) approves the strategy + // on the vault, adds it to the mint whitelist, and sets the harvester + // address. Anything below this line in the fixture must be a *consumer* of + // that deployment — no additional deploys, no init, no governance calls. const fixture = await defaultFixture(); - const { oethb, oethbVault, weth, governor, strategist, nick } = fixture; + const { oethb, oethbVault, weth, nick, strategist } = fixture; const cfg = { assetMintAmount: config?.assetMintAmount || 0, @@ -433,84 +399,62 @@ async function oethbHydrexAMOFixture( poolAddOethAmount: config?.poolAddOethAmount || 0, }; - // Pool already exists on Base. Connect to it. - const hydrexPool = await ethers.getContractAt( - "IPair", - addresses.base.HydrexOETHb_WETH.pool - ); - - // Resolve / mock the gauge. - const { gauge: hydrexGauge, isMock: hydrexGaugeIsMock } = - await _mockHydrexGaugeIfNeeded(hydrexPool.address, addresses.base.HYDX); - - const { deployerAddr } = await getNamedAccounts(); - const sDeployer = await ethers.provider.getSigner(deployerAddr); - await impersonateAndFund(deployerAddr); - - // Deploy proxy + implementation against the (possibly mocked) gauge address. - await deployWithConfirmation("OETHbHydrexAMOProxy"); + // Connect to what 048_oethb_hydrex_amo deployed. If any of these resolve + // unexpectedly the deploy script regressed — bail with a clear message. const cOETHbHydrexAMOProxy = await ethers.getContract("OETHbHydrexAMOProxy"); - - const dHydrexAMOStrategy = await deployWithConfirmation( - "OETHbHydrexAMOStrategy", - [[hydrexPool.address, oethbVault.address], hydrexGauge.address] - ); - const cOETHbHydrexAMOStrategy = await ethers.getContractAt( "OETHbHydrexAMOStrategy", cOETHbHydrexAMOProxy.address ); - - // Initialize the proxy (only if not already initialized in a previous run). - const currentImpl = await cOETHbHydrexAMOProxy.implementation(); - if (currentImpl === ZERO_ADDRESS) { - const depositPriceRange = parseUnits("0.01", 18); // 1% / 100 bp - const initData = cOETHbHydrexAMOStrategy.interface.encodeFunctionData( - "initialize(address[],uint256)", - [[addresses.base.HYDX], depositPriceRange] - ); - await withConfirmation( - // prettier-ignore - cOETHbHydrexAMOProxy - .connect(sDeployer)["initialize(address,address,bytes)"]( - dHydrexAMOStrategy.address, - addresses.base.timelock, - initData - ) - ); - } - - // Wire the strategy into the OETHBase Vault. - await withConfirmation( - oethbVault.connect(governor).approveStrategy(cOETHbHydrexAMOProxy.address) - ); - await withConfirmation( - oethbVault - .connect(governor) - .addStrategyToMintWhitelist(cOETHbHydrexAMOProxy.address) + const hydrexPool = await ethers.getContractAt( + "IPair", + await cOETHbHydrexAMOStrategy.pool() ); - - // Match the deploy script (048_oethb_hydrex_amo): the harvester address on - // the strategy is the multichain strategist multisig, not the SuperOETH - // Harvester proxy. The behavior suite's harvest config also expects the - // strategist to be both the caller and the recipient of reward tokens. - const sStrategyGovernor = await impersonateAndFund(addresses.base.timelock); - await withConfirmation( - cOETHbHydrexAMOStrategy - .connect(sStrategyGovernor) - .setHarvesterAddress(addresses.base.multichainStrategist) + const hydrexGauge = await ethers.getContractAt( + "IGauge", + await cOETHbHydrexAMOStrategy.gauge() ); - - // Reward token handle (HYDX). const hydrexRewardToken = await ethers.getContractAt( erc20Abi, addresses.base.HYDX ); + // Sanity-check that 048_oethb_hydrex_amo wired the strategy correctly. If + // the deploy script regresses we want this to fail loudly here rather than + // through some downstream behavior-suite assertion. + expect(hydrexPool.address.toLowerCase()).to.equal( + addresses.base.HydrexOETHb_WETH.pool.toLowerCase(), + "Strategy.pool() does not match addresses.base.HydrexOETHb_WETH.pool" + ); + expect(hydrexGauge.address).to.not.equal( + ZERO_ADDRESS, + "Strategy was deployed with a zero gauge address" + ); + expect(await cOETHbHydrexAMOStrategy.harvesterAddress()).to.equal( + addresses.base.multichainStrategist, + "Strategy.harvesterAddress is not the multichain strategist" + ); + expect( + (await oethbVault.strategies(cOETHbHydrexAMOProxy.address)).isSupported + ).to.equal(true, "Strategy was not approved on the OETHBase Vault by 048"); + expect( + await oethbVault.isMintWhitelistedStrategy(cOETHbHydrexAMOProxy.address) + ).to.equal( + true, + "Strategy was not added to OETHBase Vault mint whitelist by 048" + ); + + // Detect whether 048 deployed a MockHydrexGauge. The mock only exists in + // Base fork tests; live deploys use the real gauge. + const mockDeployment = await deployments.getOrNull("MockHydrexGauge"); + const hydrexGaugeIsMock = + !!mockDeployment && + mockDeployment.address.toLowerCase() === hydrexGauge.address.toLowerCase(); + // The behavior suite calls `gauge.notifyRewardAmount(token, amount)` from // the impersonated DISTRIBUTION address, which uses `transferFrom`. On a // real Hydrex Voter→Gauge wiring this allowance is pre-set during gauge - // creation. With our mock we have to set it explicitly. + // creation. The mock has no such pre-set allowance, so set it here. if (hydrexGaugeIsMock) { const distributionAddr = await hydrexGauge.DISTRIBUTION(); const distributionSigner = await impersonateAndFund(distributionAddr); From f562e639b291159b2d7891792bc4bfddfdca5e15 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 5 May 2026 11:37:40 +0200 Subject: [PATCH 3/5] move gauge mock to deploy file --- contracts/test/_fixture-base.js | 33 ++----------- .../base/oethb-hydrex-amo.base.fork-test.js | 47 +++++++++++++++++++ 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index ce81e4fe21..d6f2d10834 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -6,15 +6,12 @@ const { isFork, isBaseFork, oethUnits, usdcUnits } = require("./helpers"); const { impersonateAndFund, impersonateAccount } = require("../utils/signers"); const { nodeRevert, nodeSnapshot } = require("./_fixture"); const { deployWithConfirmation } = require("../utils/deploy"); -const { expect } = require("chai"); const addresses = require("../utils/addresses"); const erc20Abi = require("./abi/erc20.json"); const hhHelpers = require("@nomicfoundation/hardhat-network-helpers"); const log = require("../utils/logger")("test:fixtures-base"); -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; - const aeroSwapRouterAbi = require("./abi/aerodromeSwapRouter.json"); const aeroNonfungiblePositionManagerAbi = require("./abi/aerodromeNonfungiblePositionManager.json"); const aerodromeSugarAbi = require("./abi/aerodromeSugarHelper.json"); @@ -399,8 +396,9 @@ async function oethbHydrexAMOFixture( poolAddOethAmount: config?.poolAddOethAmount || 0, }; - // Connect to what 048_oethb_hydrex_amo deployed. If any of these resolve - // unexpectedly the deploy script regressed — bail with a clear message. + // Connect to what 048_oethb_hydrex_amo deployed. The fork test in + // test/strategies/base/oethb-hydrex-amo.base.fork-test.js asserts that the + // deploy script wired things correctly; here we just consume the result. const cOETHbHydrexAMOProxy = await ethers.getContract("OETHbHydrexAMOProxy"); const cOETHbHydrexAMOStrategy = await ethers.getContractAt( "OETHbHydrexAMOStrategy", @@ -419,31 +417,6 @@ async function oethbHydrexAMOFixture( addresses.base.HYDX ); - // Sanity-check that 048_oethb_hydrex_amo wired the strategy correctly. If - // the deploy script regresses we want this to fail loudly here rather than - // through some downstream behavior-suite assertion. - expect(hydrexPool.address.toLowerCase()).to.equal( - addresses.base.HydrexOETHb_WETH.pool.toLowerCase(), - "Strategy.pool() does not match addresses.base.HydrexOETHb_WETH.pool" - ); - expect(hydrexGauge.address).to.not.equal( - ZERO_ADDRESS, - "Strategy was deployed with a zero gauge address" - ); - expect(await cOETHbHydrexAMOStrategy.harvesterAddress()).to.equal( - addresses.base.multichainStrategist, - "Strategy.harvesterAddress is not the multichain strategist" - ); - expect( - (await oethbVault.strategies(cOETHbHydrexAMOProxy.address)).isSupported - ).to.equal(true, "Strategy was not approved on the OETHBase Vault by 048"); - expect( - await oethbVault.isMintWhitelistedStrategy(cOETHbHydrexAMOProxy.address) - ).to.equal( - true, - "Strategy was not added to OETHBase Vault mint whitelist by 048" - ); - // Detect whether 048 deployed a MockHydrexGauge. The mock only exists in // Base fork tests; live deploys use the real gauge. const mockDeployment = await deployments.getOrNull("MockHydrexGauge"); diff --git a/contracts/test/strategies/base/oethb-hydrex-amo.base.fork-test.js b/contracts/test/strategies/base/oethb-hydrex-amo.base.fork-test.js index 09168d4b3f..a6dc51ccd4 100644 --- a/contracts/test/strategies/base/oethb-hydrex-amo.base.fork-test.js +++ b/contracts/test/strategies/base/oethb-hydrex-amo.base.fork-test.js @@ -1,10 +1,57 @@ +const { expect } = require("chai"); + +const addresses = require("../../../utils/addresses"); const { createFixtureLoader } = require("../../_fixture"); const { oethbHydrexAMOFixture } = require("../../_fixture-base"); const { shouldBehaveLikeAlgebraAmoStrategy, } = require("../../behaviour/algebraAmoStrategy"); +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + describe("Base Fork Test: OETHb Hydrex AMO Strategy", function () { + // Verify the bring-up that 048_oethb_hydrex_amo performs. If the deploy + // script regresses, these fail loudly with a clear message rather than + // surfacing as some downstream behavior-suite assertion. + describe("deploy script wires the strategy correctly", function () { + let fixture; + before(async () => { + const fixtureLoader = await createFixtureLoader(oethbHydrexAMOFixture); + fixture = await fixtureLoader(); + }); + + it("Strategy.pool() matches addresses.base.HydrexOETHb_WETH.pool", async () => { + expect(fixture.hydrexPool.address.toLowerCase()).to.equal( + addresses.base.HydrexOETHb_WETH.pool.toLowerCase() + ); + }); + + it("Strategy.gauge() is non-zero", async () => { + expect(fixture.hydrexGauge.address).to.not.equal(ZERO_ADDRESS); + }); + + it("Strategy.harvesterAddress() is the multichain strategist", async () => { + expect(await fixture.hydrexAMOStrategy.harvesterAddress()).to.equal( + addresses.base.multichainStrategist + ); + }); + + it("OETHBase Vault has approved the strategy", async () => { + const strategyConfig = await fixture.oethbVault.strategies( + fixture.hydrexAMOStrategy.address + ); + expect(strategyConfig.isSupported).to.equal(true); + }); + + it("OETHBase Vault has the strategy on the mint whitelist", async () => { + expect( + await fixture.oethbVault.isMintWhitelistedStrategy( + fixture.hydrexAMOStrategy.address + ) + ).to.equal(true); + }); + }); + shouldBehaveLikeAlgebraAmoStrategy(async () => { // Magnitudes are tuned ~5–10× smaller than the mainnet Supernova test // because the superOETHb/WETH pool starts with a much smaller bootstrap. From 154d3bff0f5a280af4ee2f64ca00808c8889efc2 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 5 May 2026 21:24:47 +0200 Subject: [PATCH 4/5] remove mock gauge --- .../interfaces/hydrex/IHydrexGauge.sol | 15 +++ contracts/contracts/mocks/MockHydrexGauge.sol | 97 ------------------- .../algebra/OETHSupernovaAMOStrategy.sol | 3 +- .../algebra/StableSwapAMMStrategy.sol | 20 +++- .../hydrex/OETHbHydrexAMOStrategy.sol | 11 ++- contracts/contracts/strategies/hydrex/TODO.md | 49 ---------- .../sonic/SonicSwapXAMOStrategy.sol | 3 +- contracts/deploy/base/048_oethb_hydrex_amo.js | 45 +-------- contracts/deploy/deployActions.js | 6 +- contracts/test/_fixture-base.js | 23 +---- contracts/utils/addresses.js | 9 +- 11 files changed, 60 insertions(+), 221 deletions(-) create mode 100644 contracts/contracts/interfaces/hydrex/IHydrexGauge.sol delete mode 100644 contracts/contracts/mocks/MockHydrexGauge.sol delete mode 100644 contracts/contracts/strategies/hydrex/TODO.md diff --git a/contracts/contracts/interfaces/hydrex/IHydrexGauge.sol b/contracts/contracts/interfaces/hydrex/IHydrexGauge.sol new file mode 100644 index 0000000000..b0855d5dc7 --- /dev/null +++ b/contracts/contracts/interfaces/hydrex/IHydrexGauge.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title IHydrexGauge + * @notice Minimal interface exposing the staked-token getter used by the + * Hydrex GaugeV2 (>= v2.5). Hydrex renamed `TOKEN()` to `stakeToken()` + * in v2.5; the rest of the gauge surface (deposit / withdraw / + * getReward / emergency / emergencyWithdraw / balanceOf) is + * ABI-compatible with `IAlgebraGauge` and is invoked through that + * interface elsewhere in the strategy. + */ +interface IHydrexGauge { + function stakeToken() external view returns (address); +} diff --git a/contracts/contracts/mocks/MockHydrexGauge.sol b/contracts/contracts/mocks/MockHydrexGauge.sol deleted file mode 100644 index d2f11072cc..0000000000 --- a/contracts/contracts/mocks/MockHydrexGauge.sol +++ /dev/null @@ -1,97 +0,0 @@ -// 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"; - -/** - * @title MockHydrexGauge - * @notice Test-only mock implementing the subset of the IAlgebraGauge interface - * that StableSwapAMMStrategy and the AMO behavior suite invoke - * (TOKEN, balanceOf, totalSupply, deposit, withdraw, getReward, - * emergency, emergencyWithdraw, owner, DISTRIBUTION, - * notifyRewardAmount). Used by the OETHb Hydrex AMO fork-test fixture - * until Hydrex deploys the real GaugeV2 for the superOETHb/WETH pool. - */ -contract MockHydrexGauge { - using SafeERC20 for IERC20; - - address public immutable TOKEN; - address public immutable rewardToken; - address public immutable owner; - address public immutable DISTRIBUTION; - - mapping(address => uint256) public balanceOf; - uint256 public totalSupply; - bool public emergency; - - /** - * @param _token Pool LP token that gets staked in the gauge. - * @param _rewardToken Reward token (HYDX in production). - * @param _owner Address allowed to toggle emergency mode. - * @param _distribution Address allowed to call notifyRewardAmount. - */ - constructor( - address _token, - address _rewardToken, - address _owner, - address _distribution - ) { - TOKEN = _token; - rewardToken = _rewardToken; - owner = _owner; - DISTRIBUTION = _distribution; - } - - function deposit(uint256 _amount) external { - require(!emergency, "Gauge: emergency"); - IERC20(TOKEN).safeTransferFrom(msg.sender, address(this), _amount); - balanceOf[msg.sender] += _amount; - totalSupply += _amount; - } - - function withdraw(uint256 _amount) external { - require(!emergency, "Gauge: emergency"); - balanceOf[msg.sender] -= _amount; - totalSupply -= _amount; - IERC20(TOKEN).safeTransfer(msg.sender, _amount); - } - - function emergencyWithdraw() external { - uint256 bal = balanceOf[msg.sender]; - balanceOf[msg.sender] = 0; - totalSupply -= bal; - if (bal > 0) { - IERC20(TOKEN).safeTransfer(msg.sender, bal); - } - } - - function getReward() external { - uint256 bal = IERC20(rewardToken).balanceOf(address(this)); - if (bal > 0) { - IERC20(rewardToken).safeTransfer(msg.sender, bal); - } - } - - function activateEmergencyMode() external { - require(msg.sender == owner, "Gauge: not owner"); - emergency = true; - } - - function stopEmergencyMode() external { - require(msg.sender == owner, "Gauge: not owner"); - emergency = false; - } - - /// @notice Matches the real IGauge.notifyRewardAmount(address,uint256) - /// signature so the AMO behavior suite can fund rewards. - function notifyRewardAmount(address _token, uint256 _amount) external { - require(msg.sender == DISTRIBUTION, "Gauge: not distribution"); - require(_token == rewardToken, "Gauge: wrong token"); - IERC20(rewardToken).safeTransferFrom( - msg.sender, - address(this), - _amount - ); - } -} diff --git a/contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol b/contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol index a9d2de0b61..04eccabf7d 100644 --- a/contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol +++ b/contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol @@ -7,6 +7,7 @@ pragma solidity ^0.8.0; * @author Origin Protocol Inc */ import { StableSwapAMMStrategy } from "./StableSwapAMMStrategy.sol"; +import { IGauge } from "../../interfaces/algebra/IAlgebraGauge.sol"; contract OETHSupernovaAMOStrategy is StableSwapAMMStrategy { /** @@ -15,6 +16,6 @@ contract OETHSupernovaAMOStrategy is StableSwapAMMStrategy { * @param _gauge Address of the Supernova gauge for the pool. */ constructor(BaseStrategyConfig memory _baseConfig, address _gauge) - StableSwapAMMStrategy(_baseConfig, _gauge) + StableSwapAMMStrategy(_baseConfig, _gauge, IGauge(_gauge).TOKEN()) {} } diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol index 9caee8f427..709c190e5c 100644 --- a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol +++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol @@ -159,10 +159,18 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { * @param _baseConfig The `platformAddress` is the address of the Algebra pool. * The `vaultAddress` is the address of the Origin Vault. * @param _gauge Address of the Algebra gauge for the pool. + * @param _gaugeStakeToken The pool LP token address as reported by the + * gauge. The inheriting contract is expected to resolve this via + * whichever getter its gauge exposes (e.g. `IGauge.TOKEN()` for + * legacy GaugeV2 ≤ v2.4 or `IHydrexGauge.stakeToken()` for + * Hydrex GaugeV2 ≥ v2.5) and pass the result here. The constructor + * verifies it matches `_baseConfig.platformAddress`. */ - constructor(BaseStrategyConfig memory _baseConfig, address _gauge) - InitializableAbstractStrategy(_baseConfig) - { + constructor( + BaseStrategyConfig memory _baseConfig, + address _gauge, + address _gaugeStakeToken + ) InitializableAbstractStrategy(_baseConfig) { // Read the oToken address from the Vault address oTokenMem = IVault(_baseConfig.vaultAddress).oToken(); address assetMem = IVault(_baseConfig.vaultAddress).asset(); @@ -178,9 +186,11 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { IPair(_baseConfig.platformAddress).isStable() == true, "Pool not stable" ); - // Check the gauge is for the pool + // Check the gauge is wired to the expected pool LP token. The + // inheriting contract is responsible for fetching `_gaugeStakeToken` + // from whichever getter the underlying gauge variant exposes. require( - IGauge(_gauge).TOKEN() == _baseConfig.platformAddress, + _gaugeStakeToken == _baseConfig.platformAddress, "Incorrect gauge" ); oTokenPoolIndex = IPair(_baseConfig.platformAddress).token0() == diff --git a/contracts/contracts/strategies/hydrex/OETHbHydrexAMOStrategy.sol b/contracts/contracts/strategies/hydrex/OETHbHydrexAMOStrategy.sol index fbc5c4c28b..6e9467e13d 100644 --- a/contracts/contracts/strategies/hydrex/OETHbHydrexAMOStrategy.sol +++ b/contracts/contracts/strategies/hydrex/OETHbHydrexAMOStrategy.sol @@ -7,14 +7,21 @@ pragma solidity ^0.8.0; * @author Origin Protocol Inc */ import { StableSwapAMMStrategy } from "../algebra/StableSwapAMMStrategy.sol"; +import { IHydrexGauge } from "../../interfaces/hydrex/IHydrexGauge.sol"; contract OETHbHydrexAMOStrategy is StableSwapAMMStrategy { /** * @param _baseConfig The `platformAddress` is the address of the Hydrex superOETHb/WETH pool. * The `vaultAddress` is the address of the OETHBase Vault. - * @param _gauge Address of the Hydrex gauge for the pool. + * @param _gauge Address of the Hydrex gauge for the pool. Hydrex GaugeV2 + * (>= v2.5) renamed `TOKEN()` to `stakeToken()`, which is what we + * resolve here and forward to the parent. */ constructor(BaseStrategyConfig memory _baseConfig, address _gauge) - StableSwapAMMStrategy(_baseConfig, _gauge) + StableSwapAMMStrategy( + _baseConfig, + _gauge, + IHydrexGauge(_gauge).stakeToken() + ) {} } diff --git a/contracts/contracts/strategies/hydrex/TODO.md b/contracts/contracts/strategies/hydrex/TODO.md deleted file mode 100644 index 76bd4bfc4f..0000000000 --- a/contracts/contracts/strategies/hydrex/TODO.md +++ /dev/null @@ -1,49 +0,0 @@ -# OETHb Hydrex AMO — open issues - -This file tracks the loose ends from the initial Hydrex AMO PR. **Delete this -file once every item below is resolved.** - -## Blockers (must clear before mainnet deploy) - -- [ ] **Live Hydrex gauge address.** `addresses.base.HydrexOETHb_WETH.gauge` - in `contracts/utils/addresses.js` is currently `0x0000…0000`. Replace with - the real Hydrex gauge once it is deployed for the superOETHb/WETH pool. - - Verify on-chain that it is a `GaugeV2`-shaped staking gauge (exposes - `TOKEN()`, `deposit(uint256)`, `withdraw(uint256)`, `getReward()`, - `emergency()`, `emergencyWithdraw()`), **not** a non-staking - `GaugeIncentiveCampaign` (the `0xac39…9993`-style contract). - - The deploy script `deploy/base/048_oethb_hydrex_amo.js` no-ops while the - placeholder is in place; it will start running its full body the moment a - non-zero gauge address is set. - -- [ ] **HYDX reward token verification.** `addresses.base.HYDX` is - `0x00000e7efa313F4E11Bfff432471eD9423AC6B30` (per docs). Once the gauge is - live, assert `gauge.rewardToken() == addresses.base.HYDX` — ideally with a - `require(...)` in `048_oethb_hydrex_amo.js` so a wrong address fails the - deploy loudly. - -## Cleanup once gauge is live - -- [ ] **Delete the mock gauge.** Once the live gauge address is in - `addresses.base.HydrexOETHb_WETH.gauge`, the fork-test fixture - short-circuits to the live-gauge branch and the mock is no longer touched. - Remove: - - `contracts/contracts/mocks/MockHydrexGauge.sol` - - the `_mockHydrexGaugeIfNeeded` helper in `contracts/test/_fixture-base.js` - - the `if (hydrexGaugeIsMock)` HYDX-allowance block (also in the fixture) - - the `hydrexGaugeIsMock` field from the fixture's return value - - the `USING MOCK HYDREX GAUGE — replace …` warning log - - Fork tests should keep passing with no other changes. - -- [ ] **Tune `scenarioConfig` magnitudes** in - `test/strategies/base/oethb-hydrex-amo.base.fork-test.js` once the pool has - real bootstrapped liquidity. The current numbers were sized for the - fixture-seeded ~150 / 150 pool; live-pool numbers can probably go up. - -## Nice-to-have - -- [ ] Confirm the L2 initialize-time governor pattern in - `deployOETHbHydrexAMOStrategyImplementation` (currently passes - `addresses.base.timelock`) matches whichever convention the rest of - `deploy/base/*` uses for proxy `initialize(impl, governor, data)` calls. diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index 5fd18c8dfe..44fd3d2f33 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -7,6 +7,7 @@ pragma solidity ^0.8.0; * @author Origin Protocol Inc */ import { StableSwapAMMStrategy } from "../algebra/StableSwapAMMStrategy.sol"; +import { IGauge } from "../../interfaces/algebra/IAlgebraGauge.sol"; contract SonicSwapXAMOStrategy is StableSwapAMMStrategy { /** @@ -15,6 +16,6 @@ contract SonicSwapXAMOStrategy is StableSwapAMMStrategy { * @param _gauge Address of the SwapX gauge for the pool. */ constructor(BaseStrategyConfig memory _baseConfig, address _gauge) - StableSwapAMMStrategy(_baseConfig, _gauge) + StableSwapAMMStrategy(_baseConfig, _gauge, IGauge(_gauge).TOKEN()) {} } diff --git a/contracts/deploy/base/048_oethb_hydrex_amo.js b/contracts/deploy/base/048_oethb_hydrex_amo.js index 99a5baa9c0..a4c5f41856 100644 --- a/contracts/deploy/base/048_oethb_hydrex_amo.js +++ b/contracts/deploy/base/048_oethb_hydrex_amo.js @@ -1,61 +1,26 @@ const { deployOnBase } = require("../../utils/deploy-l2"); const addresses = require("../../utils/addresses"); -const { isBaseForkTest } = require("../../utils/hardhat-helpers"); const { deployOETHbHydrexAMOStrategyImplementation, } = require("../deployActions"); -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; - module.exports = deployOnBase( { deployName: "048_oethb_hydrex_amo", }, async ({ deployWithConfirmation, ethers }) => { - let gaugeAddress = addresses.base.HydrexOETHb_WETH.gauge; - - // Mock gauge fallback path. ONLY allowed in Base fork tests. Any other - // Base context (live mainnet deploy, local fork node, anything where - // IS_TEST !== "true") must hard-fail so a strategy backed by a mock or - // zero gauge can never escape into production. - if (gaugeAddress === ZERO_ADDRESS) { - if (!isBaseForkTest) { - throw new Error( - "Hydrex gauge for superOETHb/WETH is not deployed yet. Refusing " + - "to deploy the strategy with a placeholder/mock gauge outside of " + - "Base fork tests. Set addresses.base.HydrexOETHb_WETH.gauge to " + - "the live Hydrex GaugeV2 (and confirm gauge.rewardToken() == " + - "addresses.base.HYDX) before running this deploy." - ); - } - - console.warn( - "USING MOCK HYDREX GAUGE — replace addresses.base.HydrexOETHb_WETH.gauge " + - "with the live Hydrex GaugeV2 once it has been deployed for the " + - "superOETHb/WETH pool." - ); - - const { timelockAddr } = await getNamedAccounts(); - await deployWithConfirmation("MockHydrexGauge", [ - addresses.base.HydrexOETHb_WETH.pool, - addresses.base.HYDX, - timelockAddr, // owner - timelockAddr, // distribution - ]); - const cMockGauge = await ethers.getContract("MockHydrexGauge"); - gaugeAddress = cMockGauge.address; - } - // 1. Deploy the OETHb Hydrex AMO proxy await deployWithConfirmation("OETHbHydrexAMOProxy"); const cOETHbHydrexAMOProxy = await ethers.getContract( "OETHbHydrexAMOProxy" ); - // 2. Deploy & initialize the strategy implementation against the resolved - // gauge address (live or mock). + // 2. Deploy & initialize the strategy implementation against the live + // Hydrex gauge configured in addresses.base.HydrexOETHb_WETH.gauge. const cOETHbHydrexAMOStrategy = - await deployOETHbHydrexAMOStrategyImplementation(gaugeAddress); + await deployOETHbHydrexAMOStrategyImplementation( + addresses.base.HydrexOETHb_WETH.gauge + ); // 3. Connect to the OETHBase Vault as IVault const cOETHBaseVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 124e4e3385..a429921f44 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -927,11 +927,13 @@ const deployOETHbHydrexAMOStrategyImplementation = async (gaugeAddress) => { cOETHbHydrexAMOStrategyProxy.address ); - // Initialize OETHb Hydrex AMO Strategy via the proxy + // Initialize OETHb Hydrex AMO Strategy via the proxy. + // Reward token is oHYDX (call option on HYDX). The Hydrex gauge emits oHYDX + // from getReward(); off-chain plumbing exercises/sells it. const depositPriceRange = parseUnits("0.01", 18); // 1% or 100 basis points const initData = cOETHbHydrexAMOStrategy.interface.encodeFunctionData( "initialize(address[],uint256)", - [[addresses.base.HYDX], depositPriceRange] + [[addresses.base.oHYDX], depositPriceRange] ); await withConfirmation( // prettier-ignore diff --git a/contracts/test/_fixture-base.js b/contracts/test/_fixture-base.js index d6f2d10834..a4ad0e864c 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -412,30 +412,12 @@ async function oethbHydrexAMOFixture( "IGauge", await cOETHbHydrexAMOStrategy.gauge() ); + // Hydrex gauge emits oHYDX (call option on HYDX), not HYDX directly. const hydrexRewardToken = await ethers.getContractAt( erc20Abi, - addresses.base.HYDX + addresses.base.oHYDX ); - // Detect whether 048 deployed a MockHydrexGauge. The mock only exists in - // Base fork tests; live deploys use the real gauge. - const mockDeployment = await deployments.getOrNull("MockHydrexGauge"); - const hydrexGaugeIsMock = - !!mockDeployment && - mockDeployment.address.toLowerCase() === hydrexGauge.address.toLowerCase(); - - // The behavior suite calls `gauge.notifyRewardAmount(token, amount)` from - // the impersonated DISTRIBUTION address, which uses `transferFrom`. On a - // real Hydrex Voter→Gauge wiring this allowance is pre-set during gauge - // creation. The mock has no such pre-set allowance, so set it here. - if (hydrexGaugeIsMock) { - const distributionAddr = await hydrexGauge.DISTRIBUTION(); - const distributionSigner = await impersonateAndFund(distributionAddr); - await hydrexRewardToken - .connect(distributionSigner) - .approve(hydrexGauge.address, ethers.constants.MaxUint256); - } - // Impersonate the OETHBase Vault so tests can call deposit/withdraw on the // strategy directly. const oethbVaultSigner = await impersonateAndFund(oethbVault.address); @@ -549,7 +531,6 @@ async function oethbHydrexAMOFixture( hydrexPool, hydrexGauge, hydrexAMOStrategy: cOETHbHydrexAMOStrategy, - hydrexGaugeIsMock, }; } diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index b53c00c781..6251583c52 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -469,14 +469,17 @@ addresses.base.childLiquidityGaugeFactory = // Base Hydrex addresses.base.HYDX = "0x00000e7efa313F4E11Bfff432471eD9423AC6B30"; +// Hydrex gauges emit oHYDX (a call option on HYDX, redeemable for HYDX with +// USDC), not HYDX directly. The strategy receives oHYDX from gauge.getReward() +// and forwards it to the multichain strategist multisig, which exercises / +// sells it off-chain. +addresses.base.oHYDX = "0xa1136031150e50b015b41f1ca6b2e99e49d8cb78"; addresses.base.hydrexVoter = "0xc69E3eF39E3fFBcE2A1c570f8d3ADF76909ef17b"; addresses.base.HydrexOETHb_WETH = {}; addresses.base.HydrexOETHb_WETH.pool = "0xEB9ebc2dEF5aa715C0CED10749cbdC15Ac27f632"; -// TODO: replace with the live Hydrex gauge once it has been deployed for the -// superOETHb/WETH pool. Re-verify that gauge.rewardToken() == HYDX before deploy. addresses.base.HydrexOETHb_WETH.gauge = - "0x0000000000000000000000000000000000000000"; + "0x762aEFD13Ec33eb916f124E26336a148177eB093"; addresses.base.CCIPRouter = "0x881e3A65B4d4a04dD529061dd0071cf975F58bCD"; From a3a59d5570b322d27fd91b19c5a28427b7366b07 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 5 May 2026 21:46:19 +0200 Subject: [PATCH 5/5] adjust harvester --- contracts/deploy/base/048_oethb_hydrex_amo.js | 23 +++++++++++++++++-- contracts/deploy/deployActions.js | 4 ++-- .../base/oethb-hydrex-amo.base.fork-test.js | 14 ++++++++--- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/contracts/deploy/base/048_oethb_hydrex_amo.js b/contracts/deploy/base/048_oethb_hydrex_amo.js index a4c5f41856..633971ad9c 100644 --- a/contracts/deploy/base/048_oethb_hydrex_amo.js +++ b/contracts/deploy/base/048_oethb_hydrex_amo.js @@ -29,6 +29,16 @@ module.exports = deployOnBase( cOETHBaseVaultProxy.address ); + // 4. Connect to the OETHBase harvester proxy. Use the same harvester + // that AerodromeAMOStrategy uses (OETHHarvesterSimple via the + // OETHBaseHarvesterProxy) so reward token flows go through the + // standard Origin harvester pipeline. + const cHarvesterProxy = await ethers.getContract("OETHBaseHarvesterProxy"); + const cHarvester = await ethers.getContractAt( + "OETHHarvesterSimple", + cHarvesterProxy.address + ); + return { name: "Deploy OETHb Hydrex AMO Strategy on Base", actions: [ @@ -44,11 +54,20 @@ module.exports = deployOnBase( signature: "addStrategyToMintWhitelist(address)", args: [cOETHbHydrexAMOProxy.address], }, - // Set the harvester address on the strategy + // Set the harvester address on the strategy. Rewards (oHYDX) flow + // strategy → harvester → strategist via OETHHarvesterSimple's + // harvestAndTransfer. { contract: cOETHbHydrexAMOStrategy, signature: "setHarvesterAddress(address)", - args: [addresses.base.multichainStrategist], + args: [cHarvesterProxy.address], + }, + // Mark the strategy as supported on the harvester so + // harvestAndTransfer(strategy) doesn't revert. + { + contract: cHarvester, + signature: "setSupportedStrategy(address,bool)", + args: [cOETHbHydrexAMOProxy.address, true], }, ], }; diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index a429921f44..87bb6950ac 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -904,8 +904,8 @@ const deployOETHbHydrexAMOStrategyImplementation = async (gaugeAddress) => { const sDeployer = await ethers.provider.getSigner(deployerAddr); // Default to the addresses entry so any other caller still works; the - // 048_oethb_hydrex_amo deploy script always passes the resolved address - // (live gauge or, in fork tests only, a freshly-deployed MockHydrexGauge). + // 048_oethb_hydrex_amo deploy script always passes the live Hydrex gauge + // address explicitly. const _gauge = gaugeAddress || addresses.base.HydrexOETHb_WETH.gauge; const cOETHbHydrexAMOStrategyProxy = await ethers.getContract( diff --git a/contracts/test/strategies/base/oethb-hydrex-amo.base.fork-test.js b/contracts/test/strategies/base/oethb-hydrex-amo.base.fork-test.js index a6dc51ccd4..d800bb819a 100644 --- a/contracts/test/strategies/base/oethb-hydrex-amo.base.fork-test.js +++ b/contracts/test/strategies/base/oethb-hydrex-amo.base.fork-test.js @@ -30,9 +30,9 @@ describe("Base Fork Test: OETHb Hydrex AMO Strategy", function () { expect(fixture.hydrexGauge.address).to.not.equal(ZERO_ADDRESS); }); - it("Strategy.harvesterAddress() is the multichain strategist", async () => { + it("Strategy.harvesterAddress() is the OETHBase harvester", async () => { expect(await fixture.hydrexAMOStrategy.harvesterAddress()).to.equal( - addresses.base.multichainStrategist + fixture.harvester.address ); }); @@ -50,6 +50,14 @@ describe("Base Fork Test: OETHb Hydrex AMO Strategy", function () { ) ).to.equal(true); }); + + it("Harvester has the strategy marked as supported", async () => { + expect( + await fixture.harvester.supportedStrategies( + fixture.hydrexAMOStrategy.address + ) + ).to.equal(true); + }); }); shouldBehaveLikeAlgebraAmoStrategy(async () => { @@ -132,7 +140,7 @@ describe("Base Fork Test: OETHb Hydrex AMO Strategy", function () { swapOTokensToPool: "0.1", }, harvest: { - collectedBy: "strategist", + collectedBy: "harvester", }, };