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/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/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 new file mode 100644 index 0000000000..6e9467e13d --- /dev/null +++ b/contracts/contracts/strategies/hydrex/OETHbHydrexAMOStrategy.sol @@ -0,0 +1,27 @@ +// 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"; +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. 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, + IHydrexGauge(_gauge).stakeToken() + ) + {} +} 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 new file mode 100644 index 0000000000..633971ad9c --- /dev/null +++ b/contracts/deploy/base/048_oethb_hydrex_amo.js @@ -0,0 +1,75 @@ +const { deployOnBase } = require("../../utils/deploy-l2"); +const addresses = require("../../utils/addresses"); +const { + deployOETHbHydrexAMOStrategyImplementation, +} = require("../deployActions"); + +module.exports = deployOnBase( + { + deployName: "048_oethb_hydrex_amo", + }, + async ({ deployWithConfirmation, ethers }) => { + // 1. Deploy the OETHb Hydrex AMO proxy + await deployWithConfirmation("OETHbHydrexAMOProxy"); + const cOETHbHydrexAMOProxy = await ethers.getContract( + "OETHbHydrexAMOProxy" + ); + + // 2. Deploy & initialize the strategy implementation against the live + // Hydrex gauge configured in addresses.base.HydrexOETHb_WETH.gauge. + const cOETHbHydrexAMOStrategy = + await deployOETHbHydrexAMOStrategyImplementation( + addresses.base.HydrexOETHb_WETH.gauge + ); + + // 3. Connect to the OETHBase Vault as IVault + const cOETHBaseVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); + const cVault = await ethers.getContractAt( + "IVault", + 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: [ + // 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. Rewards (oHYDX) flow + // strategy → harvester → strategist via OETHHarvesterSimple's + // harvestAndTransfer. + { + contract: cOETHbHydrexAMOStrategy, + signature: "setHarvesterAddress(address)", + 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 3bb91cbcbb..87bb6950ac 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -899,6 +899,55 @@ const deployOETHSupernovaAMOStrategyImplementation = async () => { return cOETHSupernovaAMOStrategy; }; +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 live Hydrex gauge + // address explicitly. + const _gauge = gaugeAddress || addresses.base.HydrexOETHb_WETH.gauge; + + 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], + _gauge, + ] + ); + + const cOETHbHydrexAMOStrategy = await ethers.getContractAt( + "OETHbHydrexAMOStrategy", + cOETHbHydrexAMOStrategyProxy.address + ); + + // 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.oHYDX], 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 +1319,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..a4ad0e864c 100644 --- a/contracts/test/_fixture-base.js +++ b/contracts/test/_fixture-base.js @@ -1,6 +1,7 @@ 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"); @@ -363,10 +364,181 @@ mocha.after(async () => { } }); +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" + ); + } + + // `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, nick, strategist } = fixture; + + const cfg = { + assetMintAmount: config?.assetMintAmount || 0, + depositToStrategy: config?.depositToStrategy || false, + balancePool: config?.balancePool || false, + poolAddWethAmount: config?.poolAddWethAmount || 0, + poolAddOethAmount: config?.poolAddOethAmount || 0, + }; + + // 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", + cOETHbHydrexAMOProxy.address + ); + const hydrexPool = await ethers.getContractAt( + "IPair", + await cOETHbHydrexAMOStrategy.pool() + ); + const hydrexGauge = await ethers.getContractAt( + "IGauge", + await cOETHbHydrexAMOStrategy.gauge() + ); + // Hydrex gauge emits oHYDX (call option on HYDX), not HYDX directly. + const hydrexRewardToken = await ethers.getContractAt( + erc20Abi, + addresses.base.oHYDX + ); + + // 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, + }; +} + 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..d800bb819a --- /dev/null +++ b/contracts/test/strategies/base/oethb-hydrex-amo.base.fork-test.js @@ -0,0 +1,188 @@ +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 OETHBase harvester", async () => { + expect(await fixture.hydrexAMOStrategy.harvesterAddress()).to.equal( + fixture.harvester.address + ); + }); + + 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); + }); + + it("Harvester has the strategy marked as supported", async () => { + expect( + await fixture.harvester.supportedStrategies( + 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. + // 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: "harvester", + }, + }; + + 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..6251583c52 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -467,6 +467,20 @@ addresses.base.OETHb_WETH.gauge = "0x9da8420dbEEBDFc4902B356017610259ef7eeDD8"; addresses.base.childLiquidityGaugeFactory = "0xe35A879E5EfB4F1Bb7F70dCF3250f2e19f096bd8"; +// 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"; +addresses.base.HydrexOETHb_WETH.gauge = + "0x762aEFD13Ec33eb916f124E26336a148177eB093"; + addresses.base.CCIPRouter = "0x881e3A65B4d4a04dD529061dd0071cf975F58bCD"; addresses.base.MerklDistributor = "0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd";